데이터 간 연관관계에서 XtoMany에 해당하는 데이터는 없는 상태에서 조회하는 것이다. 이번에도 마찬가지로 인터넷 쇼핑몰 서비스를 가정하고 생각해보자. 단, 일반 쇼핑몰과는 다르게 한 주문에서는 한 가지 품목만 주문이 가능하다.
해당 서비스에서 주문 조회 API 설계에 대해 생각해보자. 한 주문에서 한 가지 상품만 주문이 가능하기 때문에 주문과 물건과의 관계는 OneToOne이 된다. 공동 구매 기능이 별도로 없기 때문에 주문과 회원의 관계 역시 OneToOne 상태를 갖게 된다.
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> orderV2() {
List<Order> orders = orderRepository.findAll();
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
가장 단순한 방식의 Order를 모두 찾아온 후, 이를 Dto로 변환하는 방식이다.
select
order0_.order_id as order_id1_6_,
order0_.delivery_id as delivery4_6_,
order0_.member_id as member_i5_6_,
order0_.order_date as order_da2_6_,
order0_.status as status3_6_
from
orders order0_
inner join
member member1_
on order0_.member_id=member1_.member_id limit ?
select
member0_.member_id as member_i1_4_0_,
member0_.city as city2_4_0_,
member0_.street as street3_4_0_,
member0_.zipcode as zipcode4_4_0_,
member0_.name as name5_4_0_
from
member member0_
where
member0_.member_id in (
?, ?
)
주문 조회를 햇을 뿐인데 총 2번의 쿼리가 발생했다. JPA를 이용하여 구현하다보면 가장 많이 발생하는 N+1 문제이다.
해당 API에서는 적은 양의 쿼리가 발생한 것 처럼 보이지만, 데이터 구조 설계에 따라 여러 관계가 얽혀있다면 더 많은 양의 쿼리가 발생할 수 있다. 이런 일이 발생하는 이유는 데이터 간의 관계를 각각 참조하면서 지연 로딩이 발생하기 때문이다. 이를 Fetch Join을 통해서 해결한다.
@GetMapping("/api/v3.1/simple-orders")
public List<SimpleOrderDto> orderV3_1() {
List<Order> orders = orderRepositoryIn.findAllWithMemberDelivery();
List<SimpleOrderDto> result = orders.stream()
.map(SimpleOrderDto::new)
.collect(Collectors.toList());
return result;
}
@Repository
public interface OrderRepositoryIn extends JpaRepository<Order, Long> {
@Query("select o from Order o join fetch o.member m join fetch o.delivery d")
List<Order> findAllWithMemberDelivery();
}
이번에는 Order를 조회하면서 Order와 연관관계가 있는 테이블들을 모두 JPQL의 Fetch Join을 통해서 한 번에 데이터를 가져오도록 했다.
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
기존에 있던 쿼리들보다 길이가 상당히 길어진 것을 알 수 있다. 그 대신 단 한 번의 쿼리 수행으로 모든 데이터를 가져올 수 있다. 이는 DB 커넥션의 사용을 줄이기 때문에 더욱 효율적인 조회 API 수행으로 이어질 수 있다.
물론 반환되는 데이터는 모두 동일한 값이다!