1. 조회 API에서 DTO를 사용하는 이유
- 이전 포스팅에서 Entity를 직접 사용한 조회 API를 구성하는 방법에 대해 알아보았다.
- Entity를 직접 노출하는 방식은 Entity의 내용이 변경됐을 때, API 스펙에 직접적인 영향을 미치기 때문이다.
- 그러므로 Entity 대신 DTO를 사용하여 이를 해결한다.
2. DTO를 이용한 조회 API 구성 방법
- API에서 DTO를 구성하는 방식은 매우 간단하다.
- 기존에 Entity가 직접 사용된 부분을 DTO로 교체하면 된다.
- 다음 코드를 살펴보자.
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@GetMapping("api/order-with-dto")
public List<OrderDto> orderWithDto() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
// o -> new SimpleOrderDto(o) 이 부분을 SimpleOrderDto::new 이렇게 바꿔도 동일하다.
return result;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public OrderDto(Order order) {
// this 는 메서드의 매개변수가 필드명과 동일한 경우, 이를 구분짓기 위함이다.
// 아래에 this 를 사용해도 무관하다.
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAdderss();
}
}
}
3. DTO 사용 시 발생하는 문제점
- 위의 코드에서 OrderDto의 생성자 부분을 살펴보자.
- Order는 Order, Member, Delivery 총 3가지 Entity와 연관되어 있는 객체다.
- 이처럼 연관 객체에 모두 접근하면 Lazy Loading이 실행되고, 이는 N + 1 문제를 발생시킨다.
- 예를 들어 Order 테이블에 2명의 회원이 요청한 주문 정보가 각 1개씩(= 총 2개) 있다고 가정해보자.
- 각 주문의 회원 이름을 찾기 위해, orderId를 이용한 Member 조회 쿼리가 2개 발행된다.
- 각 주문의 배송 주소를 찾기 위해, orderId를 이용한 Delievery 조회 쿼리가 2개 발행된다.
- 결과적으로 총 5개의 쿼리가 발행된다. (N + 1 문제)
- 만약 Order 테이블의 주문이 동일한 회원 1명의 주문이라면 Member는 1번, Delivery는 2번 조회한다.
- 왜냐면 JPA는 DB에 접근하기 전에 영속성 Context를 우선적으로 살펴보기 때문이다.
→ Delivery를 2번 조회하는 이유는 주문마다 deliveryId 값이 다르기 때문이다.
- 그러나 실제 운영되는 서비스는 다수의 회원이 존재한다.
- 그러므로 이처럼 N + 1 문제가 발생해서는 안된다.
- 이를 해결하기 위해 Fetch Join을 사용한다.
4. Fetch Join 사용하기
- 우선, Fetch Join 개념을 모른다면 다음 포스팅을 꼭 읽는 것을 권장합니다.
https://devraphy.tistory.com/601
- DB를 조회 시 OrderRepository의 조회 메서드를 이용한다.
- OrderRepository의 조회 메서드의 쿼리에서 N + 1 문제가 발생하므로, 새로운 쿼리가 필요하다.
- 그러므로 OrderRepository에 다음과 같이 Fetch Join 쿼리를 사용한 새로운 메서드를 생성해준다.
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
public List<Order> findAllUsingFetchJoin() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m " +
" join fetch o.delivery d", Order.class).getResultList();
}
}
- 이제 API 컨트롤러에서 위에 작성한 findAllUsingFetchJoin() 메서드를 사용하면 된다.
- 다음 예시를 살펴보자.
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
private final OrderRepository orderRepository;
@GetMapping("api/v3/simple-orders")
public List<SimpleOrderDto> orderV3() {
List<Order> orders = orderRepository.findAllUsingFetchJoin();
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o)).collect(Collectors.toList());
return result;
}
@Data
static class SimpleOrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAdderss();
}
}
}
- 위의 메서드를 실행하면 다음과 같은 쿼리가 발행된다.
- 이처럼 Fetch Join을 이용하여 N + 1 문제를 해결할 수 있다.
- 그러나 여전히 개선사항은 남아있다.
5. 개선 사항
- 다음 쿼리를 살펴보자.
- Select 절을 살펴보면 필요하지 않은 데이터까지 모두 조회하는 것을 확인할 수 있다.
- 이처럼 필요하지 않는 데이터까지 모두 조회되는 이유는 객체를 대상으로 조회했기 때문이다.
- 즉, 객체의 모든 데이터를 조회하고 필요한 데이터를 DTO에 담는 방식을 사용하기 때문이다.
- 그렇다면 필요한 데이터만 선택한다면 이 문제를 해결할 수 있지 않을까?
- 이 부분을 최적화하기 위해서 DTO를 통해 직접 조회 방법을 사용할 수 있다.
'Back-end > JPA 개념' 카테고리의 다른 글
API 조회 성능 최적화 - 컬렉션(DTO 변환) (0) | 2022.06.01 |
---|---|
API 조회 성능 최적화 - DTO 직접 조회 (0) | 2022.05.30 |
API 조회 성능 최적화 - Entity 직접 노출 방식 (0) | 2022.05.28 |
JPA를 이용한 API 개발 꿀팁 - Annotation (0) | 2022.05.27 |
JPA 개발 꿀팁 (0) | 2022.05.25 |
댓글