“김영한 강사님의 JPA 활용편 2 - API 개발과 성능 최적화"를 듣고 간단하게 정리하기”
- 양방향 연관관계에서 직접 데이터를 반환하는 경우 서로가 서로를 참조하기 때문에 무한루프가 발생한다.
Order -> Member -> Order -> Member ... 무한루프발생!
- 양방향 연관관계가 매핑된 엔티티 한 쪽에는 @JsonIgnore를 사용한다.
- 지연로딩으로 설정된 값은 DB에서 가져오지 않고 가짜 Proxy 객체를 생성하고 실제 객체에 접근하고자 할때 그 때 프록시가 쿼리를 날려서 DB에서 값을 가져온다.
- Hibernate5Module와 @JsonIgnore를 이용해 엔티티를 넘긴 경우 무한루프 없이 데이터를 조회해 오는 것이 가능하다.
- 지연 로딩을 피하기 위해서 즉시 로딩 (EAGER)로 변경하지 않는다. 연관관계가 필요없는 경우에도 데이터를 조회하기 때문에 성능 튜닝이 어려워진다.
[
{
"id": 4,
"member": null, // 지연 로딩
"orderItems": null,
"delivery": null,
"orderDate": "2022-07-06T10:44:43.457581",
"status": "ORDER",
"totalPrice": 50000
},
{
"id": 11,
"member": null,
"orderItems": null,
"delivery": null,
"orderDate": "2022-07-06T10:44:43.511754",
"status": "ORDER",
"totalPrice": 220000
}
]
- 하지만, 엔티티를 직접 프레젠테이션 계층에 넘기는 것은 하면 안된다!!! (V1의 문제점)
- DTO로 변환해서 화면으로 응답한다. (V1의 해결책)
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> orderV2() {
// Order SQL 1번 실행 -> 결과 주문수 2개
// N + 1문제라고 한다!! 첫번째 쿼리의 결과로 N번의 쿼리가 추가 실행된다.
// N + 1 : 주문 1 + 회원 N + 배송 N
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
// Member, Delivery 각 두번 씩 조회
return orders.stream()
.map(order -> new SimpleOrderDto(order))
.collect(Collectors.toList());
}
@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(); // LAZY가 초기화 : MemberId를 가지고 영속성 컨텍스트를 조회한 후 없는 경우 DB에 쿼리를 날린다.
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
- DTO로 반환하는 경우 엔티티의 컬럼이 바뀌거나 하더라도 컴파일 단계에서 에러 체크가 가능하다.
- V1, V2 모두 지연 로딩으로 인한 데이터베이스 쿼리가 너무 많이 호출되는 단점이 있다. Member, Order, Delivery 3개의 엔티티를 한번에 호출한다.
- 영속성 컨텍스트가 주문 조회 SQL을 먼저 날린다. 쿼리는 한개만 날아간다.
- 쿼리를 통해 주문이 2개가 조회되면, 각 주문 당 지연로딩에 따라 Member의 Name, Delivery의 Address 쿼리가 1번씩 추가 실행된다.
- 즉, 주문 쿼리 1번에 회원, 배송 테이블의 쿼리가 N번 추가 조회되는 N + 1문제가 발생한다.
- 지연 로딩은 영속성 컨텍스트를 조회하기 때문에 이미 조회된 경우 쿼리를 생략한다.
- 따라서, 성능 향상을 위해서 fetch join을 이용한다. (V1, V2 : 지연 로딩으로 인한 N + 1의 해결책)
public List<Order> findAllWithMemberDelivery() {
// Fetch Join을 이용 -> Order를 가져올 때 객체 그래프를 탐색해서 Member와 Delivery를 한번에 가져온다.
return em.createQuery("select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class
).getResultList();
}
- Fetch join을 이용해 주문(SQL 1번)을 가져올 때 객체 그래프를 탐색해 지연로딩되는 데이터를 한번에 가져온다. (V3)
- 기존 N+1번 돌던 쿼리가 단 1번의 쿼리로 모든 데이터를 조회할 수 있다.
- V3의 약간의 단점은 먼저 엔티티로 조회한 후 엔티티를 DTO에 매핑하는 단계가 있다.
- JPA를 이용해 데이터베이스에서 DTO로 바로 꺼냄으로써 성능을 더욱 최적화 시키는 것이 가능하다.
public List<OrderSimpleQueryDto> findOrderDtos() {
return em.createQuery("select new jpabook.jpashop.repository.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
" from Order o" +
" join o.member m" +
" join o.delivery d", OrderSimpleQueryDto.class)
.getResultList();
}
@Data
public class OrderSimpleQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
}
}
- JPQL을 이용해 new를 이용해 DTO의 데이터에 값을 매핑해 즉시 반환한다.
- V3에서는 엔티티의 모든 컬럼을 조회하기 때문에 중복되는 컬럼도 함께 조회되었지만, DTO를 직접 매핑해준 경우 원하는 데이터만 DB에서 조회해 오기 때문에 네트웍 용량을 최적화할 수 있다.
- V4는 JPQL을 이용해서 성능을 최적화 시키는 것은 가능하지만, 해당 DTO를 이용하는 경우를 제외하면 재사용하는 것이 불가능하다는 단점이 있다.
- 그에 비해 V3은 엔티티 자체를 컨트롤 하는 것이기 때문에 다른 API에서도 원하는 DTO로 변환하여 사용할 수 있기 때문에 재사용성이 좋다.
정리
- Repository는 화면과 직접 연관되게 성능 최적화를 해야하는 (DTO로 직접 반환) 부분과 엔티티를 조회하는 Repository를 구분해주는 것을 권장한다.
- 엔티티를 DTO로 변환하는 방법을 우선 선택한 후 Fetch Join을 이용해 성능 최적화한다.
- DTO로 직접 조회하는 방법은 Fetch Join을 사용해도 성능 이슈가 있는 경우에 사용한다.
- DTO로 직접 조회하는 방법도 성능 이슈가 있는 경우 JPA의 네이티브 SQL이나 스프링 JDBC Template을 이용해 SQL을 직접 사용한다.
반응형
'Backend > Spring Boot' 카테고리의 다른 글
Repository 단위 테스트 (0) | 2022.07.15 |
---|---|
[API 고급] 컬렉션 조회 최적화 (0) | 2022.07.12 |
API 개발 기본 간단 정리 (0) | 2022.07.07 |
도메인 개발 팁 간단 정리 (0) | 2022.07.05 |
RestTemplate (0) | 2022.07.01 |