본문 바로가기
Spring/JPA

[JPA] Entity가 아닌 DTO로 DB에서 data 가져오기

by 코딩균 2022. 3. 10.

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 문제를 해결하기 위해 갓영한님 강의를 보고 솔루션을 얻을 수 있었다.

  1. OrderDto에서 id값을 추출하여 리스트로 만든다. 즉, 가져온 order들의 id 리스트
  2. JPQL where 절의 in을 사용하여 해당 쿼리를 1개의 쿼리로 최적화한다
  3. DB에서 가져온 결과를 각 DTO 안의 order_id 기준으로 grouping 해준다 ( 이때, Map을 이용하여 추후 OrderDto와 OrderItem을 쉽게 매핑할 수 있도록 검색을 최소화한다 )
  4. 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

 

[JPA] 1:N / N:1 상황에서의 N+1 문제 해결 (Fetch Join ~ 지연로딩/Batch Size)

DB의 테이블이 위와 같은 관계를 가지고 있는 경우 N+1 문제 고려하지 않은 코드 ( DB n:1의 관계 ) data 가정 Order가 2개 존재 각각의 order와 연결된 member는 모두 다른 사람 모든 entity fetchType 은 LAZY -..

codinggyun.tistory.com

https://codinggyun.tistory.com/100

 

[JPA] Hibernate Batch Size 설정

Batch Size? 1:N 관계 ( 여기서는 Order : OrderItem 이라고 가정 )에서의 지연 로딩 시에 SQL 문이 각각의 N개의 객체에 대해서 select orderItem0_.order_item_id as order_item_id5_5_1_, orderItem0_.order_id..

codinggyun.tistory.com