14장. 컬렉션과 부가 기능


14.1 컬렉션

  • 자바에서 기본 제공하는 컬렉션 클래스들은 JPA에서 다음과 같은 경우 사용할 수 있음
    • @OneToMany, @ManyToMany 사용해 일대다, 다대다 엔티티 관계 매핑 시
    • @ElementCollection 사용해 값 타입 하나 이상 보관 시

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



14.1.1 JPA와 컬렉션

  • 하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용
    • 하이버네이트에서 제공하는 내장 컬렉션을 래퍼 컬렉션이라고도 부름
  • 인터페이스마다 사용되는 래퍼 컬렉션이 다름
// PersistentBag
@OneToMany
Collection<Member> collection = new ArrayList<Member>();

// PersistentBag
@OneToMany
List<Member> list = new ArrayList<Member>();

// PersistentSet
@OneToMany
Set<Member> set = new HashSet<Member>();

// PersistentList
@OneToMany
@OrderColumn
List<Member> orderColumnList = new ArrayList<Member>();
컬렉션 인터페이스 내장 컬렉션 중복 허용 순서 보관
Collection, List PersistentBag O X
Set PersistentSet X X
List + @OrderColumn PersistentList O O



14.1.2 Collection, List

  • PersistentBag 을 래퍼 컬렉션으로 사용
  • ArrayList 로 초기화 하면 됨
  • 중복을 허용하므로 add() 메소드로 엔티티 추가 시 중복 여부 비교하지 않고 저장만 함
    • add() 메소드는 항상 true 리턴
    • ∴ 엔티티 추가해도 지연 로딩된 컬렉션 초기화 X



14.1.3 Set

  • PersistentSet 을 래퍼 컬렉션으로 사용
  • HashSet 으로 초기화
  • 중복을 허용하지 않아 add() 메소드로 추가시 equals() 메소드로 중복 체크
    • 없으면 객체 추가 후 true 리턴, 있으면 추가 안하고 false 리턴
    • HashSet 은 해시 알고리즘 쓰므로 hashCode() 도 비교에 사용
    • ∴ 엔티티 추가시 지연 로딩된 컬렉션을 초기화 함



14.1.4 List + @OrderColumn

  • List 인터페이스에 @OrderColumn 붙이면 순서 있는 컬렉션으로 인식
    • DB에 순서용 컬럼을 매핑해서 관리
  • PersistentList 사용


@OrderColumn의 단점

@Entity
public class Board {
  @Id
  @GeneratedValue
  private Long id;

  private String title;
  private String content;

  @OneToMany(mappedBy = "board")
  // @OrderColumn 엔티티로 위치 지정해줄 필드 설정
  @OrderColumn(name = "position") // 매핑은 Board 에 되어있지만 1:N 관계 특성상 N 쪽인 comment 테이블에 저장됨
  private List<Comment> comments = new ArrayList<Comment>();

  ...
}

...

@Entity
public class Comment {
  @Id
  @GeneratedValue
  private Long id;

  private String comment;

  @ManyToOne
  @JoinColumn(name = "BOARD_ID")
  private Board board;

  ...
}
  • @OrderColumn 을 Board 엔티티에서 매핑하므로 Comment 는 position 값 알 수 없음
    • 따라서 Comment INSERT 시에 값 저장되지 않고, Board 의 comments 값으로 comment의 position 값 UPDATE 하는 쿼리 추가로 실행됨
  • List 변경 시 연관된 위치값 다 저장해야 함
    • 중간거 하나 삭제하면 그 뒤에거 위치값을 다 변경해야 함
  • 중간에 비어있는 위치값 있으면 List 에 null 저장됨
    • ex) position = 1, 3, 4 인 경우 2번 들어갈 위치에 null이 저장됨



14.1.5 @OrderBy

  • DB의 ORDER BY 절을 사용하여 컬렉션을 정렬
    • 순서용 컬럼을 매핑 안해도 됨
  • 모든 컬렉션에서 사용할 수 있음
@Entity
public class Team {
  @Id
  @GeneratedValue
  private Long id;
  private String name;

  @OneToMany(mappedBy = "team")
  @OrderBy("username DESC, id ASC")
  private Set<Member> members = new HashSet<Member>();

