본문 바로가기
JPA/JPA + SpringBoot

(X To ONE ) LAZY로딩에 의한 N+1문제 해결

by 킹차니 2021. 9. 8.
김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
https://www.inflearn.com/courses?s=%EA%B9%80%EC%98%81%ED%95%9C

 

DTO를 반환시에 모든 연관관계마다 LAZY로 설정하고, 엔티티를 반환하기 보다 DTO객체에 담아 반환하는 것은 반드시 필요하다.

EAGER로 설정되면 만약 멤버 엔티티가 name, address, orders, 를 포함하고 있을 때, orders에 대한 정보는 필요하지 않은데,

orders까지 가져온다면 이는 쓸데 없는 쿼리가 나가 성능을 저하시킬 수 있기 때문이다.

 

그렇다면 LAZY를 설정하고 DTO객체에 필요한 데이터만 담는다 해서 모든 것이 해결될까?

그것은 물론 아니다.

LAZY로 인해 연관관계가 있는 엔티티를 조회할 때, N + 1문제가 발생한다. 말보단 코드로 보자.

아래는 Order엔티티를 SimpleOrderDTO로 바꾼뒤 응답하는 컨트롤러이다.

result에 조회된 orders를 각각 stream을 통해 SimpleOrderDTO로 변환한뒤 넣어 반환하고 있다.

 

SimpleOrderDTO 객체는 아래와 같다.

SimpleOrderDTO

SimpleOrderDTO의 생성자를 보면 

o.getMember().getName() 이때 현재 모든 연관관계가 LAZY로 되어 있으므로,  order에 있는 member는 바이트버디에 의해 만들어진 프록시 객체이다. 즉 진짜 member에 대한 내용을 가지고 있지 않다.

그래서 member의 name을 get하기 위해서는 해당 멤버에 대해 쿼리를 날려서 가져와야한다.

 

o.getDelivery().getAddress() : 여기서도 위와 마찬가지이다.

 

그래서 결국 만약 Order의 결과가 2개라면 

맨 처음 orders에 관한 쿼리 + 첫번째 주문의 member에 관한 쿼리 + 첫번째 주문의 delivery에 관한 쿼리 + 두번째 주문의 member에 관한 쿼리 + 두번째 주문에 관한 쿼리 까지 총 5번의 쿼리가 나가는 상황이 발생한다. 하나의 결과를 얻기 위해 총 5번의 쿼리가 나가는 셈이다. 이를 해결하기 위해서는 어떻게 해야할까?

 

 

패치조인

정답은 패치조인이다. 패치조인을 사용하는 쿼리문을 repository에 만들고, 필요한 데이터를 골라 한번에 끌고 오는 것이다.

아래의 repositpry에 findOrderWithMemberDelivery 메소드를 보자.

fetch join을 사용하여 member와 delivery를 함께 불러오는 것이다. 

EAGER을 사용하면 되는 거 아닌가? 라고 생각할 수도 있겠지만 우리가 필요할 때만 연관관계가 걸린 엔티티를 선택적으로 가져올 수 있다는 것이 크게 다르다. 

이를 사용하는 컨트롤러는 다음과 같다.

위에서 만든 N+1문제가 발생하는 로직과 나머지는 똑같다. 하지만 실행결과를 보면 단 한번의 쿼리만 나가는 것을 볼 수 있다. 

 

포스트맨으로 요청해보자.

데이터 잘 받아오는 것을 확인.

 

아래를 보면 fetch join되어 한번의 쿼리만 나가는 것을 볼 수 있다.

 

 

근데 위에서 쿼리를 보면 여전히 order, member, delivery와 같은 엔티티에서 사용하지 않는데, 가져오는 데이터들도 있다. 그래서

아래처럼 DB조회시 처음부터 DTO를 만들어 버리는 방법도 있다.

하지만 위와 같은 방법을 사용한다고 해서 성능이 눈에 뛸 정도로 좋아지는 것은 아니다.

실제로 성능의 차이가 발생하는 곳은 쿼리의 where뒤의 join문 같은 곳에서 발생하는데, 위처럼 한다고해서 join문이 없어지거나 하는 것은 아니기 때문이다. 그리고 위의 레퍼지토리 메소드는 api에 의존하고 있다.

하여 위와 같은 방법은 그리 권장되는 방법은 아니다. (물론 테이블 수가 엄청나게 크다면 고민해볼만 하다.)