n:1 관계에 있는 entity의 요소 DTO로 가져오기
JPQL 일반 join을 통해 DTO 객체를 생성해주면서 가져온다
order Repository
public List<OrderDto> findAllByDto(){
List<OrderDto> em.createQuery("select new app.repository.order.dto.OrderDto(o.id, o.order_date, o.status, u.name, u.email)"
+ " from Order o"
+ " join o.user u", OrderDto.class).getResultList();
}
처음에 왜 fetch join을 쓰지 않고도 DTO로 가져올 때는 지연로딩 없이 가져올 수 있을까? 에 대해 생각했었는데
JPQL 안에 new 생성자를 통해 하나씩 값을 찍어와서 DTO class에 넣어주고 있었기 때문이다
JPA entity로 data를 가져오는 JPQL은 아래와 같은데 한번 비교를 위해 코드를 게시한다
public List<OrderDto> findAllByDto(){
List<OrderDto> em.createQuery("select o"
+ " from Order o"
+ " join o.user u", OrderDto.class).getResultList();
}
이렇게 o인 entity를 가져오게 된다면 지연로딩 전략으로 (FetchType.LAZY 설정을 entity에 해놓은 경우!)
Order만 가져오고 user자리에는 Proxy 객체를 둔다
하지만 DTO로 가져오게 된다면 new OrderDto( ... , u.name, u.email) 이런식으로 DB에서 값을 추출하여 아예 DTO 안에 넣어 생성된 것을 반환하므로 data를 모두 가져오게 된다
1:n 관계에 있는 Entity의 요소 DTO로 가져오기
orderItem을 가져오기 위해서 위에서 가져온 OrderDto를 다 돌면서
orderItem.order_id 가 OrderDto의 id와 일치하는 것을 가져오는 것은 결국
각 Order에 OrderItem 개수만큼 쿼리를 날리는 것이기 때문에 1+N 즉 N+1 문제가 터진다
이러한 N+1 문제를 해결하기 위해 갓영한님 강의를 보고 솔루션을 얻을 수 있었다.
- OrderDto에서 id값을 추출하여 리스트로 만든다. 즉, 가져온 order들의 id 리스트
- JPQL where 절의 in을 사용하여 해당 쿼리를 1개의 쿼리로 최적화한다
- DB에서 가져온 결과를 각 DTO 안의 order_id 기준으로 grouping 해준다 ( 이때, Map을 이용하여 추후 OrderDto와 OrderItem을 쉽게 매핑할 수 있도록 검색을 최소화한다 )
- OrderDto들을 돌면서 OrderItem을 set 한다
public List<OrderDto> findAllByDto(){
List<OrderDto> orders = em.createQuery("select new app.repository.order.dto.OrderDto(o.id, o.order_date, o.status, u.name, u.email)"
+ " from Order o"
+ " join o.user u", OrderDto.class).getResultList();
List<Long> orderIds = orders.stream().map(o->o.getOrderId()).collect(Collectors.toList());
List<OrderItemDto> result = em.createQuery("select new app.repository.order.dto.OrderItemDto(oi.order.id, i.name, oi.order_price, oi.count)"
+ " from OrderItem oi"
+ " join oi.item i"
+ " where oi.order.id in :orderIds", OrderItemDto.class).setParameter("orderIds", orderIds).getResultList();
// <order_id, 해당 id를 order_id로 가지고 있는 OrderItemDto의 리스트> map
Map<Long, List<OrderItemDto>> orderItemMap = orderItems.stream().collect(Collectors.groupingBy(o->o.getOrderId()));
// 각 OrderDto를 돌면서 OrderItems 변수를 설정 -> map의 get으로 value(OrderItemDto 리스트)를 가져와서!
orders.forEach(o->o.setOrderItems(orderItemMap.get(o.getOrderId())));
return orders
}
tip
DTO와 Repository는 분리하여 의존성의 관계를 혼란스럽게 하지 않는 것이 좋다
만약 DTO가 Controller의 내부 클래스로 위치한다면
이는 Repository가 Controller를 역의존 관계가 된다
의존성의 올바른 의존관계는 Controller -> Service -> Repository
정리하자면
DTO로 직접 repository에서 DB 를 조회해오는 경우는
화면 단위로 data를 가져와야 되는 상황인 경우
몇개의 attribute만 필요한데 한 table에 attribute가 너무 많은 경우
이 두가지인 경우가 있겠다
하지만 DTO로 가져오면 일단 객체그래프가 깨지게 되고 영속성 컨텍스트에서 관리를 못해준다는 단점이 있으므로
JPA 영속성 컨텍스트가 해주는 Cache 같은 기능을 사용하지 못하게 되어
과연 DB I/O 에서는 성능적 우위가 있겠지만
총괄적으로 봤을 때, 성능적 우위가 있는지는 의문이므로 상황에 맞게 잘 선택해서 이 방법을 사용해야할 것 같다
(갓영한님의 강의에 따르면 DTO로 조회해왔는데도 성능개선이 안된다? -> 그건 redis 같은 Cache를 쓰라는 것이다 라고 말씀하신다)
실시간 서비스가 아닌 이상, 앵간하면 Batch Size 설정와 fetch join으로 data 가져오는 성능 문제는 해결되지 않을까 싶다
https://codinggyun.tistory.com/101
https://codinggyun.tistory.com/100
'Spring > JPA' 카테고리의 다른 글
[JPA] OSIV (Open Session In View)를 사용하여 JPA 성능 최적화 (0) | 2022.03.11 |
---|---|
[JPA] 1:N / N:1 상황에서의 N+1 문제 해결 (Fetch Join ~ 지연로딩/Batch Size) (0) | 2022.03.09 |
[JPA] Hibernate Batch Size 설정 (0) | 2022.03.09 |
[JPA] 준영속 엔티티를 변경하는 방법 - 변경 감지 / 병합(Merge) (0) | 2022.02.15 |
Spring Boot Repository EntityManager / EntityManagerFactory 엔티티매니저 / 엔티티매니저팩토리 (0) | 2021.12.27 |