[JPA] Fetch Join 파보기 1탄 (EAGER, LAZY, N+1문제)

su_y2on·2022년 8월 23일
1

JPA

목록 보기
16/17
post-thumbnail

Fetch Join 파보기 1탄 (EAGER, LAZY, N+1문제)

객체를 조회시에 연관된 객체들까지 모두 N+1문제없이 가져오기위해 자주 쓰이는 Fetch Join + LAZY의 조합 너무 습관적으로 쓰다보니 어느샌가 왜 이렇게 쓰는지 헷갈리기 시작하더라고요... 그래서 제대로 정리해보기로 했습니다

모든 예제에서는 user, folder의 일대다관계를 이용할 것입니다.



1. FetchType = EAGER : 즉시로딩

먼저 매번 연관된 객체를 즉시 로딩해오는 EAGER모드로 진행해보겠습니다. 아래와같이 User클래스에서 folders를 즉시로딩으로 만들어줬습니다. (참고로 원래 @~ToManyLAZY, @~ToOneEAGER모드가 default입니다.)

findAll()의 결과는 아래와 같습니다. 현재 user하나를 만들었고 2개의 folder를 달아놓은 상태입니다. EAGER를 했다고 join문으로 한번에 가져오는 것이 아닌 먼저 user를 전체조회하고나서 각각 user의 folder들을 가져오기위한 join문이 날라갑니다.

user의 수 만큼 추가 쿼리가 날라가게 될 것입니다. 이게 바로 N+1문제입니다! 전체조회를 위해서 한개의 쿼리를 날렸더니 N개의 추가쿼리가 날라가는 문제이죠





2. FetchType = LAZY : 지연로딩

그래서 할 수 있는 다른 방법이 지연로딩입니다. 매번 연관관계가 달려있는 객체를 조회할때마다 추가쿼리가 날라가는 것을 막기위해 로딩을 지연하는 것입니다. 이게 딱 필요할 때는 User를 조회할때 굳이 user의 folder까지 조회할 필요가 없는 상황입니다.

이럴때 folder에 대한 쿼리가 바로 같이 날라가는 것은 낭비이기때문에 실제로 getFolders()같이 folder를 조회하게 되는 시점에 쿼리가 날라가게 됩니다.

이렇게 실제로 getFolder라는 함수를 호출할 때 쿼리가 날라갔습니다. 지금은 user가 하나이기 때문에 그렇지만 LAZY에서 findAll()후에 모든 user에 대해서 getFolders()를 하면 EAGER와 다름없이 N+1문제가 생깁니다.

그렇다고 EAGER이나 LAZY나 그게 그거네! 하면 안되는 것이 확실히 불필요한 연관관계 객체에 대한 조회를 방지하기 때문에 그러한 상환에서는 LAZY가 좋습니다.

이제 N+1문제를 해결해봅시다





3-1. N+1해결책 : Join

지금까지 보면 조회시에 Join문이 한번에 날라가지 않아서 N+1문제가 생겼습니다. 그럼 JPQL로 findAll()을 할때 join문으로 folders까지 찾아오면 해결될 수 있지 않을까요?

이번에는 조금 예제에 사용한 상황을 조금 바꾸겠습니다. 두명의 user와 각각 2개, 1개의 folder를 달아주었습니다.

일단 이렇게 하면 두가지 문제가 있습니다



문제1. Select에 연관객체가 함께 걸려오지 않음

일단 Join문이 한번에 날라가긴 했지만 select문에 user뿐입니다. 이건 위에 쿼리에 SELECT u로 user만 가져오도록 했기 때문입니다. 이러면 우리가 원하는 결과랑은 다르게 됩니다.


문제2. user 중복

아래와 같은 assert문에서 에러가 터집니다. 이는 user의 수가 2명인 것을 확인하기 위한 assert문입니다. 그런데 3명이라고 결과가 나옵니다!! 이는 SQL과 JPQL의 차이에 의한 결과입니다.

assertThat(users).hasSize(2);

실제로 Join을 하면 아래와 같은 결과의 테이블이 나옵니다. 이를 JPQL은 폴더가 하나인 user한개와 폴더가 2개인 user하나로 반환합니다. 따라서 user가 연관관계의 객체의 수만큼 중복이 생깁니다. 따라서 distinct연산으로 이를 해결할 수 있습니다.


문제해결 distinct


이제 중복이 제거되면서 정상적으로 user가 2개로 count됩니다. 하지만 여전히 아래와 같은 쿼리가 추가로 나갑니다. 이는 위에서 1번문제가 해결되지 않았기 때문입니다. 결국 Join문으로는 N+1문제를 해결할 수 없었습니다.





3-2. N+1해결책 : Fetch Join

드디어 Fetch Join을 해보겠습니다.

아래와 같이 findAll()를 JPQL을 써서 Fetch Join으로 만들어주겠습니다.

그러면 아까와는 다르게 Select에도 folder가 함께 걸려오는 것을 확인할 수 있습니다. getFolder()호출에는 아무런 쿼리도 날라가지 않으니 따라서 N+1문제를 깔끔하게 하나의 쿼리로 해결할 수 있습니다.





4. Fetch Join의 응용

여기까지 fetch join과 N+1문제에 대해서 알아봤습니다. 완벽해보이지만 fetch join을 공부하고 활용하다보면 여러가지 문제들이 생깁니다. 예를 들면 여러 일대다 연관관계를 한번에 fetch join해야하거나 ON을 사용해야하거나 Pagination을 함께 써야하는 상황입니다.

이런 상황은 조금만 복잡한 기능을 개발하다보면 자주 만나게 됩니다. 따라서 이럴때 어떻게 Fetch Join을 잘 사용할 수 있을지에 대해서 다음 포스팅에서 다뤄보도록 하겠습니다!

0개의 댓글