⭐️ 실무에서 JPA를 사용하기 위해서는 정!!!!말 중요한 내용 ⭐️
어차피 나중에 직접 겪게 될 오류 혼자 처리하고 혼자 헤매고 싶으면 대충 넘기던가!!
그리고 참고로 포스트에 기재된 코드만으로 이루어 진 것이 아님. Repository에 추가적인 코드가 필요하고, 따로 클래스가 필요하기도 함.(Github참고)
목표: 주문 + 배송정보 + 회원을 조회하는 API 만들기
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결하기
참고로 V1은 대충 들어도 됨. 어차피 엔티티를 직접 노출하는건,,👎
@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;
}
이렇게 결과가 다 나오지 않고 null로 나오는 이유는 지연로딩
order → member, order → address는 지연 로딩으로 설정되어있다. 따라서 실제 엔티티 대신 프록시로 존재한다.
jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모른다. ➡️ 예외 발생
Hibernate5Module
을 스프링 빈으로 등록하면 해결할 수 있다. (좋은 방법은 아님)
기본적으로 초기화 된 프록시 객체만 노출시키고 초기화 되지 않은 프록시 객체는 노출되지 않는다.
hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, true);
이 옵션을 사용하면 order → member, order → address 양방향 연관관계를 계속 로딩하게 된다. 따라서 @JsonIgnore
옵션을 한 곳에 주어야 한다.
엔티티를 직접 노출할 때는 양방향 연관관계가 걸리 곳은 꼭 한 쪽은
@JsonIgnore
처리하기
그렇지 않으면 양쪽을 서로 호출하며 무한 루프에 빠지게 된다.
앞 포스트에서 강조했듯 정말 간단한 애플리케이션이 아니라면 엔티티를 API 응답으로 외부에 노출시키는 것은 좋지 않은 방법이다.
따라서Hibernate5Module
을 사용하기 보다는 DTO로 변환해 반환하는 방법을 사용하자.
지연로딩(
LAZY
)를 피하기 위해 즉시로딩(EAGER
)으로 설정하지 말자.
즉시 로딩 때문에 연관관계가 필요 없는 경우에도 항상 데이터를 조회해 성능에 문제가 발생할 수 있다. 즉시로딩으로 설정하면 성능 튜닝이 매우 어려워진다.
항상 기본으로 지연 로딩을 사용하며, 성능 최적화가 필요한 경우에는 fetch join을 사용하도록 하자.
: 엔티티를 DTO로 변환하는 일반적인 방법
V1, 2 둘 다 쿼리가 너무 많이 호출된다는 문제점이 있다.
쿼리가 총 1 + N + N
번 실행된다.
(예제에서는 주문이 2개이기 때문에 1+2+2 = 5)
order 조회 1번(order 조회 결과 수가 N이 된다.)
order → member 지연 로딩 조회 N번
order → delivery 지연 로딩 조회 N번
최악의 경우 주문이 4개면 1+4+4번이 실행된다는 것이다.
(지연 로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.)
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> orderV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
엔티티를 JPA fetch join 기능으로 쿼리가 너무 많이 발생하던 문제를 쿼리 한 번으로 조회할 수 있게 함
fetch join으로 order → member
, order → delivery
는 이미 조회된 상태이므로 지연로딩 X
즉, LAZY(지연로딩)으로 설정해놓고 필요할 때만 fetch join을 활용하면 웬만한 성능 문제를 해결할 수 있다.
new
명령어를 사용해 JPQL의 결과를 DTO로 즉시 변환SELECT
절에서 원하는 데이터를 직접 선택하므로 DB → 애플리케이션 네트워크 용량 최적화(사실 생각보다 미비)단축키
Command + option + L
: 줄맞춤
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderRepository.findOrderDtos();
}
포스트맨 결과는 V3와 동일하지만 쿼리를 살펴보면
V3와는 달리 모든 것을 select하지 않고 원하는 것만 select 한 것을 확인할 수 있다.
엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두 가지 방법은 각각의 장단점이 존재한다. 둘 중 상황에 맞는 방법을 선택하자.
엔티티로 조회하면 리포지토리 재사용성도 좋고 개발도 단순해진다.
쿼리 방식 선택 권장 순서
- (V2) 우선 엔티티를 DTO로 변환하는 방법을 항상 먼저 선택
- (V3) 필요하면 fetch Join으로 성능을 최적화 → 여기서 대부분의 성능 이슈가 해결됨
- (V4) 그래도 안된다면 DTO로 직접 조회하는 방법
- 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template를 사용해 SQL을 직접 사용