N+1 문제를 어떻게 해결할 수 있을까?
이 글은 팀원 브루니 작성했습니다.
프로젝트를 진행하며 다음과 같은 문제를 해결해야했습니다.
쿠폰그룹조회시쿠폰그룹내의쿠폰들의 이름과쿠폰그룹들의 [잔여 개수 / 총개수] 를 계산하고 페이징 처리하여쿠폰그룹목록을 반환
이번 포스팅에서는 이 문제를 해결하는 과정을 포스팅해보려 합니다.
테이블간의 관계
쿠폰그룹 과 쿠폰 은 다음과 같은 연관관계를 가지고 있습니다.
위의 그림에서 볼 수 있듯이 쿠폰그룹 과 쿠폰은 일대다 연관관계를 가지고 있습니다. 하나의 쿠폰그룹 조회시 여러개의 쿠폰 을 보여줘야 했는데요. JPA를 사용하고 있는 저희 프로젝트에서 간단하게 떠올릴 수 있는 방법은 다음과 같았습니다.
OneToMany 양방향 연관관계를 사용해 하나의 쿠폰그룹 조회시 해당 쿠폰그룹의 쿠폰들을 조회
OneToMany 양방향 연관관계를 이용?
OneToMany 양방향 연관관계를 이용하면 다음과 같이 엔티티를 구성해야 했습니다.
이후 서비스 로직에서 아래와 같이 쿠폰그룹내의 쿠폰들을 조회할 수 있었습니다.
하지만 막상 코드를 실행시켜보면 아래와 같은 쿼리가 날라가는 것을 확인할 수 있습니다.
쿼리의 결과를 보면 쿠폰그룹을 전체 조회하는 쿼리 1번, 각각의 쿠폰그룹에 대하여 쿠폰 목록이 조회되는 쿼리가 N번 날라가는 것을 확인할 수 있습니다. 이는 쿠폰그룹의 개수가 많아질수록 성능이 떨어지기 때문에 개선해야합니다.
Fetch Join
N + 1 문제를 해결하는 다양한 방법 중 먼저 FETCH JOIN을 사용해 문제를 해결해보고자 했습니다.
OneToMany 관계에서 조인을 하게 되면 카테시안 곱이 발생해 의도와는 다른 결과를 얻을 수 있기 때문에
DISTINCT키워드를 붙여 중복을 제거합니다.페이징을 수행하기 위해서는
@Query에countQueryattribute도 넣어주어야 하기 때문에 아래와 같이 코드를 작성했습니다.
왜 countQuery를 넣어야 하는가?🧐 JPA에서
@Query를 사용하지 않고 페이징을 수행하게 되면 의도하지 않았던 countQuery가 날라가는 것을 확인할 수 있는데요, 이는Page인터페이스의getTotalElements()의 메서드의 수행 결과를 반환해야 하기 때문이 아닌가 예상하고 있습니다. 따라서@Query를 사용해 직접 쿼리를 날려 페이징을 수행하기 위해서는 별도의 countQuery가 필요합니다.
결과를 보면 다음과 같은 쿼리가 날라가고 있는 것을 확인할 수 있습니다.
하나의 쿼리로 조인을 해오는 것을 확인할 수 있었습니다. 그런데 이상한 점은 LIMIT이 안보인다는 점인데요. 로그를 보니 아래와 같은 메시지를 보내고 있었습니다.
JPA를 사용할 때 OneToMany 관계에서 FETCH JOIN과 Pagination을 함께 사용하면 쿼리의 결과를 모두 메모리에 적재한 뒤 메모리에서 페이징을 수행하기 때문입니다. 이와 같은 방법은 OOM(Out Of Memory)문제를 발생시킬 수 있기 때문에 좋지 않은 방법입니다.
default_batch_fetch_size 를 통한 문제 해결
JPA의 XXXToMany 관계에서 FETCH JOIN과 페이징을 함께 사용할 수는 없습니다. 그렇기 때문에 FETCH JOIN을 제거하고 N+1 문제를 해결하기 위해 default_batch_fetch_size 옵션을 통해 문제를 해결했습니다.
default_batch_fetch_size 옵션을 활용하면 Lazy Loading으로 인해 발생되는 N개의 쿼리를 설정한 옵션만큼 모아 IN 절로 처리하게 됩니다. 예를들어 100개의 추가 쿼리가 발생한다고 했을 때 default_batch_fetch_size 옵션을 10으로설정하게 되면 발생하는 추가 쿼리의 수를 100 / 10으로 줄일 수 있게 됩니다.
실제로 발생하는 쿼리를 확인해볼까요?
마지막 쿼리를 확인해보면 IN 절을 통해 데이터를 한 번에 가져오는 것을 확인할 수 있었습니다.
물론 이 방법에도 문제가 있습니다. 쿠폰그룹의 수가 굉장히 많다면 N/(옵션으로 설정한 값)번의 쿼리가 추가적으로 발생하는 것은 어쩔 수 없습니다. 하지만 저희 프로젝트에서는 페이징을 통해 한 페이지당 조회하는 쿠폰그룹의 수가 제한되어 있기 때문에 이 방법을 통해 N+1 문제를 해결할 수 있었습니다.
Last updated