JPA N+1 문제?

박건태·2023년 10월 24일
0

jpa를 사용할때 n+1문제가 발생한다는 것을 보고 어떤 주제인지 궁금해서 바로 검색해 보았다.

나는 mybatis만 사용해 보았기 때문에 아직 jpa관련된 설정들은 잘 모른다. 따라서 인터넷에 나와있는 글을 옮겨 적어 놓겠다.

n+1문제란?

  • 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 개수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상

현상 재현

  • DB 구조는 유저(USER)는 한개의 팀(TEAM)에만 속할 수 있고 팀(TEAM) 하나는 여러 명의 유저(USER)가 가입할 수 있다. 테스트 데이터로는 4개의 팀당 유저 4명씩 총 20명의 유저를 추가했다.

  • Fetch 모드를 EAGER(즉시 로딩)으로 한 경우

    JpaRepository를 extends한 Interface 객체인 TeamRepository에서 findAll을 호출을 하면

    n+1문제가 발생한다.

  • Fetch 모드를 LAZY(지연 로딩)으로 한 경우
    Team과 User Entity객체에서 Fetch모드만 LAZY로 변경한 후 똑같이 findAll을 호출 하면

    이번에는 n+1문제가 발생하지 않는 것처럼 보인다.

    하지만 아래와 같이 users를 사용하려고 하면 n+1문제가 발생하게 된다.


    즉 지연 로딩에서는 n+1문제가 발생하지 않는 것처럼 보였지만 막상 객체를 탐색하려고 하면 n+1문제가 발생되어 n+1문제가 발생되는 시점만 즉시 로딩과 다를 뿐이다.

발생 이유?

  • n+1문제가 발생하는 이유는 JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 Fetch전략을 참고하지 않고 오직 JPQL자체만을 사용한다.
  1. Fetch전략이 즉시 로딩인 경우
    1. findAll()을 한 순간 select t from Team t 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select from team 이라는 SQL이 생성되어 실행된다. ( SQL 로그 중 Hibernate: select team0.id as id1_0, team0.name as name2_0 from team team0_ 부분 )
    2. DB의 결과를 받아 team 엔티티의 인스턴스들을 생성한다.
    3. team과 연관되어 있는 user 도 로딩을 해야 한다.
    4. 영속성 컨텍스트에서 연관된 user가 있는지 확인한다.
    5. 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 개수에 맞게 select
    from user where team_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )

  2. Fetch 전략이 지연 로딩인 경우
    1. findAll()을 한 순간 select t from Team t 이라는 JPQL 구문이 생성되고 해당 구문을 분석한 select * from team 이라는 SQL이 생성되어 실행된다. ( SQL 로그 중 Hibernate: select team0.id as id1_0, team0.name as name2_0 from team team0_ 부분 )
    2. DB의 결과를 받아 team 엔티티의 인스턴스들을 생성한다.
    3. 코드 중에서 team 의 user 객체를 사용하려고 하는 시점에 영속성 컨텍스트에서 연관된 user가 있는지 확인한다
    4. 영속성 컨텍스트에 없다면 2에서 만들어진 team 인스턴스들 개수에 맞게 select * from user where team_id = ? 이라는 SQL 구문이 생성된다. ( N+1 발생 )

해결방법

  • n+1문제를 해결하는 방법에는 Fetch Join, EntityGraph 어노테이션, Batch Size등의 방법이 있다.
  1. Fetch Join
    - JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다. (SQL Join문을 생각하면 된다.)
    별도의 메서드를 만들어주어야 하며 @Query 어노테이션을 사용해서 "join fetch 엔티티.연관관계_엔티티" 구문을 만들어 주면 된다.

    SQL로그를 보면 별도의 지정을 안하면 JPQL에서 join fetch 구문은 SQL문의 inner join 구문으로 변경되어 실행된다.


  2. EntityGraph 어노테이션
    • @EntityGraph라는 어노테이션을 사용해서 fetch 조인을 하는 것인데 그냥 이런게 있구나만 알아두고 사용하지 말자. 사용하는 순간 조금만 관계가 복잡해져도 헬게이트가 열린다.
  3. Batch Size
    • 이 옵션은 정확히는 n+1문제를 안 일어나게 하는 방법은 아니고 n+1문제가 발생하더라도 select from user where team_id=? 이 아닌 select * from user where team_id in (?,?,?) 방식으로 n+1문제가 발생하게 하는 방법이다. 이렇게 하면 100번 일어날 n+1문제를 1번만 더 조회하는 방식으로 성능을 최적화 할 수 있다.

실무에서 n+1문제로 DB가 죽어버리는 문제를 방지하기 위해서는 어떻게 해야 할까?

  • 우선 연관관계에 대한 설정이 필요하다면 FetchType을 성능 최적화를 하기 어려운 즉시 로딩(EAGER)을 사용하는게 아니라 지연 로딩(LAZY) 모드로 사용을 하고 성능 최적화가 필요한 부분에서는 Fetch 조인을 사용한다.
    또한 기본적으로 Batch Size의 값을 1000이하로 설정한다.(대부분의 DB에서 IN절의 최대 개수 값 : 1000)
    그 외에 팀바팀이긴 한데 꼭 연관관계 설정이 필요 없다면 n+1문제로 인하여 DB가 죽어버리는 불상사를 막기 위해 연관관계를 끊어버리고 사용하는 것도 방법이다.

출처 : https://programmer93.tistory.com/83


아직 이해가 잘되지않는 부분이 많아서 JPA에 관하여 더 공부해보고 실습까지 해보고 다시 찾아봐야 될것같다.

0개의 댓글