waveofmymind
기록하는 습관
waveofmymind
전체 방문자
오늘
어제
  • 분류 전체보기 (124)
    • 📝 정리 (5)
    • 🌊TIL (9)
    • 💻CS (1)
      • 자료구조 (1)
    • 📙Language (9)
      • ☕Java (6)
      • 🤖Kotlin (3)
    • 🍃Spring (28)
    • 👨🏻‍💻알고리즘 (67)
      • 프로그래머스 (59)
      • 백준 (3)
    • 👷DevOps (4)
      • 🐳Docker (2)
      • 🤵Jenkins (1)

블로그 메뉴

  • 홈
  • Spring
  • Java
  • 알고리즘

공지사항

인기 글

태그

  • SpringAOP
  • 챗GPT
  • kotest
  • spring
  • kotlin
  • 스택
  • 스프링 부트
  • mybatis
  • til
  • 트랜잭션 전파
  • 힙
  • 다이나믹 프로그래밍
  • CORS
  • BFS
  • Open AI
  • LeetCode
  • sql
  • Connection
  • JDBC
  • 스프링
  • chat GPT
  • 완전탐색
  • AOP
  • spring boot
  • 스프링 시큐리티
  • resultset
  • 통합테스트
  • Spring Security
  • 트랜잭션
  • 코틀린

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
waveofmymind

기록하는 습관

[PUDDY] JPA N+1 문제 해결하기
📝 정리

[PUDDY] JPA N+1 문제 해결하기

2023. 4. 10. 01:21

전에 일대일 양방향 관계에서 발생했던 문제로 질문글 한개를 조회할때 발생하는 쿼리가 7개나 되었다.

 

프론트에서도 테스트할 때 별로 성능 이슈가 발생하지 않아서 상관 없을 것이라고 했지만, 백엔드 개발을 담당하는 사람으로써

엔티티 하나를 조회할때 필요한 정보가 많더라도 쿼리가 7개나 발생하는 것은 양심에 가책을 느꼈다.

그래서 이번 기회에 쿼리 수를 최대한 줄여보자 라는 마음에 기존 로직을 뜯어보게 되었다.

https://waveofymymind.tistory.com/112

 

[트러블 슈팅] 일대일 관계에서의 지연로딩 트레이드 오프

프로젝트가 진행됨에 따라 테이블이 점점 많아지고 있다. 그에 따라 하나를 조회할 때 발생하는 쿼리 수도 예상치 못하게 많아지게 되어 한 테이블 조회 시 쿼리가 6개나 발생하는 것도 경험해

waveofymymind.tistory.com

우선, 전 글에서 유저 - 펫, 유저 - 전문가 엔티티를 모두 일대일 단방향 관계로 변경했다.

예상한 대로 지연 로딩으로 모두 잘 기능하였다.

그리고 우선 질문글 조회에 대한 로직을 querydsl을 사용하여 페치 조인을 통해 연관된 레코드를 한꺼번에 조회하도록 변경했다.

public Optional<Question> getQuestion(Long questionId) {
        return Optional.ofNullable(queryFactory
                .selectFrom(question)
                .leftJoin(question.user, user).fetchJoin() // User 엔티티를 함께 조회하기 위한 조인
                .leftJoin(user.pet, pet).fetchJoin() // Pet 엔티티를 함께 조회하기 위한 조인
                .leftJoin(question.answerList, answer).fetchJoin() // Answer 엔티티를 함께 조회하기 위한 조인
                .where(question.id.eq(questionId)) // 주어진 questionId에 해당하는 엔티티를 조회하기 위한 조건
                .fetchOne());
    }

 

위 메서드를 실행시

  1. 질문글 조회 쿼리 발생
  2. 연관된 유저 페치조인
  3. 유저의 펫도 같이 페치조인
  4. 답변글 페치조인
  5. 답변글 작성자 쿼리 추가로 발생
  6. 이미지 리스트에 대한 쿼리 발생

아직 답변글 작성자, 이미지 리스트에 대한 쿼리가 2개가 추가로 발생했다.

 

그래서 추가로, 답변글을 조회할때 답변글에 대한 연관된 유저도 페치조인을 통해 가져오도록 설정했다.

 

그 결과, 쿼리가 2개로 줄었다!

 

이제 마지막으로 이미지 리스트도 페치 조인을 통해 가져오면 1개로 모든 조회가 가능할 것이라고 생각했다.

 

그러나, 컬렉션에 대해 페치 조인을 두개 이상 할 경우 MultiBagException을 발생시킨다고 한다.

 

우선, MultipleBagFetchException이 발생하는 원인부터 알아보자면, 이는 중복을 방지하기 위해 터지는 예외이다.

즉, join된 두 컬렉션 간의 카테시안 곱이 발생함을 방지하기 위해서이다. 만약, 지금처럼 답변글이 1개, 이미지가 2개인 경우에는 1 * 2 = 2이므로 성능에 커다란 영향을 미치지 않겠지만, 답변글이 200개, 이미지 100개라면 무려 20000개의 결과가 나오므로 엄청난 성능 저하가 발생할 것이다.

 

Set 컬렉션을 사용하기

 

그래서 완벽한 해결 방법까진 아니지만, List와 Set을 같이 페치조인할 때는 컬렉션이지만 예외가 발생하지 않는다.

Set은 중복을 허용하지 않기 때문이다

그러나 단점으로는 Set은 순서가 없기 때문에 인덱스를 지원하지 않는다.

 

BatchSize 사용하기

기존의 컬렉션 페치조인은 이미지와 답변글 중 데이터가 더 많은 테이블에 설정하고,

나머지 테이블에는 @BatchSize를 선언해준다.

이런 식으로 컬렉션 위에 @BatchSize를 정해주면 N+1 문제가 발생하는 대신, 한꺼번에 지정한 size만큼 IN 절로 조회해온다.

혹은, 전역적으로 처리하기 위해 yml파일에 

  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000

위와 같이 설정할 수 있다.

 

그러나 이 경우 배치 사이즈의 조절이 필요하다. 

이러한 batch fetch가 활성화되었을 때 Hibernate는 많은 쿼리를 준비하게 되고, 이 과정에서 많은 메모리를 사용한다.

따라서 전역적으로 적용하는 batch_size는 10, 50 같은 작은 숫자로 하고, 커다란 batch 작업이 필요할 때에는 @BatchSize를 이용해 큰 값을 설정해주는 것이 이상적이다.

 

 

 

'📝 정리' 카테고리의 다른 글

[트러블 슈팅] 도메인 설계에 대한 고민  (0) 2023.05.19
[PUDDY] 첫 프로젝트 마무리  (0) 2023.04.25
[PUDDY] 일대일 관계에서의 지연로딩 고민해보기  (0) 2023.04.08
클린 아키텍처에 대한 지난 프로젝트 회고  (0) 2023.03.14
    '📝 정리' 카테고리의 다른 글
    • [트러블 슈팅] 도메인 설계에 대한 고민
    • [PUDDY] 첫 프로젝트 마무리
    • [PUDDY] 일대일 관계에서의 지연로딩 고민해보기
    • 클린 아키텍처에 대한 지난 프로젝트 회고
    waveofmymind
    waveofmymind

    티스토리툴바