실무에서 JPA를 사용하려면 앞으로 나오는 내용을 100% 이해해야 한다.
예제
간단한 주문 조회 V1 : 엔티티를 직접 노출
@GetMapping("/api/v1/simple-orders")
private List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByCriteria(new OrderSearch());
return all;
}
음.. 그러니까
위 v1 버전을 사용했을 경우 문제점을 나열해보자
문제점:
Order 객체의 필드의 member와, Member 객체의 필드의 orders가 (다른것도 존재)
서로 무한루프를 돌게된다.
해결법:
이러한 문제를 해결하려면 양방향 중 한 쪽에다가 @JsonIgnore 을 설정해야 한다.
이뿐만 아니라, 지연로딩(FetchType.LAZY)로 설정해준 것들을 모두 EAGER로 바꿔야한다.
하지만 이때 다른 API 스펙과 호환성 및 성능 튜닝을 고려하여 EAGER로 바꾸면 안되고
Hibernate5Module 라이브러리를 추가하여 강제지연로딩 (Hibernate5Module.Feature.FROCE_LAZY,LOADING) 을 해주어야 한다.
(중요하지 않기 때문에 코드 생략)
Hibernate를 직접 가져다 쓰는것은 좋지않기 때문에 지연로딩된 것을 강제 초기화 할 수 있다
order.getMember().getName() //Lazy 강제 초기화
order.getDelivery().getAddress() //Lazy 강제 초기화
//getName(), getAddress() 시점에 초기화
간단한 주문 조회 V2 : 엔티티를 DTO로 변환
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByCriteria(new OrderSearch());
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
orders가 2개가 조회되고, 그에 따라 서로 다른 members가 조회되어
총 쿼리는 1 + N + N 번 실행된다
1 => orders
2 => member
2 => delivery
만약 같은 멤버라면 (member_Id가 같다면)
(자주 있는 경우는 아님, 보통 최악의 경우를 가정하는게 좋다)
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
첫 번째 멤버 조회
order.getMember().getName(); 을 처음 실행했을때는 영속성 컨텍스트에 없기때문에 member를 가져온다.
order.getDelivery().getAddress();을 처음 실행했을때는 영속성 컨텍스트에 없기때문에 delivery를 가져온다.
두 번째 멤버 조회
order.getMember().getName(); 이미 같은 user를 영속성 컨텍스트에 조회를 했기 때문에 다시 조회하지 않고 바로 delivery를 조회한다
1 + 1 + 2
1 => orders
1 => member
2 => delivery
간단한 주문 조회 V3 : 엔티티를 DTO로 변환 - 페치 조인 최적화
@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;
}
V2와 코드는 동일하지만, findAllWithMemberDelivey( ) 라는 메서드를 새로 생서한다.
이 메서드는 order_Id를 조회하는 동시에 member와 delivery를 fetch join 하여 모두 한 번에 가져오는 메서드이다.
//OrderRepository
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class
).getResultList();
}
패치 조인을 하게되면 프록시값이 아닌 본래의 값을 가져오고, 지연로딩의 영향을 받지 않게된다. (자세한건 ORM 기본편 참고)
그리고 항상 마지막엔 List로 감싸주어야 한다.
간단한 주문 조회 V4 : JPA에서 DTO로 바로 조회
결론 : v4가 v3보다 select 절이 조금 나가지만, 결과적으로 성능을 따져보면 별로 차이가 나지 않는다