개발

JPA N + 1 해결

백수왕 2024. 7. 17. 16:03

프로젝트를 진행하면서 마이페이지에서 내가 쓴 리뷰를 조회하는 기능을 하고있었다.

 

내가 쓴 리뷰에는 이미지도 포함되어 있는데, 리뷰와 이미지가 1 : N 관계에 fetch = FetchType.LAZY 전력을 사용하고 있었다.

 

이미지 까지 조인해서 가져오기에는 한 번에 너무 많은 조인이 필요하다고 판단되고 중복된 행이 나타날 수 있어 분리를한 후 이미지를 따로 가져와서 dto에 따로 넣어주기로 했다.

문제 코드

 

 

따로 넣어주고 있는데 이미지의 갯수 + 1 개의 쿼리문이 날라갔다

평소 N + 1 문제에 대해 인식하고 있었지만 대부분의 쿼리는 Querydsl의 프로젝션을 사용해서 발생하지 않았다.

또한 묵시적 조인을 사용했기 때문에 문제가 된다고 생각하지 못 했다.

 

하지만 여기서는 프로젝션을 사용하지 않고 개별쿼리를 사용하니 바로 N + 1 문제가 발생했다.

 

 

첫 번째 트러블 슈팅

 

명시적 조인으로 바꿔준 후 fetchJoin()을 붙여 주었다.

 

 

또 다른 에러

fetchJoin()을 붙여주고나서 imageUrl을 한 번에 불러오는 기대를 했다.

하지만 다른 에러가 떳다.

org.hibernate.query.SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list 

 

이거 또한 알고보니 페치조인 대상을 select 문에 적어주지 않아 발생한 에러다.

 

 

이렇게 사용하면 데이터를 한 번에 가져와서 n + 1 문제가 해결된다.

 

하지만 이렇게만 가져오면 리뷰에 대한 이미지인지 찾기가 힘들다.

 

다음 내용은 Map<Long, List<String>> 형식으로 

리뷰 아이디에 대한 이미지 리스트를 가져오게 못하나? 라는 호기심과 또 다른 트러블 슈팅 방식이다.

두 번째 트러블 슈팅 및 원하는 데이터 형식으로 변환

두 번째 방식으로는 이미지 리스트만 따로 가져와서 넣어주는 방식을 생각했다.

 

하지만 따로 가져와서 넣어준다고 해도 n + 1 문제가 발생할 뿐더러

이미지가 어떤 리뷰의 이미지인지 알 수 없다.

 

그래서 Map<Long, List<String>> 방식으로 반환을 한다면

Long은 이미지가 어떤 이미지인지 확인할 수 있는 key값으로 넣어주어 해결하려고 했다.

 

 

이렇게 바꿔주면 해결은 되지만 Map 형태로 바꿔주는 코드가 너무 난잡하고 가독성이 떨어진다

 

그래서 또 다른 방법을 사용해보기로 했다.

 

어떻게 접근할지 막막했는데 우연히 Queydsl 깃허브에서 힌트를 얻었다.

 

https://github.com/querydsl/querydsl/blob/master/querydsl-collections/src/test/java/com/querydsl/collections/GroupByTest.java

 

 

여기서 보면 내가 원했던 Map을 사용하는 것을 볼 수있다.

 

그래서 직접 적용해봤다.

 

Querydsl transfrom 적용

 

먼저 위에서 말했듯이 image를 따로 넣어주는 방식으로 만들었다

 

실제 쿼리를 보면 공식문서와 조금 다르지만 거의 똑같다.


트러블 슈팅 결과 및 성능 테스트

 

 

N + 1 문제도 해결 되었으니 성능 테스트를 해봤다

 

20만건 기준으로 

6.628s -> 851ms 약 800% 성능 향상