꿈꾸는 새벽하늘

14장. 컬렉션과 부가 기능 본문

🌿 Spring & Spring Boot/📗 자바 ORM 표준 JPA 프로그래밍

14장. 컬렉션과 부가 기능

rovemin 2023. 8. 28. 01:05

JPA가 지원하는 컬렉션의 종류와 중요한 부가 기능

  • 컬렉션: 다양한 컬렉션과 특징
  • 컨버터: 엔티티의 데이터를 변환해서 데이터베이스에 저장
  • 리스너: 엔티티에서 발생한 이벤트 처리
  • 엔티티 그래프: 엔티티를 조회할 때 연관된 엔티티들을 선택해서 함께 조회

1. 컬렉션

JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원한다.

이 컬렉션은 @OneToMany, @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 때, 그리고 @ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때 사용 가능하다.

 

자바 컬렉션 인터페이스의 특징

  • Collection: 자바가 제공하는 최상위 컬렉션이다. 하이버네이트는 중복을 허용하고 순서를 보장하지 않는다고 가정한다.
  • Set: 중복을 허용하지 않는 컬렉션이다. 순서를 보장하지 않는다.
  • List: 순서가 있는 컬렉션이다. 순서를 보장하고 중복을 허용한다.
  • Map: Key, Value 구조로 되어 있는 특수한 컬렉션이다.

1) JPA와 컬렉션

하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다.

@Entity
public class Team {

    @Id
    private String id;
    
    @OneToMany
    @JoinColumn
    private Collection<Member> members = new ArrayList<Member>();
    ...
}

인터페이스와 컬렉션 래퍼

// org.hibernate.collection.internal.PersistentBag
@OneToMany
Collection<Member> collection = new ArrayList<Member>();

// org.hibernate.collection.internal.PersistentBag
@OneToMany
List<Member> list = new ArrayList<Member>();

// org.hibernate.collection.internal.PersistentSet
@OneToMany
Set<Member> set = new HashSet<Member>();

// org.hibernate.collection.internal.PersistentList
@OneToMany @OrderColumn
List<Member> orderColumnList = new ArrayList<Member>();

하이버네이트 내장 컬랙션과 특징

컬렉션 인터페이스 내장 컬렉션 중복 허용 순서 보관
Collection, List PersistentBag O X
Set PersistentSet X X
List + @OrderColumn PersistentList O O

2) Collection, List

Collection, List 인터페이스는 중복을 허용하는 컬렉션이고 PersistentBag을 래퍼 컬렉션으로 사용한다. 이 인터페이스는 ArrayList로 초기화한다.

@Entity
public class Parent {
    
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany
    @JoinColumn
    private Collection<CollectionChild> collection = new ArrayList<CollectionChild>();
    
    @OneToMany
    @JoinColumn
    private List<ListChild> list = new ArrayList<ListChild>();
    ...
}

Collection, List는 중복을 허용한다고 가정하므로 객체를 추가하는 add() 메소드는 내부에서 어떤 비교도 하지 않고 항상 true를 반환한다. 같은 엔티티가 있는지 찾거나 삭제할 때는 equals() 메소드를 사용한다.

 

Collection, List는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다.

따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.

3) Set

Set은 중복을 허용하지 않는 컬렉션이고  하이버네이트는 PersistentSet을 래퍼 컬렉션으로 사용한다. 이 인터페이스는 HashSet으로 초기화한다.

@Entity
public class Parent {

    @OneToMany
    @JoinColumn
    private Set<SetChild> set = new HashSet<SetChild>();
}

HashSet은 중복을 허용하지 않으므로 add() 메소드로 객체를 추가할 때마다 equals() 메소드로 같은 객체가 있는지 비교한다. 같은 객체가 없으면 객체를 추가한 뒤 true를 반환하고, 같은 객체가 이미 있어서 추가에 실패하면 false를 반환한다. 참고로 HashSet은 해시 알고리즘을 사용하므로 hashcode()로 함께 사용해서 비교한다.

 

Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야 한다.

따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.

4) List + @OrderColumn

List 인터페이스에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다.

순서가 있다는 것은 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다는 의미다.

하이버네이트는 내부 컬렉션인 PersistentList를 사용한다.

 

