•
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로 설정된 연관관계도 포함해서 함께 조회한다.