@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAll(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); //LAZY 프록시 초기화
order.getDelivery().getAddress(); //LAZY 프록시 초기화
}
return all;
//문제점: 무한루프 장애 발생(Order -> Member -> Order -> Member...)
}
- 📌 엔티티를 직접 노출할 경우 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore 처리
- 안그러면 양쪽을 서로 호출하면서 무한 루프 발생
- 📌 엔티티의 외부노출은 가급적 지양 => DTO로 변환하여 반환
- 📌 지연로딩을을 피하기 위해 즉시로딩 사용은 X
(2) ver2: 엔티리를 DTO로 변환
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
List<Order> orders = orderRepository.findAll(new OrderSearch());
List<SimpleOrderDto> collect = orders.stream()
.map(m -> new SimpleOrderDto(m))
.collect(Collectors.toList());
return collect;
//문제점: 1+N 문제 발생
}
@Data
static class SimpleOrderDto {
private Long orderId;
private String name;
private OrderStatus orderStatus;
private Address address;
public SimpleOrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName(); //LAZY 프록시 초기화
orderStatus = order.getStatus();
address = order.getDelivery().getAddress(); //LAZY 프록시 초기화
}
}
- 📌 N+1 문제가 발생
- 지연로딩은 영속성 컨텍스트에서 조회하므로 이미 있을 경우 쿼리가 생략되긴 하지만, 최악의 경우를 상정했을 때 연관관계로 인해 1+N+N... 쿼리가 발생
- 예) order 결과가 10개라면, 1(order 조회) + 10(member조회) + 10(delivery조회) 쿼리
(3) ver3: fetch join
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();
}
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<SimpleOrderDto> collect = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return collect;
}
select
order0_.order_id as order_id1_6_0_,
member1_.member_id as member_i1_4_1_,
delivery2_.delivery_id as delivery1_2_2_,
order0_.delivery_id as delivery4_6_0_,
order0_.member_id as member_i5_6_0_,
order0_.order_date as order_da2_6_0_,
order0_.status as status3_6_0_,
member1_.city as city2_4_1_,
member1_.street as street3_4_1_,
member1_.zipcode as zipcode4_4_1_,
member1_.name as name5_4_1_,
delivery2_.city as city2_2_2_,
delivery2_.street as street3_2_2_,
delivery2_.zipcode as zipcode4_2_2_,
delivery2_.status as status5_2_2_
from
orders order0_
inner join
member member1_
on order0_.member_id=member1_.member_id
inner join
delivery delivery2_
on order0_.delivery_id=delivery2_.delivery_id
(4) ver4: dto로 바로 조회
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderRepository.findOrderDtos();
}
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;
}
//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();
}
select
order0_.order_id as col_0_0_,
member1_.name as col_1_0_,
order0_.order_date as col_2_0_,
order0_.status as col_3_0_,
delivery2_.city as col_4_0_,
delivery2_.street as col_4_1_,
delivery2_.zipcode as col_4_2_
from
orders order0_
inner join
member member1_
on order0_.member_id=member1_.member_id
inner join
delivery delivery2_
on order0_.delivery_id=delivery2_.delivery_id
📌 ver3처럼 엔티티를 DTO로 변환 VS ver4처럼 DTO로 바로 조회
- 전자의 경우 엔티티로 조회하면 리포지토리 재사용성이 좋고 개발도 단순
- 후자의 경우 원하는 데이터를 직접 선택하므로 네트워크 용량이 더 최적화
- but 리포지토리 재사용성이 떨어지고 API스펙에 맞춘 코드가 리포지토리에 들어가는 단점
- 이렇게 화면에 맞춘 코드가 필요할 경우 쿼리용 리포지토리 별도로 분리(화면 조회 전용)
- 기본 리포지토리는 엔티티를 순순하게 조회하는 용도
📌 쿼리방식 선택 권장순서
1. 우선 엔티티를 DTO로 변환(유지보수성)
2. 필요시 fetch join으로 성능 최적화 -> 대부분의 성능이슈는 여기서 해결
3. 그래도 안되면 DTO로 직접 조회
4. 최후의 방법은 JPA가 제공하는 네이티브 SQL, 스프링 JDBC Template을 사용해서 SQL 직접 사용