쌩로그
자바 ORM 표준 JPA 프로그래밍 Ch.14. 컬렉션과 부가 기능 (2) 본문
목록
- 포스팅 개요
- 본론
2-1. 리스너
2-2. 엔티티 그래프 - 요약
1. 포스팅 개요
자바 ORM 표준 JPA 프로그래밍
의 14. 컬렉션과 부가 기능
를 학습하며 정리한 포스팅이다.
해당 포스팅은 이전 포스팅에서 다루지 않았던 리스너와 엔티티 그래프에 대해서 살펴본다.
참고로 이전 포스팅에서는 컬렉션
과 @Converter
에 대해 알아보았었다.
2. 본론
2-1. 리스너
모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야 하는 요구사항이 있다고 가정하자.
이때 애플리케이션 삭제 로직을 하나씩 찾아서 로그를 남기는 것은 너무 비효율적이다.
JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.
이벤트 종류
이벤트의 종류와 발생 시점은 다음과 같다.
![](https://velog.velcdn.com/images/tjdtn4484/post/60af847f-f132-41d6-be49-c6af04a9014f/image.png)
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
을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.
이벤트 적용 위치
이벤트는 엔티티에서 직접 받거나 별도의 **리스너를 등록해서 받을 수 있다.
- 엔티티 직접 적용
- 별도의 리스너 등록
- 기본 리스너 사용
엔티티에 직접 적용
package spring.datajpa.entity;
import jakarta.persistence.*;
@Entity
public class Duck {
@Id
@GeneratedValue
public Long id;
private String name;
@PrePersist
public void prePersist() {
System.out.println("Duck.prePersist id=" + id);
}
@PostPersist
public void postPersist() {
System.out.println("Duck.postPersist id=" + id);
}
@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");
}
}
위의 코드는 엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메서드가 실행된다.
다음은 필자가 엔티티를 저장하고 삭제까지의 코드를 작성한 후 나온 출력 결과다.
@DataJpaTest
class DuckTest {
@Autowired
EntityManager em;
@Test
@DisplayName("리스너 알아보기")
void Test() {
System.out.println("Duck 생성");
Duck duck = new Duck();
System.out.println("Duck DB에 저장 전");
em.persist(duck);
em.flush();
em.clear();
Duck findDuck = em.find(Duck.class, duck.id);
System.out.println("삭제 전");
em.remove(findDuck);
em.flush();
em.clear();
}
}
출력 결과는 다음과 같다.
Duck 생성
Duck DB에 저장 전
Duck.prePersist id=null
...
...
Duck.postLoad
삭제 전
Duck.preRemove
이렇게 엔티티에 이벤트를 직접 받을 수도 있지만 이벤트를 처리할 별도의 리스너를 등록하는 방법도 있다.
별도의 리스너 등록
다음은 별도의 리스너를 사용하는 예이다.
@Entity
@EntityListeners(DuckListener.class)
public class Duck {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
private String name;
}
...
...
import jakarta.persistence.PostPersist;
import jakarta.persistence.PrePersist;
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.portPersist obj = [" + obj + "]");
}
}
위의 테스트 코드 출력결과이다.
Duck 생성
Duck DB에 저장 전
DuckListener.prePersist obj = [spring.datajpa.entity.Duck@424769f2]
...
...
insert into duck (name,id) values (?,default)
insert into duck (name,id) values (NULL,default);
DuckListener.portPersist obj = [spring.datajpa.entity.Duck@424769f2]
리스너는 대상 엔티티를 파라미터로 받을 수 있다.
반환 타입은 void
로 설정해야 한다.
기본 리스너 사용
모든 엔티티의 이벤트를 처리하려면 다음 예와 같이 META-INF/orm.xml
에 기본 리스터로 등록하면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd"
version="2.2">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="jpabook..jpashop.domain.test.listener.DefaultListener"/>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
여러 리스너를 등록했을 때 이벤트 호출 순서는 다음과 같다.
더 세밀한 설정
더 세밀한 설정을 위한 어노테이션도 있다.
다음은 그 사용예이다.
jakarta.persistence.ExcludeDefaultListeners
: 기본 리스너 무시jakarta.persistence.ExcludeSuperclassListeners
: 상위 클래스 이벤트 리스너 무시
@Entity
@EntityListeners(DuckListener.class)
@ExcludeDefaultListeners
@ExcludeSuperclassListeners
public class Duck extends BaseEntity {
...
}
이벤트를 잘 활용하면 대부분의 엔티티에 공통으로 적용하는 등록 일자, 수정 일자 처리와 해당 엔티티를 누가 등록하고 수정했는지에 대한 기록을 리스너 하나로 처리할 수 있다.
2-2. 엔티티 그래프
엔티티를 조회할 때 연관된 엔티티들을 함께 조회하려면 다음처럼 글로벌 fetch 옵션
을 FetchType.EAGER
로 설정한다.
@Entity
class Order {
@ManyToOne(fetch = FetchType.EAGER)
Member member;
...
}
또는 다음처럼 JPQL에서 페치 조인을 사용하면 된다.
select o from Order o join fetch o.member
글로벌 fetch
옵션은 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점이 있다.
그래서 일반적으로 글로벌 fetch
옵션은 FetchType.LAZY
를 사용하고, 엔티티를 조회할 때 연관된 엔티티를 함께 조회할 필요가 있으면 JPQL의 페치 조인을 사용한다.
그런데 페치 조인을 사용하면 같은 JPQL을 중복해서 작성하는 경우가 많다.
예를 들어 주문 상태를 검색조건으로 주문(Order) 엔티티를 조회하는 JPQL을 작성해보자.
select o from Order o where o.status = ?
주문과 회원을 함께 조회할 필요가 있어서 다음 JPQL을 새로 추가했다.
select o from Order o
join fetch o.member
where o.status = ?
주문과 주문상품을 함께 조회하는 기능이 필요해서 다음 JPQL을 새로 추가했다.
select o from Order o
join fetch o.orderItems
where o.status = ?
3가지 JPQL 모두 주문을 조회하는 같은 JPQL이지만, 함께 조회할 엔티티에 따라서 다른 JPQL을 사용해야 한다. 이것은 JPQL이 데이터를 조회하는 기능뿐만 아니라 연관된 엔티티를 함께 조회하는 기능도 제공하기 때문인데, 결국 JPQL이 두 가지 역할을 모두 수행해서 발생하는 문제다.
JPA 2.1에 추가된 엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있다.
따라서 JPQL은 데이터를 조회하는 기능만 수행하면 되고 연관된 엔티티를 함께 조회하는 기능은 엔티티 그래프를 사용하면 된다.
그러므로 엔티티 그래프 기능을 적용하면 다음 JPQL만 사용된다.
select o from Order o
where o.status = ?
엔티티 그래프 기능은 엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능이다.
엔티티 그래프는 정적으로 정의하는 Named 엔티티 그래프와 동적으로 정의하는 엔티티 그래프가 있다.
먼저 Named 엔티티 그래프를 알아보자.
예제에 사용할 엔티티 모델은 다음과 같다.
![](https://velog.velcdn.com/images/tjdtn4484/post/3dd3c1a1-918d-47e1-826a-06eb93a3ee3d/image.png)
Named 엔티티 그래프
다음 코드를 통해 주문(Order)을 조회할 때 연관된 회원(Member)도 함께 조회하는 엔티티 그래프를 사용해보자.
@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
@NamedAttributeNode("member")
})
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "member_id")
private Member member; // 주문 회원
}
Named 엔티티 그래프는 @NamedENtityGraph
로 정의한다.
- name : 엔티티 그래프의 이름을 정의한다.
- attributeNodes : 함께 조회할 속성을 선택한다. 이때
@NamedAttributeNode
를 사용하고 그 값으로 함께 조회할 속성을 선택하면 된다.
위 예제 코드의 Order.member
가 지연 로딩으로 설정되어 있지만, 엔티티 그래프에서 함께 조회할 속성으로 member
를 선택했으므로 이 엔티티 그래프를 사용하면 Order
를 조회할 때 연관된 member
도 함께 조회할 수 있다.
참고로 둘 이상 정의하려면 @NamedEntityGraphs
를 사용하면 된다.
em.find()에서 엔티티 그래프 사용
다음 예를 통해서 엔티티 그래프를 사용하는 코드를 보자.
@Test
@DisplayName("")
void Test() {
Member member = new Member("member1", 20);
em.persist(member);
Order order = new Order(member);
em.persist(order);
em.flush();
em.clear();
EntityGraph<?> graph = em.getEntityGraph("Order.withMember");
Map hints = new HashMap<>();
hints.put("jakarta.persistence.fetchgraph", graph);
Order findOrder = em.find(Order.class, order.getId(), hints);
}
Named 엔티티 그래프
를 사용하려면 정의한 엔티티 그래프를 em.getEntityGraph("Order.withMember")
를 통해서 찾아오면 된다.
엔티티 그래프는 JPA의 힌트 기능을 사용해서 동작하는데 힌트의 키로 jakarta.persistence.fetchgraph
를 사용하고 힌트의 값으로 찾아온 엔티티 그래프를 사용하면 된다.em.find(Order.class, order.getId(), hints)
로 Order
엔티티를 조회할 때 힌트 정보도 포함했다.
실행된 sql은 다음과 같다.
select
o1_0.order_id,
o1_0.member_id,
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
where
o1_0.order_id=?
subgraph
이번에는 Order -> OrderItem -> Item
까지 함께 조회해보자.
Order -> OrderItem
은 Order
가 관리하는 필드지만 OrderItem -> Item
은 Order
가 관리하는 필드가 아니다. 이때는 다음과 같이 subgraph
를 사용하면 된다.
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@NamedEntityGraph(name = "Order.withAll", attributeNodes = {
@NamedAttributeNode("member"),
@NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
},
subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
@NamedAttributeNode("item")
})
)
@Entity
@Table(name = "ORDERS")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "member_id")
private Member member; // 주문 회원
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
public Order(Member member) {
this.member = member;
}
...
}
@Entity
@Table(name = "ORDER_ITEM")
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
}
위의 예제에서는 Order.withAll
이라는 Named 엔티티 그래프를 정의했다.
이 엔티티 그래프는 Order -> Member, Order -> OrderItem, OrderItem -> Item
의 객체 그래프를 함께 조회한다.
이때 OrderItem -> Item
은 Order
의 객체 그래프가 아니므로 subgraphs
속성으로 정의해야 한다.
이 속성은 @NamedSubgraph
를 사용해서 서브 그래프를 정의한다.
여기서는 orderItems
라는 이름의 서브 그래프가 item
을 함께 조회하도록 정의했다.
사용하는 코드를 보자.
@DataJpaTest
class OrderTest {
@Autowired
EntityManager em;
@Test
@DisplayName("")
void Test() {
Member member = new Member("member1", 20);
em.persist(member);
Order order = new Order(member);
em.persist(order);
em.flush();
em.clear();
Map hints = new HashMap<>();
hints.put("jakarta.persistence.fetchgraph", em.getEntityGraph("Order.withAll"));
Order findOrder = em.find(Order.class, order.getId(), hints);
}
}
Order.withAll
이라는 Named 엔티티 그래프
를 사용해서 Order 엔티티를 조회했다.
실행된 쿼리는 다음과 같다.
실행된 SQL을 보면 엔티티 그래프에서 지정한 엔티티들을 함께 조회한다.
select
o1_0.order_id,
o1_0.member_id,
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username,
oi1_0.order_id,
oi1_0.order_item_id,
i1_0.item_id
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
left join
order_item oi1_0
on o1_0.order_id=oi1_0.order_id
left join
item i1_0
on i1_0.item_id=oi1_0.item_id
where
o1_0.order_id=?
JPQL에서 엔티티 그래프 사용
JPQL에서 엔티티 그래프를 사용하는 방법은 em.find()
와 동이랗게 힌트만 추가하면 된다.
다음 예를 보자.
@Test
@DisplayName("jpql에서 엔티티그래프 사용")
void entityGraphWithJpql() {
Member member = new Member("member1", 20);
em.persist(member);
Order order = new Order(member);
em.persist(order);
em.flush();
em.clear();
Order findOrder = em.find(Order.class, order.getId());
em.clear();
List<Order> resultList = em.createQuery("select o from Order o where o.id = :orderId",
Order.class)
.setParameter("orderId", findOrder.getId())
.setHint("jakarta.persistence.fetchgraph", em.getEntityGraph("Order.withAll"))
.getResultList();
}
실행된 SQL을 보면 연관된 엔티티도 함께 조회한다.
select
o1_0.order_id,
o1_0.member_id,
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username,
oi1_0.order_id,
oi1_0.order_item_id,
i1_0.item_id
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
left join
order_item oi1_0
on o1_0.order_id=oi1_0.order_id
left join
item i1_0
on i1_0.item_id=oi1_0.item_id
where
o1_0.order_id=?
참고
다음 코드 같이 Order.member
는 필수 관게로 설정되어 있다.
@ManyToOne(fetch = FetchType.LAZY, optional = false) // 필수 관계로 설정
@JoinColumn(name = "member_id")
private Member member; // 주문 회원
em.find()
에서 엔티티 그래프를 사용하면 하이버네이트는 필수 관계를 고려해서 SQL 내부조인을 사용하지만 JPQL에서 엔티티 그래프를 사용할 때는 항상 SQL 외부 조인을 사용한다.
만약 SQL 내부 조인을 사용하려면 다음처럼 내부 조인을 명시하면 된다.
select o from Order o join fetch o.member where o.id = :orderId
동적 엔티티 그래프
엔티티 그래프를 동적으로 구성하려면 createEntityGraph()
메서드를 사용하면 된다.
public <T> EntityGraph<T> createEntityGraph(Class<T> rootType);
처음에 사용한 Named 엔티티 그래프
를 다음 예에서 동적으로 구성해보자.
@Test
void entityGraphTest() {
Member member = new Member("member1", 20);
em.persist(member);
Order order = new Order(member);
em.persist(order);
em.flush();
em.clear();
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
Map hints = new HashMap<>();
hints.put("jakarta.persistence.fetchgraph", graph);
Order findOrder = em.find(Order.class, order.getId(), hints);
}
다음은 실행된 쿼리다.
select
o1_0.order_id,
o1_0.member_id,
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
where
o1_0.order_id=?
위의 예에서 em.createEntityGraph(Order.class)
를 사용해서 **동적으로 엔티티 그래프를 만들었다. 그리고 graph.addAttributeNodes("member")
를 사용해서 Order.member
속성을 엔티티 그래프에 포함했다.
다음 예를 통해서 조금 더 복잡한 subgraph
기능을 동적으로 구성해보자.
@Test
void entityGraphTest() {
Member member = new Member("member1", 20);
em.persist(member);
Order order = new Order(member);
em.persist(order);
em.flush();
em.clear();
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
Subgraph<Object> orderItems = graph.addSubgraph("orderItems");
orderItems.addAttributeNodes("item");
Map hints = new HashMap<>();
hints.put("jakarta.persistence.fetchgraph", graph);
Order findOrder = em.find(Order.class, order.getId(), hints);
}
실행된 SQL은 다음과 같다.
select
o1_0.order_id,
o1_0.member_id,
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username,
oi1_0.order_id,
oi1_0.order_item_id,
i1_0.item_id
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
left join
order_item oi1_0
on o1_0.order_id=oi1_0.order_id
left join
item i1_0
on i1_0.item_id=oi1_0.item_id
where
o1_0.order_id=?
위의 예는 graph.addSubgraph("orderItems")
메서드를 사용해서 서브 그래프를 만들었다.
그리고 서브 그래프가 item 속성을 포함하도록 했다.
엔티티 그래프 정리
지금까지 엔티티 그래프를 알아보았는데 정리해보면,
ROOT에서 시작
엔티티 그래프는 항상 조회하는 엔티티의 ROOT에서 시작해야 한다.
당연한 이야기지만, Order 엔티티를 조회하는데 Member부터 시작하는 엔티티 그래프를 사용하면 안 된다.
이미 로딩된 엔티티
다음처럼 영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프가 적용되지 않는다.(아직 초기화되지 않은 프록시에는 엔티티 그래프가 적용된다.)
Order order1 = em.find(Order.class, orderId); // 이미 조회
hints.put("jakarta.persistence.fetchgraph", em.getEntityGraph("Order.withMember"));
Order order2 = em.find(Order.clas, orderId, hints);
이 경우 조회된 order2에는 엔티티 그래프가 적용되지 않고 처음 조회한 order1과 같은 인스턴스가 반환된다.
fetchgraph, loadgraph
예제에서는 jakarta.persistence.fetchgraph
힌트를 사용해서 엔티티 그래프를 조회했다.
이것은 엔티티 그래프에 선택한 속성만 함께 조회한다.
반면에 jakarta.persistence.loadgraph
속성은 엔티티 그래프에 선택한 속성뿐만 아니라 글로벌 fetch 모드가 FetchType.EAGER
로 설정된 연관관계도 포함해서 함께 조회한다.
참고 (집필 당시의 참고)
하이버네이트 4.3.10.Final 버전에서는 loadgraph
기능이 em.find()
를 사용할 때는 정상 동작하지만 JPQL을 사용할 때는 정상 동작하지 않고 fetchgraph
와 같은 방식으로 동작한다.
3. 요약
- 리스너를 사용하면 엔티티에서 발생한 이벤트를 받아서 처리할 수 있다.
- 페치 조인은 객체지향 쿼리를 사용해야 하지만 엔티티 그래프를 사용하면 객체 지향 쿼리를 사용하지 않아도 원하는 객체 그래프를 한 번에 조회할 수 있다.
'Spring > JPA' 카테고리의 다른 글
실전! Queydsl (feat. JPA 로드맵의 끝 + 앞으로의 계획살짝) (2) | 2024.03.03 |
---|---|
자바 ORM 표준 JPA 프로그래밍 Ch.16. 트랜잭션과 락, 2차 캐시 (0) | 2024.02.27 |
자바 ORM 표준 JPA 프로그래밍 Ch.12. 스프링 데이터 JPA (0) | 2024.02.21 |
실전! 스프링 데이터 JPA (인프런 - 김영한) (0) | 2024.02.18 |
자바 ORM 표준 JPA 프로그래밍 Ch.15. 고급 주제와 성능 최적화 (0) | 2024.02.16 |