BE전문가 프로젝트

값 타입 컬렉션 본문

JPA

값 타입 컬렉션

원호보고서 2022. 11. 2. 22:30

값 타입을 컬렉션에 담아서 사용하는 것을 의미한다.

@Entity
public class Member extends BaseEntity{

    @Id
    @GeneratedValue
    private Long id;

    @Column(name="USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name="TEAM_ID")
    private Team team;

    @OneToOne
    @JoinColumn(name="LOCKER_ID")
    private Locker locker;

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();
}

실행시키면 위에 있는 테이블과 같은 테이블이 만들어진다.

 

값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.(일대다 개념이기 때문에 한 테이블에 넣을 수는 없다.)
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요함

값 타입 저장 예제

 try {
            Address address = new Address("city", "street", "10000");

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setHomeAddress(address);

            member1.getFavoriteFoods().add("치킨");
            member1.getFavoriteFoods().add("피자");
            member1.getFavoriteFoods().add("족발");
            
            member1.getAddressHistory().add(new Address("old1", "street", "1000"));
            member1.getAddressHistory().add(new Address("old2", "street", "10005"));
            
            em.persist(member1);

            Address copiedAddress = new Address("newCity", address.getStreet(), address.getZipcode());
            member1.setHomeAddress(copiedAddress);

            tx.commit();
}

코드를 실행하면 ADDRESS테이블에는 주소 정보, FAVORITE_FOODS에는 음식 값이 각자 맞는 테이블에 입력된 것을 확인할 수 있다. persist시에 모든 값이 동시에 들어가는 것을 확인 할 수 있는데 이것은 라이프 사이클이 같다는 것을 의미한다. 그 이유는 값타입이기 때문이며 모든 라이프 사이클 즉 생명주기가 member에 소속된 것을 알 수 있다. String name도 값타입이고 List<Address>도 마찬가지로 값타입이다. 따라서 별도로 persist를 해줄 필요가 없다.

 

값 타입 조회 예제

값 타입 컬렉션도 지연 로딩 전략 사용

@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();

@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();

지연로딩을 사용하지 않는 다면 member테이블에 있는 정보만 조회가 된다. 따라서 지연로딩을 설정해주는 것이 좋다.

 

값 타입 수정 예제

Member findMember = em.find(Member.class, member1.getId());

Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("home", a.getStreet(), a.getZipcode()));

findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

값 타입 통으로 바꿔야 한다. set 역시 값 타입이기 때문에 삭제 후 생성해주어야 한다.

member에 lifeCycle모두 맏기는 것이다.

public class
JPAMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();                                     //트랜잭션 시작

        try {
            Address address = new Address("city", "street", "10000");

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setHomeAddress(address);

            member1.getFavoriteFoods().add("치킨");
            member1.getFavoriteFoods().add("피자");
            member1.getFavoriteFoods().add("족발");

            member1.getAddressHistory().add(new Address("old1", "street", "1000"));
            member1.getAddressHistory().add(new Address("old2", "street", "10005"));

            em.persist(member1);

            Address copiedAddress = new Address("newCity", address.getStreet(), address.getZipcode());
            member1.setHomeAddress(copiedAddress);

            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member1.getId());

            findMember.getAddressHistory().remove(new Address("old1", "street", "1000"));
            findMember.getAddressHistory().add(new Address("old4", "street", "1000"));
            tx.commit();
        }catch (Exception e){
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}

list에 담겨있는 address클래스 중 변수의 일부분을 수정하고 싶다면 위에 코드처럼 전체를 가져와 삭제후 add로 값을 넣어줘야 한다. 따라서 hashCode가 제대로 구현되어 있지 않으면 로직에 큰 문제가 생긴다.

 

위에 코드를 실행 후 콘솔창들 보면 old1이 포함되어 있는 데이터를 삭제 후 새로운 데이터 즉 old4의 데이터만 insert되기를 기대했지만 old2와 old4 둘다 insert가 되는 것을 확인할 수 있다. 그 이유는 값 타입 컬렉션의 제약사항에서 알아보자

 

 

값 타입 컬렉션의 제약 사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함: null 입력X, 중복 저장X

따라서 사용하지 않는 것을 권장한다.

 

값 타입 컬렉션의 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용

 

값 타입 컬렉션의 사용

추적할 필요도 없고 값이 변경되어도 update 쿼리를 보내지 않을 정도로 단순하게 사용한다면 사용이 가능하지만 되도록이면 Entity를 사용하는 것이 좋다.

엔티티 타입의 특징 값 타입의 특징
식별자O 식별자X
생명 주기 관리 생명 주기를 엔티티에 의존
공유 공유하지 않는 것이 안전(복사해서 사용)
  불변 객체로 만드는 것이 안전

값 타입은 정말 값 타입이라 판단될 때만 사용

엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안됨

식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티

'JPA' 카테고리의 다른 글

JPQL(Java Persistence Query Language)  (0) 2022.11.06
객체지향 쿼리 언어  (0) 2022.11.06
값 타입의 비교  (0) 2022.11.02
값 타입과 불변 객체  (0) 2022.11.02
임베디드 타입(복합 값 타입)  (0) 2022.11.01
Comments