[트러블 슈팅] 전체 후기 조회시 N+1로 응답하는 쿼리 해결하기
해당 글은 수강후기 공유 시스템 프로젝트를 진행하며 겪은 문제입니다.
문제 상황:
수강 후기 전체 데이터를 조회할 때, 강의 정보/후기/평점을 개별적으로 지연 로딩(Lazy Loading) 하면서 N+1 문제가 발생하여 조회 성능이 크게 저하되는 문제가 발생했습니다. 조회 속도는 초기 10초이상이 발생하였습니다. 조회 시 하나의 강의 목록을 가져온 뒤, 각 강의마다 후기와 평점을 추가로 조회하는 방식으로 수십-수백 건의 쿼리가 추가 발생해 응답 지연이 심화될 수 있었습니다.
N+1 문제란?
연관관계가 걸린 엔티티를 조회할 때, 첫 쿼리(1번)로 부모 엔티티 N개를 가져오고, 각 부모 엔티티마다 연관된 자식 엔티티를 조회하기 위해 추가로 N번 쿼리가 발생하는 문제.
해결방법
1. Fetch Join
연관 엔티티를 조인하여 한번에 가져옵니다. 장점으로는 즉시 해결 가능하고, 쿼리수가 줄어든다는 장점이 있습니다. 단점으로는 조인 결과가 많아질 경우 성능 저하와 페이징이 불가하다는 점이 있습니다. 대부분의 경우 즉시 로딩일 이기에 고려해볼 수 있습니다.
2. Batch Size 설정
JPA/Hibernate에서 Lazy Loading 시 여러 개의 연관 엔티티를 모아서 한 번에 조회하는 최적화 기법입니다. N+1 쿼리를 완전히 없애지는 않지만 N+1 을 1+M(M은 N/batch_size 한것) 으로 줄여줍니다.
저는 Lazy한 엔티티로 구성한 상태였고,
먼저 문제상황을 정확히 검증하기 위해서 해당 쿼리를 생성한 data jpa 메서드를 테스트 환경에서 구성해보았습니다. 데이터베이스는 로컬 환경에서 실행하였습니다.
@Transactional
@Test
@DisplayName("수강후기 작성 요청이 오면, 수강후기를 추가하고 별점을 증가시킨다.")
void addReview() {
ClassReviewRequest request = ClassReviewRequest.of(
"작성글제목",
"작성글내용",
20191434L,
4.5,
"아이데이션융합실습4-SW(캡스톤디자인)"
);
Lecture lecture = lectureDataRepository.findByLectureName(request.getLectureName()).get();
User user = userDataRepository.findById(request.getUserNumber()).get();
ClassReview review = ClassReview.create(lecture, user, request.getStarLating(), 0, request.getPostContent(), request.getPostTitle());
assertThat(classReviewDataRepository.findByUserNumberAndLecId(user, lecture)).isNotNull();
lecture.addReview(request.getStarLating());
em.persist(review);
em.flush();
em.clear();
Lecture lecture2 = lectureDataRepository.findByLectureName(request.getLectureName()).get();
assertThat(lecture2.getStarRating().getTotalRating()).isEqualTo(request.getStarLating());
assertThat(lecture2.getStarRating().getReviewCount()).isNotZero();
}
진행과정
- 해당 강의와 학생 정보를 조회
- 이미 수강후기가 존재하는지 확인
- 수강후기에 별점 추가 후 persist 처리
- 영속성 detached
- 강의 재 조회 후 별점이 변경되었는지 검증
테스트 결과, 연관된 엔티티도 많이 구성되어있어서 필요한 엔티티가 많은 상태였습니다. Fetch Join을 두면 추후에 성능 저하가 일어날 가능성이 있다고 판단하여 Batch Size를 100
에서 10
으로 줄여 연관 조회수를 줄였습니다.