백엔드/JPA

[JPA] 값 타입 컬렉션 @ElementCollection과 @CollectionTable 활용 예시

happy_life 2022. 8. 22. 15:24

db에는 컬렉션을 저장할 수 없습니다. 따라서 jpa의 값 타입 컬렉션은 @ElementCollection과 @CollectionTable 어노테이션을 통해 구현할 수 있습니다. 이번 글에서는 이 어노테이션들을 활용한 값 타입 컬렉션의 개념과 특징, 수정에 대해 알아보겠습니다.

 

 

목차

1. 값 타입 컬렉션의 개념과 특징

2. 값 타입 컬렉션의 수정

 

 

값 타입 컬렉션의 개념과 특징

1. 개념

값 타입을 컬렉션에 담아 사용하는 것을 의미합니다.DB에서는 따로 컬렉션을 저장할 수 없으므로, 컬렉션에 해당하는 테이블을 하나 추가하여 컬렉션을 구현합니다. 이를 위해 @ElementCollection과 @CollectionTable 어노테이션을 사용합니다.

 

 

2. 특징

① 값 타입 컬렉션은 값 타입과 마찬가지로, 따로 생명주기를 가지지 않고 엔티티와 같은 생명주기를 갖습니다. 일대다 관계에서 CASCADE = ALL, orphanREmoval = TRUE를  설정해준 것과 같습니다. 아래의 예를 통해 이해해보겠습니다.

 

 

Member.class

@Entity
public class Member {


    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

getter setter가 있다고 가정합니다.

 

 

실행.class

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 {
            Member member = new Member();
            member.setUsername("member1");

            member.getAddressList().add(new Address("city1", "street1", "1"));
            member.getAddressList().add(new Address("city2", "street2", "2"));

            em.persist(member);
            tx.commit();
        } catch (Exception e) {
            e.printStackTrace();
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}

 

아래는 실행 결과입니다. 따로 값 타입 컬렉션을 persist해주지 않았음에도 insert쿼리가 작동하는 것을 알 수 있습니다.

 

실행결과

 

 

 

② 지연로딩전략을 사용합니다.

try {
    Member member = new Member();
    member.setUsername("member1");

    member.getAddressList().add(new Address("city1", "street1", "1"));
    member.getAddressList().add(new Address("city2", "street2", "2"));

    em.persist(member);

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

    System.out.println("================== START ================");
    Member foundMember = em.find(Member.class, member.getId());
    System.out.println("================== 지연로딩 ================");
    List<Address> addressList = foundMember.getAddressList();
    for (Address address : addressList) {
        System.out.println("address.getCity() = " + address.getCity());
    }

    tx.commit();

member를 find할 때 바로 값 타입 컬렉션을 꺼내오는 것이 아니라, 필요한 순간에 지연로딩 됩니다. 아래의 결과를 보고 이해할 수 있습니다.

 

지연로딩

 

 

 

 

값 타입 컬렉션의 수정

remove 후 add 하는 방식

값 타입 컬렉션은 call by reference 이므로 값만 단순히 수정해줄 수 없습니다. 따라서 remove후 add하는 방식으로 값 타입을 수정할 수 있지만 독특한 부분이 있습니다. update로 원하는 부분만  수정해주는 것이아니라, delete로 모두 삭제하고 insert 쿼리가 나가기 때문입니다.

 

코드 예시

Member member = new Member();
member.setUsername("member1");

member.getAddressList().add(new Address("city1", "street1", "1"));
member.getAddressList().add(new Address("city2", "street2", "2"));

em.persist(member);

Member foundMember = em.find(Member.class, member.getId());


foundMember.getAddressList().remove(new Address("city1", "street1", "1"));
foundMember.getAddressList().add(new Address("newCity1", "street1", "1"));

tx.commit();

 

delete로 모두 지우는 상황

 

 

이유

1. 값 타입은 엔티티와 다르게 식별자 개념이 없습니다. 값은 변경하면 추적이 어렵습니다.  JPA에서는 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장하게 됩니다.

 

 

값 타입 컬렉션의 대안

실무에서는 상황에 따라 값 타입 컬렉션 대신 컬렉션을 Entity로 승격하고 일대다 관계를 고려할 수 있습니다. 아래의 코드와 비교해보면 좋을 것같습니다.

 

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressEntityList = new ArrayList<>();