본문 바로가기
JPA

[JPA] @ElementCollection, @CollectionTable을 통한 값 타입 컬렉션 사용법

by 당코 2023. 4. 22.

값 타입

int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체이다.

 

값타입 분류

기본값 타입

  • 자바 기본 타입(int, double)
  • 래퍼 클래스(Integer, Long)
  • String

임베디드 타입(embedded type, 복합 값 타입)

컬렉션 값 타입(collection value type)

 

값 타입 컬렉션

값 타입을 컬렉션에 담아서 사용하는 것을 값 타입 컬렉션이라고 한다.

//기본 값 타입 컬렉션
List<String> stringlist = new ArrayList()<>;
//임베디드 값 타입 컬렉션
Set<Address> addressSet = new HashSet<>();

만약 다음과 같이 데이터베이스 안에 값 타입 컬렉션을 저장하려면 어떻게 해야 할까?

 

기본적으로 관계형 데이터베이스에는 컬렉션을 저장할 수 없다.

따라서 컬렉션을 저장하기 위해서는 별도의 테이블을 만들어서 컬렉션을 저장해야 한다.

이때 사용할 수 있는 것이 @ElementCollection과 @CollectionTable이다.

 

@ElementCollection

컬렉션 객체임을 JPA가 알 수 있게 하게 한다.

엔티티가 아닌 값 타입, 임베디드 타입에 대한 테이블을 생성하고 1대다 관계로 다룬다.

 

@CollectionTable

값 타입 컬렉션을 매핑할 테이블에 대한 역할을 지정하는 데 사용한다.

테이블의 이름과 조인정보를 적어줘야 한다.

 

사용법

@Entity
public class Member {

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

    @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<>();
}

값 타입 컬렉션에 @ElementCollect과 @CollectionTable을 붙여주고 @CollectionTable에 Table이름과 join 할 column 정보를 입력해 주면 된다.

예외적으로 기본 값 타입 컬렉션은 컬럼명을 재정의 해줄 수 있다.

 

특징

값 타입 컬렉션은 자신의 라이프 사이클을 가지지 않는다.

이것은 마치 영속성 전이(Cascade)와 고아 객체 제거 기능을 필수로 가진 것과 비슷하다고 볼 수 있다.

또한 기본적으로 @ElementCollection이 LAZY로 설정돼 있어 지연로딩으로 작동한다.

 

만약 값 타입 컬렉션의 값을 수정하기 위해서는 어떻게 해야 할까?

값 타입 컬렉션 안의 데이터를 수정할 때는 일부만 수정하는 것이 아닌 데이터를 삭제 후 변경된 데이터 전체를 새로 추가해줘야 한다.

member.getFavoriteFoods().remove("치킨");
member.getFavoriteFoods().add("피자");

member.getAddressHistory().remove(new Address("oldCity", "street", "10000"));
member.getAddressHistory().add(new Address("newCity", "street", "10000"));

위와 같은 방식으로 수정해야만 사이드이펙트를 없앨 수 있다.

 

제약사항

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

member.getAddressHistory().remove(new Address("oldCity1", "street", "10000"));
member.getAddressHistory().add(new Address("newCity", "street", "10000"));

값 타입 컬렉션에 Address 데이터가 2개 저장되어 있는 상황에서 기존에 있던 데이터 1개를 삭제하고 새로운 데이터를 추가해 주었다.

우리가 기대하는 SQL은 delete 쿼리 1개와 insert 쿼리 1개이다.

하지만 값 타입 컬렉션에서는 주인 엔티티와 연관된 모든 데이터를 삭제하고 다시 저장하기 때문에 Member에 연관된 oldCity1, oldCity2를 모두 삭제하고 oldCity2와 newCity를 insert 한다.

따라서 delete 쿼리 1개와 insert 쿼리 2개가 DB로 나가게 된다.

 

대안

@ElementCollection과 @CollectionTable을 사용해서 값 타입 컬렉션을 사용하는 것은 제약사항이 많아 사용에 어려움이 많다.

따라서 대안은 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용하는 방법이다.

@Entity
public class Member {
...
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
...
}

@Entity
public class AddressEntity {
	
    @Id @GenerateValue
    private Long id;
    
    private Address address;
}

Address라는 값 타입을 한번 감싸는 AddressEntity를 새롭게 만들고 저장하는 방식이다.

cascade를 ALL, orphanRemoval을 true로 설정해 주면 주인 엔티티에 의존하면서 기존의 제약사항을 해결할 수 있다.

'JPA' 카테고리의 다른 글

[JPA] 프록시와 지연로딩  (2) 2023.03.05
[JPA] @MappedSuperclass 사용법  (0) 2023.03.03
[JPA] 상속관계 매핑  (0) 2023.03.03
[JPA] 연관관계 매핑  (0) 2023.02.28
[JPA] 기본 키 매핑 방법  (0) 2023.02.27