-
3. API 개발 고급 - 지연 로딩과 조회 성능 최적화Spring-Boot/실전! 스프링 부트와 JPA활용2 - API개발과 성능 최적화 2023. 2. 13. 15:06
1. 간단한 주문 조회 V1 : 엔티티를 직접 노출
@GetMapping("/api/v1/simple-orders") public List<Order> ordersV1() { List<Order> all = orderRepository.findAllByString(new OrderSearch()); return all; // 무한루프 - 양방향 연관관계 발생 }
1. 무한루프 발생
ㆍ양방향 연관관계일 경우 한쪽을 @JsonIgnore을 해야한다.
2. bytebuddy.ByteBuddyInterceptor 에러 발생
ㆍLAZY 지연로딩은 디비에서 값을 가져오는게 아니라 Proxy 객체를 가져온다.
@Entity @Table(name = "orders") @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Order { @ManyToOne(fetch = LAZY) @JoinColumn(name = "member_id") private Member member = new ByteBuddyInterceptor(); // new ProxyMember(); }
ㆍ이 proxy가 bytebuddy 라는 라이브러리를 쓴다.
ㆍproxy 객체를(가짜) 넣어두고 Member객체에 접근할려고 하면 디비에 sql을 날린다.
ㆍ해결방안
ㆍ1. CPU 스케줄링과 프로세스 관리
º 1. hibernate5Module 설정
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
@Bean Hibernate5Module hibernate5Module() { Hibernate5Module hibernate5Module = new Hibernate5Module(); //강제 지연 로딩 설정 //hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true); return hibernate5Module; }}
º 2. Lazy 강제 초기화
@GetMapping("/api/v1/simple-orders") public List<Order> ordersV1() { List<Order> all = orderRepository.findAllByString(new OrderSearch()); for (Order order : all) { order.getMember().getName(); // Lazy 강제 초기화 order.getDelivery().getAddress(); // Lazy 강제 초기화 } return all; // 무한루프 - 양방향 연관관계 발생 }
* 참고 : 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다.
- 따라서 Hibernate5Module 를 사용하기 보다는 DTO로 변환해서 반환하는 것이 더 좋은 방법이다.
* 주의 : 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다!
- 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다.
- 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.
- 항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용해라
2. 간단한 주문 조회 V2 : 엔티티를 DTO로 변환
@GetMapping("/api/v2/simple-orders") public List<SimpleOrderDto> ordersV2() { // ORDER 2개 // N + 1 -> 1 + N(회원) + N(배송) List<Order> orders = orderRepository.findAllByString(new OrderSearch()); List<SimpleOrderDto> result = orders.stream() .map(SimpleOrderDto::new) // 람다 함수 .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(); //LAZY 초기화 orderDate = order.getOrderDate(); orderStatus = order.getStatus(); address = order.getDelivery().getAddress(); //LAZY 초기화 } }
- 엔티티를 DTO로 변환하는 일반적인 방법이다.
- 쿼리가 총 1 + N + N 번 실행된다. ( v1과 쿼리수 결과는 같다.)
ㆍorder 조회 1번 ( order 조회 결과 수가 N이 된다.)
ㆍorder -> member 지연 로딩 조회 N 번
ㆍorder -> delivery 지연 로딩 조회 N 번
ㆍ예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다. (최악의 경우)
º 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.
3. 간단한 주문 조회 V3 : 엔티티를 DTO로 변환 - 페치 조인 최적화
- 엔티티를 페치 조인(fetch join)을 사용해서 쿼리 1번에 조회
- 페치 조인으로 order → member, order → delivery 는 이미 조회된 상태이므로 지연로딩 X
4. 간단한 주문 조회 V4 : JPA에서 DTO로 바로 조회
- 일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회
- new 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환
- SELECT 절에서 원하는 데이터를 직접 선택하므로 DB → 애플리케이션 네트웍 용량 최적화 ( 생각보다 미비 )
- 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점
정리
- 엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다.
- 둘 중 상황에 따라서 더 나은 방법을 선택하면 된다.
- 엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해진다.
* 쿼리 방식 선택 권장 순서 *
1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
ㆍV2
2. 필요하면 페치 조인으로 성능을 최적화 한다. → 대부분의 성능 이슈가 해결된다.
ㆍV3
3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
ㆍV4
4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
'Spring-Boot > 실전! 스프링 부트와 JPA활용2 - API개발과 성능 최적화' 카테고리의 다른 글
5. API 개발 고급 - 실무 필수 최적화 & 6. 다음으로 (0) 2023.02.15 4. API 개발 고급 - 컬렉션 조회 최적화 (0) 2023.02.13 2. API 개발 고급 - 준비 (0) 2023.02.10 1. API 개발 기본 (0) 2023.02.09