[Spring/JPA] - N+1 이슈 (paging을 곁들인)
crud기반의 개인 프로젝트를 만들고 있는데 게시글 리스트 목록 로드API 에서 N+1현상이 발생하였다.
양방형 맵핑관계의 게시글(Board)과 댓글(Comment)의 갯수를 불러오고자 했다.
이 두테이블은 양방향맵핑관계이며
Board->Comment (OneToMany)
Comment->Board (ManyToOne)관계이다.
겉으로 보기에는 정상적으로 로드 되는것 같았지만, 로그를 통해 N+1현상을 확인하게 되었다.
N+1현상은 무엇이며 왜 일어나는 것일까??
N+1 이슈란?
JPA같은 ORM에서 연관된 엔티티를 조회할 때 발생하는 성능이슈.
한번의 쿼리로 원하는 데이터를 가져오는것처럼 보이지만, 실제로 추가적인 N개의 쿼리가 실행되는 문제
N+1 발생 원인??
JPA에서 @OneToMany, @ManyToOne의 관계는 기본적으로 지연로딩(Lazy)설정이 적용됩니다.
부모 엔티티를 조회할 때, 자식 엔티티를 즉시 가져오지 않고 필요할 때 개별적으로 조회합니다.
따라서 루프를 돌며(부모엔티티의 데이터갯수만큼) 각 연관된 엔티티를 조회할 때 추자거인 쿼리가 발생하여 성능이 저하됩니다.
N+1를 유발하게 되는 순서는 다음과 같다.
1. CrudRepository를 상속받는 Repository에서 findAll(Pageable pageable)메소드를 실행한다.
2. 메소드를 통해 쿼리를 생성하여 DB에서 조회한 결과를 받아 Board 엔티티 인스턴스를 생성한다.(로그의 1라인)
3. 그리고 Board와 연관관계에 있는 Comment를 추가로 조회 및 생성한다. (로그의 2라인)
4. Comment는 1번만 조회해야하는데 로그를 보면 총 11번이 조회가 되었다. 왜냐하면 Board의 데이터가 10개 존재하기 때문에 Board조회시 추가적으로 양방향관계에 있는 연관 엔티티 Comment가 추가적으로 조회 된 것이다.
5. 정리해보자면 Board조회시 Board의 연관관계에 있는 Comment를 조회 하게 되는데 이를 Board의 갯수만큼 추가로 조회하게 되는 현상을 N+1현상이라고 한다. 그리고 이 현상이 발생하게 되면 성능 및 부하문제를 가져올 수 있다.
그리고 이 N+1 해결 대안으로는 아래와 같이 3가지 대안이 있다.
N+1 해결대안
해결대안은 크게 아래와 같이 3가지가 있다.
1. Fetch Join
2. @EntityGraph
3. @BatchSize
1.Fetch Join
가장 일반적으로 사용하는 방법으로 Fetch Join을 쿼리로 사용하는것이다. 이는 SQL 조인을 사용해서 연관된 엔티티를 함께 조회되도록 동작한다.
위 처럼 (LEFT) JOIN FETCH구문을 사용하면 되며 JOIN FETCH대상 엔티티는 Board산하의 comment를 명시해준다.
일반 SQL 쿼리처럼 Comment 엔티티를 직접적으로 명시해주면 안된다!
또한 LEFT JOIN을 사용한 이유는 게시글에 댓글이 존재하지 않는경우도 있기에 게시글 중심으로 데이터를 로드해야 하기에 LEFT JOIN을 사용하였다.
위와 같이 연관 엔티티를 한꺼번에 조회하도록 JOIN을 통해 조회가 된다.
다만 여기서 문제가 생긴게있으니.. 조회할때 마다 아래의 로그가 발생하였다.
firstResult/maxResults specified with collection fetch; applying in memory
fetch콜렉션을 통해 명시된 첫번째 결과와 최대(?)결과가 메모리를 사용하고 있다.
2. @EntityGraph
위와같이 사용하고자 하는 JPA Repository 메소드에 @EntityGraph어노테이션을 추가하고 FK가 존재하는 주인관계에 있는 엔티티(comment)의 속성을 기재하면 끝이다.
N+1현상은 쉽게 사라졌지만 해당 방법도 firstResult/maxResults specified with collection fetch; applying in memory
경고가 발생하였다.
그래서 혹시나해서 Return Type을 Page가 아닌 List로 코드수정을 해보니 메모리 관련 에러가 사라졌다.
아무래도 Paging과정에서 이슈가 있는것같아 원인을 찾아보니 페이징이나 정렬이 데이터베이스가 아닌 메모리에서 수행되고 있기 때문이라고 한다.
3. BatchSize
부모엔티티에서 양방향관계에서의 주인엔티티(외래키를 가지고 있는 엔티티) 필드에 @BatchSize어노테이션을 추가하는 방법이다.
해당 어노테ㅐ이션을 사용하면 지정한 size만큼 SQL의 IN절을 사용하여 조회한다. 만일 조회엔티티가 10개인데 size=5로 지정하면 2번의 SQL만 추가로 실행한다. 위 스샷처럼 size=10으로 지정하면 1번의 SQL만 추가한다.
그리고 size가 조회엔티티보다 크다면 지정한 size만큼 IN절을 호출한다.
즉 엔티티 조회가 10건, size가 15면 IN절을 15개 호출한다.
반면 size가 8이면 IN절 8개를 두번 호출한다!
BatchSize를 사용하니 N+1현상이 사라지고 board,comment각각의 엔티티를 정상적으로 조회한다.
다만 엔티티조회갯수가 BatchSize의 크기가 IN절 조회수에 영향을 받으니 이 부분을 체크해봐야할것 같다