기록하자..
Dto 로 성능 최적화 본문
문제
다음과 같은 Payment 엔티티가 있다.

회원의 결제 목록을 조회하기 위해서 Payment와 Member를 fetch join해서 한 번에 가져오려는 시도를 하고 있었다.
@Query(value = "select p from Payment p join fetch p.member m join fetch p.ticket t where m.email = :email",
countQuery = "select count(p) from Payment p")
List<Payment> findByEmail(@Param("email") String email, Pageable pageable);
사용자의 email이 일치하는 경우의 결제 목록을 가져오고 싶어 다음과 같은 쿼리를 작성했다.
이렇게 쿼리를 날리게 되면 다음과 같은 결과를 받을 수 있다.

그런데 이런 방법은 Entity 전체를 노출하게 되고 응답 스펙을 맞추기 위해서 별도의 로직(@JsonIgnore 등)이 추가될 수 있다. 지금은 간단하지만 Member와 관계를 맺고 있는 엔티티가 늘어날 경우 API 스펙이 변하게 되버린다.
해결 - Dto를 활용하자
따라서 Entity를 직접 노출하지 않고 Dto를 이용해서 전달하고자 하는 정보만 반환하도록 하자!
@Query(value = "select new com.letmeclean.payment.dto.response.PaymentDetailResponse(m.email, p.totalPrice, p.quantity, t.name, p.paymentStatus, p.createdAt) " +
"from Payment p " +
"join p.member m " +
"join p.ticket t " +
"where m.email = :email",
countQuery = "select count(p) from Payment p")
List<PaymentDetailResponse> findPaymentsByMemberEmail(@Param("email") String email, Pageable pageable);
이제 결제 목록을 조회하는 요청을 날려보면 다음과 같이 필요한 정보만 넘어오는 것을 확인할 수 있다.

모두 똑같은 정보를 저장해서 이상하게 보일 수도 있지만 페이징처리가 잘 되어, 의도한대로 10개만 넘어오는 것을 확인할 수 있고, 응답시간도 이전보다 빨라진 것을 확인할 수 있다.
이제 쿼리를 확인해보자.
select
member1_.email as col_0_0_,
payment0_.total_price as col_1_0_,
payment0_.quantity as col_2_0_,
ticket2_.name as col_3_0_,
payment0_.payment_status as col_4_0_,
payment0_.created_at as col_5_0_
from
payment payment0_
inner join
member member1_
on payment0_.member_id=member1_.member_id
inner join
ticket ticket2_
on payment0_.ticket_id=ticket2_.ticket_id
where
member1_.email=? limit ?
다음과 같이 1번의 쿼리만 날라가는 것을 확인할 수 있다.
Join, Fetch Join
두 번째 Dto로 조회해오는 쿼리를 확인해보면 fetch join을 쓰지 않고 일반 join을 쓴 것을 확인할 수 있다.
fetch join은 엔티티 객체와 객체 그래프를 함께 조회하는 것이 목적이다. 첫 번째 경우 Payment 엔티티를 조회해오는 과정에서 Member, Ticket 엔티티의 FetchType을 Lazy로 설정해두어서 프록시가 조회되어 예외가 발생하여 fetch join을 사용하였다. 하지만 일반 join의 경우 데이터베이스가 제공해주는 join과 동일한 기능을 하게 된다. 따라서 원하는 필드를 바로 조회할 수 있어 굳이 fetch join을 사용하지 않은 것이다.
이제 두 join의 차이를 알 수 있다.
- 첫 번째 쿼리에서는 Payment, Member, Ticket 엔티티를 모두 조회해와 영속성 컨텍스트에 올린다.
- 두 번째 쿼리에서는 Payment 엔티티만을 영속성 컨텍스트에 올린다.
- 만약 Member, Ticket의 정보가 필요하지 않다면 일반 join을 쓸 것을 권한다.
여기 두 방식에 대한 좋은 글이 있어 남긴다.
What is the difference between JOIN and JOIN FETCH when using JPA and Hibernate
Please help me understand where to use a regular JOIN and where a JOIN FETCH. For example, if we have these two queries FROM Employee emp JOIN emp.department dep and FROM Employee emp JOIN FETCH emp.
stackoverflow.com