Skip to content

Myanglog

Kotlin으로 JPA @ElementCollection 사용하기

1 min read

최근 실무에서 오랜만에 JPA의 @ElementCollection 기능을 사용하게되었다. 많이 까먹은데다 정리해둔 것도 없어 처음 쓸때도 여러 번 검색하며 고민했고, 이런저런 에러도 마주하게 되어 간단히 정리해보려 한다.

@ElementCollection 이란?

  • JPA의 @ElementCollection은 값 타입 collection을 매핑할 때 사용할 수 있는 기능이다.
  • @Entity가 아닌 기본 타입이나 Embeddable 클래스로 정의된 컬렉션을 참조할 때 사용한다.
  • db상으로는 별도의 테이블을 생성하게 된다.

간단한 예시로 유저의 ‘신청’이라는 엔티티가 있고, ‘희망하는 날짜’를 여러개 체크할 수 있다고 가정하면 아래와 같이 구성해볼 수 있다.

1@Entity
2class Apply(
3 userId: Long,
4 desiredDates: MutableList<LocalDate>
5) {
6 @Id
7 @GeneratedValue(strategy = GenerationType.IDENTITY)
8 val id: Long = 0
9
10 val userId: Long = userId
11
12 @ElementCollection
13 @CollectionTable(name = "apply_desired_date", joinColumns = [JoinColumn(name = "apply_id")])
14 var desiredDates: MutableList<LocalDate> = desiredDates
15 protected set
16}
  • @CollectionTable 이라는 어노테이션이 함께 필요하다. 위 코드에서 여러개의 값을 저장하는 테이블은 apply_desired_date 이고, join은 apply_id 라는 컬럼으로 하게 된다.
  • 위 엔티티에 대한 DDL은 대략 아래처럼 쓸 수 있다.
1create table apply (
2 id bigint not null auto_increment,
3 user_id bigint not null,
4 primary key (id)
5);
6create table apply_desired_date (
7 apply_id bigint not null,
8 desired_dates date not null
9);

@ElementCollection 을 쓰게 되었는가

  • 가끔 엔티티의 필드에 여러개의 단순한 value(Int, String, LocalDate 타입 등)를 저장해야하는 상황이 생긴다. 여러 가지 방법들이 있을 수 있다.
    • 정해진 구분자(comma(,) 등)로 연결해서 string으로 하나의 컬럼에 저장
    • mysql과 같이 json타입을 허용하는 db라면 json형식으로 저장하는 방법
    • JPA의 @ElementCollection 사용
    • JPA의 일대다 매핑 사용
  • 나의 경우 엔티티가 특정 status를 가질 때만 사용되는 여러개의 날짜를 저장해야했다. 단순히 저장하고 그대로 변환해서 조회하는 로직만 필요했다면 위의 방식을 좀더 생각해볼 수 있었을텐데, 저장된 날짜들에 대해 조회해와서 오늘 날짜와 같으면 알림을 보내는 배치가 필요했다. ⇒ 조회 성능을 고려했을 때 + 엔티티 상에서 관리하기도 편할 것 같아서 사용하게 되었다.
  • 엔티티에 완전히 종속적인 필드 + 단순한 value가 아니라면 다대일/일대다 관계로 relation을 걸거나 간접 참조 방식을 사용하는게 좋을 것 같다.

List 타입을 사용하다가 만난 에러

Kotlin의 List는 Immutable한 타입이다. 값을 변경해야한다면 MutableList를 사용한다. @ElementCollection 을 사용한 필드를 수정할 때 ‘완전히 새로운 List를 할당한다면 MutableList를 쓰지 않아도 괜찮지 않을까?’ 라고 무심코 생각하며 List를 사용했더니 아래와 같은 에러가 생겼다.

1java.lang.UnsupportedOperationException
2 at java.base/java.util.AbstractList.remove(AbstractList.java:167)
3 at java.base/java.util.AbstractList$Itr.remove(AbstractList.java:387)
4 at java.base/java.util.AbstractList.removeRange(AbstractList.java:598)
5 at java.base/java.util.AbstractList.clear(AbstractList.java:243)
6 at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:580)
7 at org.hibernate.type.CollectionType.replace(CollectionType.java:757)
8 at org.hibernate.type.TypeHelper.replace(TypeHelper.java:168)
9 ...
10 at jdk.proxy3/jdk.proxy3.$Proxy110.merge(Unknown Source)
11 at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:669)
12 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
13 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
14 at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
15 at java.base/java.lang.reflect.Method.invoke(Method.java:568)

JPA 내부적으로는 AbstractList.remove()를 호출했는데 List타입에는 없는 메서드라 UnsupportedOperationException이 발생한 것.

그 전에 호출된 아래의 CollectionType의 replaceElements() 를 보면 iterator를 돌며 한땀한땀 target객체를 비운 뒤 original 객체에 add 하는 것을 확인할 수 있다.

1/**
2 * Replace the elements of a collection with the elements of another collection.
3 *
4 * @param original The 'source' of the replacement elements (where we copy from)
5 * @param target The target of the replacement elements (where we copy to)
6 * @param owner The owner of the collection being merged
7 * @param copyCache The map of elements already replaced.
8 * @param session The session from which the merge event originated.
9 * @return The merged collection.
10 */
11 public Object replaceElements(
12 Object original,
13 Object target,
14 Object owner,
15 Map copyCache,
16 SharedSessionContractImplementor session) {
17 java.util.Collection result = ( java.util.Collection ) target;
18 result.clear();
19
20 // copy elements into newly empty target collection
21 Type elemType = getElementType( session.getFactory() );
22 Iterator iter = ( (java.util.Collection) original ).iterator();
23 while ( iter.hasNext() ) {
24 result.add( elemType.replace( iter.next(), null, session, owner, copyCache ) );
25 }
26/* ....너무 길어서 생략 */
27
28 return result;
29 }

⇒ 수정될일 없는/수정되면 안되는 Collection이라면 List를 사용하고, 그렇지 않은 경우에는 MutableList를 사용해야한다는 결론을 얻었다. (쓰고보니 너무나 당연한 문장)

예제 코드 repository

https://github.com/myangw/jpa-test

ApplyServiceTest 클래스에서 테스트를 돌려보며 확인 가능하다.