스터디/JPA 스터디

[JPA] 컬렉션과 부가 기능

제이온 (Jayon) 2022. 3. 1.

jpa-study에서 스터디를 진행하고 있습니다.

컬렉션

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

 

JPA와 컬렉션

@Entity
public class Team {

    @Id
    private String id;

    @OneToMany
    @JoinColumn
    private Collection<Member> members = new ArrayList<>();
  • 하이버네이트는 엔티티를 영속 상태로 만들 때, 컬렉션 필드를 하이버네이트에서 준비한 컬렉션을 감싸서 사용한다.
  • 위 members의 타입은 ArrayList지만, 영속 상태가 되면 PersistentBag 타입으로 한 단계 감싼 래퍼 컬렉션이 된다.

 

Collection, List

  • List는 중복을 허용하므로 객체를 추가할 때 내부적으로 어떤 비교도 하지 않고 true를 반환한다.
  • 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.

 

Set

  • Set은 중복을 허용하지 않는 컬렉션이다.
  • Set은 객체를 추가할 때마다 equals() 를 통해 매번 같은 객체가 있는지 확인한다. 만약 HashSet을 사용 중이라면 해시 코드를 추가로 비교한다.
  • 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.

 

List + @OrderColumn

  • List 인터페이스에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다.
  • 순서가 있다는 의미는 데이터베이스에 순서 값을 저장해서 조회한다는 뜻이다.

 

@OneToMany(mappedBy = "board")
@OrderColumn(name = "POSITION")
private List<Comment> comments = new ArrayList<>();

 

  • 위 코드는 Board와 Comment가 일대다 관계이고, 테이블의 일대다 특성상 위치 값은 N쪽에 저장해야하므로 POSITION 컬럼은 Comment 테이블에 저장된다.

 

Untitled

 

@OrderColumn의 단점

  • @OrderColumn을 Board 엔티티에서 매핑하므로 Comment는 POSITION 값을 알 수 없다. 그래서 Comment를 삽입할 때 POSITION 값이 저장되지 않으므로, Board.comments의 위치 값을 활용하여 POSITION을 업데이트하는 쿼리가 추가로 날아간다.
  • List를 변경하면 연관된 수많은 위치 값을 변경해야 한다.
  • 중간에 POSITION이 없으면 조회한 List에는 Null이 보관된다.

 

@OrderBy

  • 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다.

 

@OneToMany(mappedBy = "board")
@OrderBy("id desc, username asc")
private List<Comment> comments = new ArrayList<>();

 

@Converter

  • 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
    • ex) Boolean 타입을 데이터베이스에 저장할 때 Y 또는 N으로 저장하고 싶을 경우

 

@Entity
public class Member {

    @Id
    private String id;

    private String username;

    @Convert(converter=BooleanToYNConverter.class)
    private boolean vip;
}

@Converter(autoApply = true) // 모든 엔티티에 컨버터가 적용된다. 디폴트는 false
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);    
    }
}

 

리스너

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

 

이벤트 종류

Untitled

  • PreLoad
    • 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후
  • PrePersist
    • persist() 를 호출하여 엔티티를 영속성 컨텍스트에 관리하기 직전
  • PreUpdate
    • flush나 commit을 호출하여 엔티티를 데이터베이스에 수정하기 직전
  • PreRemove
    • remove() 를 호출하여 엔티티를 영속성 컨텍스트에서 삭제하기 직전
    • orphanRemoval에 대해 flush나 commit이 호출된 후
  • PostPersist
    • flush나 commit을 호출하여 엔티티를 데이터베이스에 저장한 직후
  • PostUpdate
    • flush나 commit을 호출하여 엔티티를 데이터베이스에 수정한 직후
  • PostRemove
    • flush나 commit을 호출하여 엔티티를 데이버테이스에 삭제한 직후

 

이벤트 적용 위치

엔티티에 직접 적용

@Entity
public class Duck {

    @Id
    @GeneratedValue
    public Long id;

    private String name;

    @PrePersist
    public void prePersist() {
        ...
    }

    // 위와 같이 나머지 이벤트 오버라이딩
}

 

별도의 리스너 등록

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

public class DuckListener {

    // 특정 타입이 확실해야 함.
    @PrePersist
    private void prePersist(Object obj) {
        ...
    }

    // 위와 같이 나머지 이벤트 오버라이딩
}

 

엔티티 그래프

  • 엔티티를 조회할 때 연관된 엔티티를 함께 조회하려면 FetchType.EAGER로 설정하면 되지만, 사용하지 않는 연관 관계까지 로딩하므로 효율적이지 않다.
  • JPQL에서 페치 조인을 사용할 수 있지만, 같은 JPQL을 중복해서 작성하는 경우가 많다.
    • 주문만 조회
    • 주문과 회원을 함께 조회
    • 주문과 주문 상품을 함께 조회
  • 엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있다.
  • 따라서 JPQL은 데이터를 조회하는 기능만 수행하면 되고, 연관된 엔티티를 함께 조회하는 기능은 엔티티 그래프를 사용하면 된다.

 

Named 엔티티 그래프

  • 주문을 조회할 때 연관된 회원을 같이 조회한다.

 

@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
    @NamedAttributeNode("member")
})
@Entity
public class Order {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

 

  • member가 지연 로딩으로 설정되어 있지만, 엔티티 그래프에 의해 연관된 member도 함께 조회된다.

 

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);

 

  • 엔티티 그래프는 JPA의 힌트 기능을 사용하므로 힌트의 키와 값을 위와 같이 넣어 주어야 한다.

 

subgraph

  • Order → OrderItem → Item과 같이 연달아서 엔티티 그래프를 조회할 경우 사용한다.

 

@NamedEntityGraph(name = "Order.withAll", attributeNodes = {
    @NamedAttributeNode("member"),
    @NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
    },
    subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
        @NamedAttributeNode("item")
    })
)
@Entity
public class Order {
    ...
}

 

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();

 

  • 힌트만 설정해 주면 된다.

 

동적 엔티티 그래프

  • createEntityGraph() 메소드를 사용하여 동적으로 addSubgraph() 를 넣어주면 된다.

 

출처

김영한 - 자바 ORM 표준 JPA 프로그래밍

댓글

추천 글