@OrderColumn의 단점

  • @OrderColumn을 Board 엔티티에서 매핑하므로 Comment는 POSITION의 값을 알 수 없다. (* Board.comments에 List 인터페이스를 사용하고 @OrderColumn을 추가한 예시 참고)
  • List를 변경하면 연관된 많은 위치 값을 변경해야 한다.
  • 중간에 POSITION 값이 없으면 조회한 List에는 null이 보관된다.

5) @OrderBy

데이터베이스에 순서용 컬럼을 매핑해서 관리한 @OrderColumn과 달리, @OrderBy는 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 따라서 순서용 컬럼을 매핑하지 않아도 된다. 그리고 @OrderBy는 모든 컬렉션에 사용할 수 있다는 장점이 있다.

2. @Converter

컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.

컨버터 클래스는 @Converter 어노테이션을 사용하고 AttributeConverter 인터페이스를 구현해야 한다. 그리고 제네릭에 현재 타입과 변환할 타입을 지정해야 한다.

 

Boolean을 YN으로 변환해주는 컨버터와 AttributeConverter 예시

@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
    
    @Override
    public String convertToDatabaseColumn (Boolean attribute) {
        return (attribute != null && attribute) ? "Y" : "N";
    }
    
    @Override
    public Boolean convertToEntityAttribute (String dbData) {
        return "Y".equals(dbData);
    }
}
public interface AttributeConverter<X,Y> {
    
    public Y convertToDatabaseColumn (X attribute);
    public X convertToEntityAttribute (Y dbData);
}
  • convertToDatabaseColumn(): 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환한다.
  • convertToEntityAttribute(): 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환한다.

글로벌 설정

@Converter(autoApply = true) 옵션을 적용하면 모든 Boolean 타입에 컨버터를 적용할 수 있다.

3. 리스너

JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.

 

이벤트의 종류와 발생 시점

  • PostLoad:  엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후 (2차 캐시에 저장되어 있어도 호출된다)
  • PrePersist: persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출된다. 식별자 생성 전략을 사용한 경우 엔티티에 식별자는 아직 존재하지 않는다. 새로운 인스턴스를 merge할 때도 수행된다.
  • PreUpdate: flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.
  • PreRemove: remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval에 대해서는 flush나 commit 시에 호출된다.
  • PostPersist: flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 전략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출하면서 데이터베이스에 해당 엔티티를 저장하므로 이때는 persist()를 호출한 직후에 바로 PostPersist가 호출된다.
  • PostUpdate: flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.
  • PostRemove: flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.

이벤트 적용 위치

  • 엔티티에 직접 적용
  • 별도의 리스너 등록
  • 기본 리스너 사용

여러 리스너를 등록했을 때 이벤트 호출 순서

  1. 기본 리스너
  2. 부모 클래스 리스너
  3. 리스너
  4. 엔티티

4. 엔티티 그래프

엔티티 그래프 기능은 엔티티를 조회 시점에 연관된 엔티티들을 함께 조회하는 기능이다.

 

엔티티 그래프는 정적으로 정의하는 Named 엔티티 그래프와 동적으로 정의하는 엔티티 그래프가 있다.

1) Named 엔티티 그래프

@NamedEntityGrpah (name = "Order.withMember", attributeNodes = {
    @NamedAttributeNode ("member")
})
@Entity
@Table (name = "ORDERS")
public class Order {
    
    @Id	@GeneratedValue
    @Column (name = "ORDER_ID")
    private Long id;
    
    @ManyToOne (fetch = FetchType.LAZY, optional = false)
    @JoinColumn (name = "MEMBER_ID")
    private Member member;
    ...
}

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

EntityGraph graph = em.getEntity("Order.withMember");

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

Order order = em.find(Order.class, orderId, hints);

JPQL에서 엔티티 그래프 사용

List<Order> resultList = 
    em.createQuery("select o from Order o where o.id = :orderId",
        Order.class)
        .setParameter("orderId", orderId)
        .setHint("javax.persistence.fetchgraph", em.getEntityGraph("Order.withAll"))
        .getResultList();

2) 동적 엔티티 그래프

엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메소드를 사용하면 된다.

public <T> EntityGraph<T> createEntityGraph (Class<T> rootType);

EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

Order order = em.find(Order.class, orderId, hints);

3) 엔티티 그래프 정리

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