14장 컬렉션과 부가 기능
14.1 컬렉션
14.2 @Converter
14.3 리스너
14.4 엔티티 그래프
14.5 정리
컬렉션: 다양한 컬렉션과 특징을 설명한다.
컨버터: 엔티티의 데이터를 변환해서 데이터베이스를 저장한다.
리스너: 엔티티에서 발생한 이벤트를 처리한다.
엔티티 그래프: 엔티티를 조회할 때 연관된 엔티티들을 선택해서 함께 조회한다.
14.1 컬렉션
JPA는 자바에서 기본으로 제공하는 Collection, Listm Set, Map 컬렉션을 지원하고 다음 경우에 이 컬렉션을 사용할 수 있다.
- @OneToMany @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 때
- @ElementCollection을 사용해서 값타입을 하나 이상 보관할 때
- Collection: 자바가 제공하는 최상위 컬렉션, 하이버네이트는 중복을 허용하고 순서를 보장하지 않는다고 가정
- Set: 중복을 허용하지 않는 컬렉션, 순서를 보장하지 않음
- List: 순서가 있으며, 순서를 보장하고 중복을 허용
- Map: Key, Value 구조로 되어 있는 특수한 컬렉션
(참고) Map은 복잡한 매핑에 비해 활용도가 떨어지고, 다른 컬렉션을 사용해도 충분함
하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용
@Entity
public class Team{
@Id
private String id;
@OneToMany
@JoinColumn
private Collection<Member> members = new ArrayList<Member>();
...
}
위 예제의 Team은 members 컬렉션을 필드로 가지고 있음
Team team = new Team();
System.out.println("before persist = " + team.getMembers().getClass());
em.persist(team);
System.out.println("after persist = " + team.getMembers().getClass());
위 코드는 Team을 영속상태로 만드는 코드로,
실행 결과는 아래와 같다.
before persist = class java.util .Arraylist
after persist = class org .hibernate. collection. internal. PersistentBag
결과를 보면 원래 ArrayList 타입이었던 컬렉션이 엔티티를 영속 상태로 만든 직후에 하이버네이트가 제공하는 PersistentBag 타입으로 변경된 것을 볼 수 있다. 하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 이 내장 컬렉션을 사용하도록 참조를 변경한다. (래퍼 컬렉션)
그러므로 하이버네이트를 사용할 때 즉시 초기화해서 사용하는 것을 권장한다.
Collection<Member> members = new ArrayList<Member>();
Collection, List
- Collection, List 인터페이스는 중복을 허용, PersistentBag을 래퍼 컬렉션으로 사용
- ArrayList로 초기화
...
private Collection<CollectionChild> collection = new ArrayList<CollectionChild>();
...
private List<ListChild> list = new ArrayList<ListChild>();
...
- 중복 허용 -> add() 메서드 항상 true
- 비교 -> equals() 메서드 사용
즉, Collection, List는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 하지 않는다.
Set
- 중복을 허용하지 않으며 PersistentSet을 컬렉션 래퍼로 사용
- HashSet으로 초기화
...
private Set<SetChild> set = new HashSet<SetChild>();
...
- 중복 허용 안함 -> add() 메서드 호출 시 equals() 메서드로 같은 객체 있는지 비교 (true/false 반환)
- HashSet은 해시 알고리즘을 사용하므로 hashcode()도 함께 사용해서 비교
Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야 하기 때문에 지연 로딩된 컬렉션을 초기화한다.
List + @OrderColumn
- List에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식
- 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다는 의미
- PersistentList 사용
...
@OneToMany(Mapped by = "board")
@OrderColumn(name = "POSITION")
private List<Comment> comments = new ArrayList<Comment>();
...
순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리
@OrderColumn의 단점
- List를 변경하면 연관된 많은 위치 값을 변경해야 한다.
- 중간에 POSITION값이 없으면 조회한 리스트에는 null 이 보관된다.
@OrderBy
- 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬
- 순서용 컬럼 매핑할 필요 없음
- 모든 컬렉션에 사용할 수 있음
...
@OrderBy("username desc, id asc")
private Set<Member> members = new HashSet<Member>();
...
@OrderBy의 값은 JPQL order by절처럼 엔티티의 필드를 대상으로 한다.
(참고) 하이버네이트는 Set에 @OrderBy를 적용해서 결과를 조회하면 순서를 유지하기 위해 HashSet대신에 LinkedHAshSet을 내부에서 사용한다.
14.2 @Converter
컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
(예시) boolean 타입을 0이나 1과 같은 숫자 대신 Y, N 으로 저장하고 싶을 때
...
VIP VARCHAR(1) NOT NULL,
...
...
@Convert(converter=BooleanToYNConverter.class)
private boolean vip;
...
@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
@Override
public String convertToDatabaseColumn(Boolean attribute) {
return (attribute != null && attribute) ? "Y" : "N";
}
@Override
public String convertToEntityAttribute(String dbData) {
return "Y".equals(dbData)
}
}
- 컨버터클래스는 @Converter 어노테이션 사용
- AttributeConverter 인터페이스 구현
- 제네릭에 현재 타입과 변환할 타입을 지정해주어야 함 (예시에서는 Boolean -> String)
AttributeConverter 인터페이스에서 구현해야 할 두 메서드
- convertToDatabaseColumn(): 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환
- convertToEntityAttribute(): 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 반환
만약 컨버터를 클래스 레벨에서 설정 시 attributeName속성을 사용해서 어떤 필드에서 컨버터를 적용할 시 명시해야 한다. (아래 참고)
@Entity
@Convert(converter = BooleanToYNConverter.class, attributeName = "vip")
public class Member{
...
모든 Boolean 타입에 컨버터를 적용하려면 @Converter(autoApply = true) 옵션을 적용하면 된다. (아래참고)
@Converter(autoApply=true)
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
@Override
public String convertToDatabaseColumn(Boolean attribute) {
return (attribute != null && attribute) ? "Y" : "N";
}
@Override
public String convertToEntityAttribute(String dbData) {
return "Y".equals(dbData)
}
}
위와 같이 컨버터 속성을 지정하면 모든 Boolean타입에 대해 자동으로 컨버터가 적용된다.
14.3 리스너
모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야 한다면 JPA 리스너 기능을 사용하면 된다.
JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.
이벤트는 엔티티에서 직접 받거나 별도의 리스너를 등록해서 받을 수 있다.
- 엔티티에 직접 적용
- 별도의 리스너 등록
- 기본 리스너 사용
엔티티에 직접 적용
@PrePersist
public void prePersist(){
System.out.println("prePersist id = " + id);
}
@Postsist
public void Postsist(){
System.out.println("Postsist id = " + id);
}
...
엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메서드가 실행된다.
(출력 예시 아래 참고)
prePersist id=nul1 (아이디가생성되기전에호출된다.)
postPersist i d=1 (아이디가생성된후에호출된다.)
별도의 리스너 등록
@Entity
@EntityListeners(DuckListener.class)
public class Duck{
...
}
public class DuckListener{
@PrePersist
private void prePersist(Object obj){
System.out.println("DuckListener.prePersist obj = " + obj);
}
}
리스너는 대상 엔티티를 파라미터로 받을 수 있으며 반환 타입은 void로 설정해야 한다.
기본 리스너 사용
META-INF/orm.xml에 기본 리스너로 등록하는 방법이다.
호출순서는
- 기본 리스너
- 부모 클래스 리스너
- 리스너
- 엔티티
순서이다.
(참고) 더 세밀한 설정을 위한 어노테이션
- javax.persistence.ExcludeDefaultListeners: 기본 리스너 무시
- javax.persistence.ExcludeSuperClassListeners: 상위 클래스 이벤트 리스너 무시
이벤트를 잘 활용하면 엔티티에 공통으로 적용하는 등록 일자, 수정일자 처리와 해당 엔티티를 누가 등록하고 수정했는지에 대한 기록을 리스너 하나로 처리할 수 있다.
14.4 엔티티 그래프
- 엔티티를 조회할 때 연관된 엔티티들을 함께 조회하려면 글로벌 fetch 옵션을 FetchType.EAGER로 설정
- 또는 JPQL에서 페치 조인을 사용 -> select o from Order o join fetch o.member
글로벌 페치 옵션은 애플리케이션 전체에 영향을 주고 변경할 수 없다는 단점이 있기 때문에 페치조인을 사용하는 것이 좋다.
엔티티 그래프 기능: 엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능
정적으로 지정하는 Named 엔티티 그래프와 동적으로 정의하는 엔티티 그래프가 있다.
ROOT에서 시작
엔티티 그래프는 항상 조회하는 엔티티의 ROOT에서 시작해야 한다. 당연한 이야기지만 Order엔티티를 조회하는데 Member 부터 시작하는 엔티티 그래프를 사용하면 안된다.
이미 로딩된 엔티티
영속성 컨텍스트에 엔티티가 이미 로딩되어 있으면 엔티티 그래프가 적용되지 않는다.
fetchgraph, loadgraph의 차이
fetchgraph는 그래프에 선택한 속성만 함께 조회하지만 loadgraph는 엔티티 그래프에 선택한 속성뿐만 아니라 글로벌 fetcha 모드가 EAGER로 설정된 연관관계도 포함해서 함께 조회한다.
14.5 정리
'Backend > Spring Boot' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 13장 (0) | 2023.07.04 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 10장 (0) | 2023.05.30 |
[자바 ORM 표준 JPA 프로그래밍] 9장 (0) | 2023.05.23 |
[자바 ORM 표준 JPA 프로그래밍] 8장 (1) | 2023.05.23 |
[자바 ORM 표준 JPA 프로그래밍] 7장 (0) | 2023.05.23 |