[JPA] 값타입 컬렉션 ( @ElementCollection, @CollectionTable )
지난 포스팅에서 값타입의 개념과 임베디드타입을 다루어보았다.
테이블 컬럼에 삽입 될 데이터는 정합성을 가져야 한다. 컬럼과 매핑되는 엔티티는 정합성을 지킨 데이터를 가져야 한다. 원시타입은 JAVA에서 값복사를 지원하기에 정합성을 가지지만 참조타입은 참조복사를 하기에 다른 객체에서 데이터를 조작할 가능성이 있다. 그러므로 객체(컬렉션)는 참조복사가 아닌 값복사가 지원되어야 한다. 값타입이란 참조복사가 아닌 값복사가 지원되는 객체 및 컬렉션을 의미한다.
이번 포스팅에서는 값타입 중 컬렉션을 다루어 보겠다.
값타입 컬렉션
테이블은 자료구조(컬렉션)를 가질 수 없다. 그러므로 별도의 테이블을 구성해야 한다. 이렇게 만들어진 테이블은 엔티티 테이블과 구분되어야 한다. 값타입 테이블은 본인이 속한 엔티티에 '종속'된다. 엔티티와 엔티티의 종속은 영속성 전이와 고아객체 제거로 구현되지만 이 경우는 값타입 컬렉션임만 명시하면 종속설정이 자동으로 이루어진다.
@ElementCollection, @CollectionTable
@ElementCollection은 해당 컬렉션이 값타입 컬렉션임을 표시하는 어노테이션이다. @ElementCollection 어노테이션은 targetClass 속성을 가진다. targetClass 속성은 컬럼에 매핑 될 때의 타입을 지정한다. 그런데 보통 컬렉션은 제네릭으로 타입을 지정하므로 targetClass를 설정하지 않으면 제네릭 타입에 맞추어 타입이 정해진다.
값타입 컬렉션은 지연로딩(Lazy Loading) 전략을 사용한다. 엔티티가 로딩될 때, 컬렉션의 모든 데이터가 같이 로딩되면 성능저하를 불러올 수 있다. 추가로 컬렉션 데이터가 저장될 테이블의 구체적인 설정을 하고 싶다면 @CollectionTable 어노테이션을 활용하면 된다.
Member 엔티티
@Entity
@Data
public class Member {
@Id @GeneratedValue
@Column (name = "MEMBER_ID")
private Long id;
private String name;
@Embedded //임베디드 타입
private Period period;
@ElementCollection //값타입 컬렉션
@CollectionTable(name = "FAVORITE_FOOD")
private Set<String> favoriteFoods = new HashSet<>();
}
@ElementCollection으로 값타입 컬렉션임을 명시하고 @CollectionTable 어노테이션으로 테이블 이름을 지정하였다. 그럼 간단한 Main 함수를 만들어 실행해보자.
public class Main {
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("h2");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction tx = entityManager.getTransaction();
tx.begin();
try {
//Member 객체 생성
Member member = new Member();
member.setName("이지훈");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("소고기");
member.getFavoriteFoods().add("콜라");
//영속화
entityManager.persist(member);
tx.commit();
}catch (Exception e) {
e.printStackTrace();
tx.rollback();
}finally {
entityManager.close();
}
}
}
Member 테이블
값타입 컬렉션 테이블
값타입 컬렉션과 엔티티 테이블은 '다대일' 관계이다. 값타입 컬렉션 테이블이 '다'이므로 외래키를 갖는다.
주의할 점
값타입 컬렉션도 엔티티처럼 테이블이 생성되지만 한 가지 다른 점이 있다. JPA가 변경감지를 못한다. JPA는 값타입 컬렉션에 객체가 추가되고 삭제되는 것은 감지한다. 엔티티 안에서 일어나는 현상이기 때문이다. 그러나 컬렉션 안의 객체의 필드, 즉 값타입 컬렉션 테이블의 컬럼의 데이터가 변경되는 것은 감지하지 못한다.
그러므로 수정하려면 아예 기존객체를 삭제하고 새로운 객체를 컬렉션에 추가해야 한다. 절대! 객체의 필드가 수정되면 안된다. JPA가 변경감지를 못하여 정합성에 문제가 생긴다. 그러므로 컬렉션에 추가되는 객체는 SETTER 함수가 막힌 불변객체이어야 한다. 위에서 사용한 값타입 컬렉션인 Set<String>의 String도 대표적인 불변객체이다. String같이 JAVA에서 제공하는 불변객체가 아닌, 개발자가 만든 클래스의 객체라면 setter 함수를 막아야 한다.
이와 같은 특성 때문에 값타입 컬렉션은 복잡한 상황에서 쓰이지 않는다.
복잡한 상황은 여러 요구사항이 있는 상황이다. 요구사항이 많으면 컬렉션의 객체는 많은 필드를 가져야한다. 많은 필드가 있으면 각 컬럼이 여러 요인에 의해 수정된다. 수정될 때마다 요인과 관련없는 데이터도 삭제되었다가 다시 재생성되어야 한다. 그래서 Set<String>처럼 많은 필드가 없는 불변객체가 쓰이는 단순한 상황에서 쓰면 좋다.
대표적으로 블로그의 태그가 있다. 블로그 포스트의 태그는 '생성','삭제' 두 가지 요구사항 밖에 없다. 또한 태그는 블로그 포스트 엔티티에 종속된다. 그러므로 태그를 Set<String> tags = new HashMap();으로 만드는 것이 적절하다.
참고자료