  ...
}

...

@Entity
public class Member {
  @Id
  @GeneratedValue
  private Long id;

  @Column(name = "member_name")
  private String username;

  @ManyToOne
  private Team team;

  ...
}
  • 위와 같이 @OrderBy 어노테이션에 정렬 기준 필드와 차순 설정하면 ORDER BY 절에 해당 값 넣어서 조회
    • 그 순서대로 컬렉션 정렬되어 나옴

하이버네이트는 Set@OrderBy 적용해서 조회하면 순서 유지하려고 HashSet 대신 LinkedHashSet 을 사용함




14.2 @Converter

  • 컨버터로 엔티티의 데이터를 변환하여 DB 저장할 수 있음
    • ex) 엔티티에서 boolean 타입으로 쓰고있는 필드를 DB 저장시 0, 1 대신 Y, N 으로 저장하려면 컨버터를 사용
# 매핑할 테이블
CREATE TABLE member (
  id VARCHAR(255) NOT NULL,
  username VARCHAR(255),
  vip VARCHAR(1) NOT NULL,
  PRIMARY KEY (id)
)
// Member 엔티티
@Entity
// @Convert(converter = BooleanToYNConverter.class, attributeName = "vip")  => 필드 대신 클래스 레벨에 설정해도 됨
public class Member {
  @Id
  private String id;
  private String username;

  // BooleanToYNConverter 로 boolean 값을 Y, N 문자로 변환해서 DB 저장
  @Convert(converter=BooleanToYNConverter.class)
  private boolean vip;

  // Getter, Setter
  ...

}

...

// BooleanToYNConverter
@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);
  }
}
  • 컨버터 클래스는 @Converter 어노테이션 사용, ATtributeConverter 인터페이스 구현해야 함
    • 제네릭에 <현재 타입, 변환할 타입> 을 지정
// AttributeConverter
// X: 현재 타입, Y: 변환할 타입
public interface AttributeConverter<X, Y> {
  public X convertToDatabaseColumn (X attribute);
  public Y convertToEntityAttribute(Y dbData);
}
  • AttributeConverter 인터페이스에는 구현해야 할 메소드 두 개 있음
    • convertToDatabaseColumn(): 엔티티 데이터를 DB 컬럼 저장용 데이터로 변환
    • convertToEntityAttribute() DB 에서 조회한 컬럼 데이터를 엔티티 데이터로 변환



14.2.1 글로벌 설정

  • 컨버터 타겟 타입에 별도 설정 없이 적용하려면 컨버터 작성 시 @Converter(autoApply = true) 옵션 사용
  • 위 옵션 사용 시 @Converter 지정 안해도 자동으로 컨버터가 적용 됨
// 컨버터 글로벌 설정
@Converter(autoApply = true)  // 이렇게 설정하면 모든 Boolean 타입에 대해 자동으로 컨버터 적용
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
  ...
}


@Convert 속성

속성 기능 기본값
converter 사용할 컨버터 지정  
attributeName 컨버터 적용할 필드 지정  
disableConversion 글로벌 컨버터나 상속 받은 컨버터 사용 안함 false




14.3 리스너

  • JPA 리스너 기능을 사용하면 엔티티 생명주기에 따른 이벤트 처리 가능
  • 이벤트를 활용하여 등록 일자, 수정 일자, 등록자, 수정자 등에 대한 기록을 리스너 하나로 처리할 수 있음



14.3.1 이벤트 종류

  1. PostLoad:
    • 엔티티가 영속성 컨텍스트에 조회된 직후
    • refresh 를 호출한 직후
    • 2차 캐시에 저장되어 있어도 호출됨
  2. PrePersist:
    • persist() 호출하여 엔티티를 영속성 컨텍스트에 관라하기 직전
      • 식별자 생성 전략 사용한 경우 엔티티에 식별자 아직 없음
    • 새로운 인스턴스 merge 할 때
  3. PreUpdate:
    • flush, commit 호출해서 엔티티를 DB에 수정하기 직전
  4. PreRemove:
    • remove() 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전
    • 삭제 명령어로 영속성 전이 일어날 때
    • orphanRemoval 에 대해서는 flushcommit 시에 호출됨
  5. PostPersist:
    • flush, commit 호출해서 엔티티를 DB 저장한 직후
      • 식별자가 항상 존재
    • 식별자 생성 전략이 IDENTITY이면 식별자 생성 위해 persist() 호출하며 DB에 해당 엔티티 저장
      • 이 때는 persist() 호출한 직후에 바로 PostPersist 호출됨
  6. PostUpdate:
    • flush, commit 호출해서 엔티티를 DB에 수정한 직후
  7. PostRemove:
    • flush, commit 호출해서 엔티티를 DB에 삭제한 직후



