////
Search
Duplicate
🎱

14장. 컬렉션과 부가 기능

JPA가 지원하는 컬렉션의 종류와 중요한 부가 기능들을 살펴보자.
컬렉션: 다양한 컬렉션과 특징을 설명한다.
컨버터: 엔티티의 데이터를 변환해서 데이터베이스에 저장한다.
리스너: 엔티티에서 발생한 이벤트를 처리한다.
엔티티 그래프: 엔티티를 조회할 때 연관된 엔티티들을 선택해서 함께 조회한다.

1. 컬렉션

JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음과 같은 경우에 사용할 수 있다.
@OneToMany, @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 때
@ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때
@MapKey* 어노테이션을 사용해 Map을 매핑하려는 경우
JPA 명세에는 자바 Collection API에 대한 특별한 언급이 없으므로 구현체에 따라 제공하는 기능이 조금씩 다를 수 있으며 여기선 하이버네이트를 기준으로 설명한다.

1. JPA와 컬렉션

하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 래핑해서 사용한다.
하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어서 래퍼 컬렉션으로도 부른다.
컬렉션 인터페이스
하이버네이트 내장 컬렉션
순서 유지
Colleciton, List
PersistentBag
X
Set
PersistentSet
X
List + @OrderColumn
PersistentList
O

2. Collection, List

Collection, List는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하므로 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.

3. Set

Set은 엔티티 추가 시 중복된 엔티티가 있는지 비교해야 한다. 따라서 엔티티를 추가할 대 지연 로딩된 컬렉션을 초기화한다.

4. List + @OrderColumn

List 인터페이스에 @OrderColumn을 추가하면 특수한 컬렉션으로 인식하는데, 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다는 의미다.
순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리한다.
이를 통해 List의 위치 값을 보관하면 편리할 것 같지만 다음에서 설명하는 것처럼 실무에서 사용하기에는 단점이 많다. 따라서 직접 값을 관리하거나 @OrderBy를 사용하는 것을 권장한다.
@OrderColumn을 Board 엔티티에서 매핑하므로 Comment는 POSITION의 값을 알 수 없다. 따라서 Comment를 저장할 때 POSITION 값이 저장되지 않는다.
이를 보완하기 위해 POSITION의 값을 변경하는 SQL이 추가로 동작한다.
List를 변경하면 많은 위치 값도 변경해야 한다.
값이 없는 경우 조회한 List에 null이 보관되며 이는 NullPointerException이 발생할 가능성을 만든다.

5. @OrderBy

@OrderBy는 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 따라서 순서용 컬럼을 매핑하지 않아도 된다.

2. @Converter

컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
예를 들어 회원의 VIP 여부를 boolean 변수로 사용하고 있다면 데이터베이스에는 보통 TINYINT 데이터형으로 1 또는 0이 저장되나 Y, N을 사용하고 싶다면 컨버터를 사용한다.
해당 필드에 @Convert(converter=해당 기능을 구현한 클래스.class)를 작성해주면 된다.
이때 해당 구현 클래스는 AttributeConverter 인터페이스를 구현해야 하며 제네릭에 현재 타입과 변환할 타입을 지정해야 한다.
해당 인터페이스에는 convertToDatabaseColumn()convertToEntityAttribute()가 존재한다.

1. 글로벌 설정

모든 boolean 타입에 컨버터를 적용하려면 해당 구현 클래스에 @Converter(autoApply = true) 옵션을 적용하면 된다.

3. 리스너

모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야하는 요구사항이 발생했다고 가정하자.
이때 어플리케이션의 삭제 로직을 하나씩 찾아 로그를 남기는 것을 너무나 비효율적이다. JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.

1. 이벤트 종류

PostLoad, PrePersist, PreUpdate, PreRemove, PostPersist, PostUpdate, PostRemove

2. 이벤트 적용 위치

엔티티에 직접 적용
엔티티에 메소드를 만들고 이벤트 종류에 따른 어노테이션을 붙이면 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드가 실행된다.
별도의 리스너 등록
@EntityListeners 어노테이션을 사용하여 리스너 클래스를 매핑해주면 된다.
이때 리스너 클래스에는 이벤트 종류에 따른 어노테이션과 메소드를 구현해주어야 한다.
기본 리스너 사용
META-INF/orm.xml에 기본 리스너로 등록하여 모든 엔티티의 이벤트를 처리하도록 만들 수 있다.
여러 리스너가 등록되었을 경우, 호출 순서는 다음과 같다.
기본 리스너 → 부모 클래스의 리스너 → 자신의 리스너 → 엔티티
더 세밀한 설정
javax.persistence.ExcludeDefaultListeners: 기본 리스너 무시
javax.persistence.ExcludeSuperclassListeners: 상위 클래스 이벤트 리스너 무시

4. 엔티티 그래프

엔티티를 조회할 때 연관된 엔티티를 함께 조회하려면 글로벌 fetch 옵션을 FetchType.EAGER로 설정한다.
또는 JPQL에서 페치 조인을 사용하면 된다.
글로벌 fetch 옵션은 광범위한 영향을 끼치고 변경할 수 없다는 단점이 있다.
엔티티를 조회할 때 연관된 엔티티를 함께 조회해야 한다면 JPQL의 페치 조인을 사용한다.

1. Named 엔티티 그래프

Named 엔티티 그래프는 @NamedEntityGraph로 정의한다.
name: 엔티티 그래프의 이름을 정의한다.
attributeNodes: 함께 조회할 속성을 선택한다. 이때 @NamedAttributeNode를 사용하고 그 값으로 함께 조회할 속성을 선택한다.

2. em.find()에서 엔티티 그래프 사용

em.find(Order.class, orderId, hints)와 같이 Order 엔티티를 조회할 때 힌트 정보를 포함하면 Order.withMember 엔티티 그래프를 사용해 함께 조회한다.

3. subgraph

엔티티 그래프의 서브 그래프를 조회하고 싶은 경우, @NamedSubgraph를 사용하면 된다.
객체 그래프가 아니기 때문에 subgraphs 속성으로 정의해야 한다.

4. JPQL에서 엔티티 그래프 사용

JPQL에서 엔티티 그래프를 사용하는 방법은 힌트만 추가하면 된다.

5. 동적 엔티티 그래프

엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메소드를 사용하면 된다.
public <T> EntityGraph<T> createEntityGraph(Class<T> rootType);

6. 엔티티 그래프 정리

Root에서 시작
엔티티 그래프는 항상 조회하는 엔티티의 ROOT에서 시작해야 한다.
이미 로딩된 엔티티
영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프가 적용되지 않는다.
fetchgraph, loadgraph의 차이
fetchgraph는 엔티티 그래프에 선택한 속성만 함께 조회한다. 반면 loadgraph는 선택한 속성뿐 아니라 글로벌 fetch 모드가 FetchType.EAGER로 설정된 연관관계도 포함해서 함께 조회한다.