Spring JPA를 사용하다보면 쉽게 마주할 수 있는 N+1 쿼리에 대해 원인과 해결 방법에 대해서 알아보려한다.
N+1?
JPA의 Entity 조회 시 내부에 존재하는 다른 연관관계에 접근할 때 또 다시 한번 쿼리가 발생하는 비효율적인 상황을 일컫는 말이다.
1번의 쿼리를 날렸을 때 의도하지 않은 N번의 쿼리가 추가적으로 실행되는 것을 의미한다.
위의 글만 본다면 어떤 상황인지 알기 어려우니 N+1설명 때 가장 많이 사용되는 post예시를 들어보겠다.
N+1 발생 케이스
예시로 게시글에 대한 테이블을 post라고 두고, post에 달린 댓글들을 command라는 테이블이라 지칭하자.
한 post에는 여러 개의 command가 달릴 것이다.
1. 즉시 로딩 (fetchType.EAGER)후 findAll()로 조회하는 경우
@OnetoMany로 두고 즉시 로딩(fetchType.EAGER)로 변경 후 findAll()하게 된다면 문제가 발생한다.
실제 실행되는 쿼리를 보면 post에 대해 select쿼리 실행되고 해당 post에 대한 command를 조회하기 위해서 post의 수만큼 쿼리가 추가적으로 발생하게 된다. (데이터의 수가 많을 수록 성능에 큰 영향을 주게 된다.)
해결방안
@OnetoMany(fetchType.LAZY)로 변경하는 것이다.
변경 이후 findAll() 메서드로 호출하면 지연 로딩이기 때문에 Post select 쿼리만 실행된다.
하지만 이 해결 방안도 loop조회를 하게되면 동일 문제가 발생한다.
2. 지연 로딩(fetchType.LAZY) + Loop 조회하는 경우
@Transactional
@Test
public void loop throws JsonProcessingException {
savePostWithComments(4, 2);
List<Post> posts = postRepository.findAll(); //N+1 발생하지 않는다.
List<Comment> commentList;
for (Post post : posts) {
commentList = post.getCommentList();
log.info("post author: {}", commentList.size()); //N+1 발생한다
}
}
즉시 로딩 (fetchType.EAGER)후 findAll()로 조회하는 경우와 동일 이슈 발생!
이유
LAZY로딩으로 findAll() 조회하면 post에 대한 정보를 조회하게 된다.
그 후 command를 조회하면 post에 대한 조회가 이미 끝난 상태라 join 쿼리가 생성되지 않는다.
where command.postId=?형식으로 JPQL쿼리 생성한다.
이로 인해 매번 조회 쿼리가 생성되어 N번 실행하는 이슈가 발생한다.
해결방안
1. JPQL페치 조인 사용 (사용 추천)
JPQL에서 fetch join 키워드를 사용해서 join대상을 함께 조회한다.(post 조회시 p.commendList도 같이 조회한다.)
LAZY로딩 설정 후 LOOP사용 시 생기던 N+1에 대한 문제도 findAllWithFetchJoin()사용 시 더이상 발생하지않는다.
2. Batch Size 지정 + EAGER
@BatchSize 어노테이션에 size를 지정하고 fetch 타입은 즉시로 설정한다.
이와 같이 진행한다면 findAll() 사용 시 where in 쿼리를 이용해서 배치 사이즈 만큼 조회한다.
(배치 사이즈가 넘는다면 추가로 조회해오는 쿼리가 생성된다.)
하지만 이 해결법은 글로벌 패치 전략을 즉시 로딩으로 변경해야하고 배치 사이즈만큼만 조회가능하여
N+1을 완벽하게 해결하지않아 권장되지는 않는다.
참고
https://blog.advenoh.pe.kr/database/JPA-N1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95/
'DB > JPA' 카테고리의 다른 글
SQL 기본 문법 정리 2 - DCL, TCL (0) | 2022.02.27 |
---|---|
JPA 개념 (0) | 2022.02.24 |