14.3.2 이벤트 적용 위치

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


엔티티에 직접 사용

@Entity
public class Duck {
  @Id
  @GeneratedValue
  public Long id;

  private String name;

  @PrePersist
  public void prePersist() {
    System.out.println("Duck.perpersist id = " + id); // Duck.prePersist id = null
  }

  @PostPersist
  public void postPersist() {
    System.out.println("Duck.postPersist id = " + id);  // Duck.postPersist id = 1
  }

  @PostLoad
  public void postLoad() {
    System.out.println("Duck.postLoad");
  }

  @PreRemove
  public void preRemove() {
    System.out.println("Duck.preRemove");
  }

  @PostRemove
  public void postRemove() {
    System.out.println("Duck.postRemove");
  }

  ...
}


별도의 리스너 등록

@Entity
@EntityListeners(DuckListener.class)
public class Duck {
  ...
}

public class DuckListener {
  @PrePersist
  // 특정 타입이 확실하면 해당 타입으로 파라미터 받을 수 있음
  private void prePersist(Object obj) {
    System.out.println("DuckListener.prePersist obj = [" + obj + "]");
  }

  @PostPersist
  // 특정 타입이 확실하면 해당 타입으로 파라미터 받을 수 있음
  private void postPersist(Object obj) {
    System.out.println("DuckListener.postPersist obj = [" + obj + "]");
  }
}


기본 리스너 사용

  • 모든 엔티티의 이벤트 처리하려면 META-INF/orm.xml 에 디폴트 리스너로 등록
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings ...>
  <persistence-unit-metadata>
    <persistence-unit-defaults>
      <entity-listeners>
        <entity-listener class="jpabook.jpashop.comain.test.listener.DefaultListener" />
      </enetiy-listeners>
    </persistence-unit-defaults>
  </persistence-unit-metadata>
</entity-mappings>
  • 여러 리스너 등록시 이벤트 호출 순서는 다음과 같음
    1. default listener
    2. 부모 클래스 listener
    3. listener
    4. entity


더 세밀한 설정

  • @ExcludeDefaultListeners: 기본 리스너 무시
  • @ExcludeSuperclassListeners: 상위 클래스 이벤트 리스너 무시
@Entity
@EntityListeners(DuckListener.class)
@ExcludeDefaultListeners
@ExcludeSuperclassListeners
public class Duck extends BaseEntity {
  ...
}




14.4. 엔티티 그래프

  • 엔티티 조회시 연관된 엔티티 조회하려면 FetchType.EAGER 설정하거나 fetch join 사용
  • fetch join 사용 시 함께 조회할 엔티티에 따라 유사한 JPQL이 여러개 생길 수 있음
    • ex) member 와 같이 조회하는 order 조회, item 과 같이 조회하는 order 조회 …
      • 모두 order 조회지만 member 또는 item 을 같이 조회하는지에 따라 fetch join 부분만 달라짐
  • JPA 2.1에 추가된 엔티티 그래프 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티 선택 가능
    • JPQL은 데이터 조회 기능만 수행
    • 연관된 엔티티 함께 조회 기능은 엔티티 그래프 사용
    • 엔티티 조회시점에 연관된 엔티티 들을 함께 조회하는 기능



14.4.1 Named 엔티티 그래프

// Order 조회시 연관된 Member 도 함께 조회되는 엔티티 그래프 예제
@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
  @NamedAttributeNode("member")
})
@Entity
@Table(name = "orders")
public class Order {
  @Id
  @GeneratedValue
  @Column(name = "order_id")
  private Long id;

// 지연 로딩으로 설정되어 있지만 엔티티 그래프에서 함께 조회할 속성으로 member 선택해서 
// 이 엔티티 그래프 사용시 Order 조회하면 Member 도 같이 조회됨
  @JoinColumn(name = "member_id")
  @ManyToOne(fetch = FetchType.LAZY, optional = false)
  private Member member;

  ...
}
  • Named 엔티티 그래프는 @NamedEntityGraph 로 정의
    • name: 엔티티 그래프의 이름 정의
    • attributeNodes: @NamedAttributeNode 사용해서 그 값으로 함께 조회할 속성 선택
  • 여러개 정의하려면 @NamedEntityGraphs 사용



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

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

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

Order order = em.find(Order.class, orderId, hints);
  • Named entity graph 사용 시 em.getEntityGraph(Named 엔티티 그래프 이름) 으로 정의한 엔티티 그래프 찾아오면 됨
  • JPA 힌트 기능을 통해 동작하며, 힌트 키로 javax.persistence.fetchgraph 사용하고 값으로 찾아온 엔티티 그래프 사용
  • 만들어진 힌트를 엔티티 조회시 파라미터로 넘기면 됨



14.4.3 subgraph

  • Order -> OrderItem -> Item 까지 함께 조회하는 경우
    • Item 은 Order 가 관리하는 필드 아님
    • 이런 경우 subgraph 를 사용
@NamedEntityGRaph(
  name = "Order.withAll", 
  attributeNodes = {
    @NamedAttributeNode("member"),
    @NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
  },
  subgraphs = @NapedSubgraph(name = "orderItems", attributeNode = {
    @NamedAttributeNode("item")
  })
)
@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;

  @OneToMany(mappedBy = "order", casacade = CascadeType.ALL)
  private List<OrderItem> orderItems = new ArrayList<OrderItem>();

  ...
}

...

@Entiy
@Table(name = "order_item")
public class OrderItem {
  @Id
  @GeneratedValue
  @Column(name = "order_item_id")
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "team_id")
  private Item item;

  ...
}

...

Map hints = new HashMap();
hints put("javax.persistence.fetchgraph", em.getEntityGraph("Order.withAll"));

Order order = em.find(Order.class, orderId, hints);
  • Named 엔티티 그래프 Order.withAll 로 Order -> Member, Order -> OrderItem, OrderItem -> Item 객체 그래프를 함께 조회
    • OrderItem -> Item 은 Order 의 객체 그래프가 아니므로 subgraphs 속성에 @NamedSubgraph 어노테이션으로 값 넣어서 정의



14.4.4 JPQL 에서 엔티티 그래프 사용

  • JPQL 에서도 em.find() 와 동일하게 힌트만 추가하면 됨
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();



14.4.5 동적 엔티티 그래프

  • 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);
  • em.createEntityGraph(Order.class) 로 Order 에 대한 엔티티 그래프를 동적으로 생성
  • graph.addAttributeNodes("member")Order.member 속성을 엔티티 그래프에 추가
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttriubteNode("member");
Subgraph<OrderItem> orderItems = graph.addSubgraph("orderItems");
orderItems.addAttributeNodes("item");

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

Order order = em.find(Order.class, orderId, hints);
  • 마찬가지로 엔티티 그래프 동적으로 생성한 후 graph.addSubgraph("orderItems") 로 서브 그래프 생성
  • orderItems.addAttributeNodes("item") 으로 서브 그래프에 item 속성 포함시킴



14.4.6 엔티티 그래프 정리

  • ROOT 에서 시작
    • 엔티티 그래프는 항상 조회하는 엔티티의 ROOT 에서 시작
  • 이미 로딩된 엔티티
    • 영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프 적용 안됨
      • 아직 초기화 되지 않은 프록시에는 적용됨
  • fetchgraph, loadgraph의 차이
    • javax.persistence.fetchgraph
      • 엔티티 그래프에 선택한 속성만 함께 조회
    • javax.persistence.loadgraph
      • 선택한 속성 + 글로벌 fetch 모드가 FetchType.EAGER 로 설정된 연관관계도 포함해서 함께 조회