ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] N+1 문제
    프로그래밍/JPA 2022. 3. 2. 15:39
    반응형

    이 글은 혼자 학습한 내용을 바탕으로 작성되었습니다.

    틀리거나 잘못된 정보가 있을 수 있습니다.

    댓글로 알려주시면 수정하도록 하겠습니다.


     

    1. N+1이란

    조회하고자 하는 데이터를 조회하고 조회된 엔티티에 연관 엔티티를 추가로 JPA가 조회하여 특정 엔티티들을 조회하기 위 부가적으로 연관 엔티티 조회 쿼리가 추가로 실행되는 것을 의미합니다.

     

    예를 들어 멤버 엔티티가 팀 엔티티와 연관 관계가 있는 상황에서 멤버의 이름이 '김'으로 시작하는 멤버를 조회하기 위해 1개의 쿼리를 실행합니다. 이 결과로 10명의 멤버가 조회되고 멤버 엔티티에 연관된 엔티티의 팀 엔티티를 조회하기 위해 추가로 10개의 쿼리가 수행되는 것입니다.

     

    2. 즉시 로딩 N+1

    PK를 이용한 조회

    위 테스트의 경우 Repository의 findById를 호출 시 PK 값(Id)을 이용하여 조회하기 때문에 연관관계의 엔티티를 Join을 이용하여 하나의 쿼리에서 멤버와 팀 모두 조회하는 것을 확인할 수 있습니다.

     

    이 경우는 PK값을 이용하여 조회를 하였기 때문에 JPQL을 사용하지 않기 때문에 N+1이 발생하지 않고 Join을 이용한 조회가 되는 것입니다.

     

    ※ Outer Join이 아닌 Inner Join을 사용하고 싶은 경우 연관관계 어노테이션의 optional 속성을 false로 설정하면 Inner Join을 이용하여 조회를 진행 합니다.

    메소드 명명법을 이용한 조회

    위 테스트의 경우 Repository의 findAllByName 메소드를 이용하여 멤버의 이름으로 조회한 결과입니다.

     

    위 메소드의 경우 JPA가 JPQL을 이용하여 쿼리를 작성하고 실행합니다.

     

    그러므로 실행된 쿼리를 확인해 보면 먼저 이름이 'tester1'로 등록된 멤버를 조회합니다. 그 결과 2명의 멤버가 조회됩니다.

     

    이후 각각의 멤버의 팀 정보를 조회하기 위해 2개의 팀 조회 쿼리를 수행한 것을 확인할 수 있습니다.

     

    Query 어노테이션을 이용한 조회

    Repository에 Query 어노테이션을 사용하여 JPQL을 작성 하였습니다. 이제 AllByName메소드를 호출 하면 Query 어노테이션에 작성한 JPQL이 실행 됩니다.

     

    위 실행 쿼리처럼 findAllByName 메소드를 이용한 것과 동일한 쿼리가 실행되는 것을 확인 할 수 있습니다.

     

    두 메소드 모두 JPQL을 사용하므로 동일한 결과와 동일한 쿼리가 실행되는 것 입니다.

     

    3. 지연 로딩 N+1

    지연 로딩으로 설정한 멤버의 경우 findById 메소드를 호출 하여도 Join을 이용한 쿼리를 수행하지 않습니다.

     

    지연 로딩의 경우 연관된 엔티티가 사용되는 시점에 해당 엔티티들 조회하기 때문에 단순 멤버의 조회는 멤버 조회 쿼리만 실행하게 됩니다.

     

    이번에는 멤버를 조회한 뒤 멤버의 팀명을 가져오는 경우를 살펴 보겠습니다.

    서비스 클래스에서 멤버를 조회하고 해당 멤버에 대한 팀명을 반환하는 메소드를 테스트에서 호출 하였습니다.

     

    결과 멤버 조회 쿼리 1개와 해당 멤버의 팀명을 반환하기 위해서 팀을 조회하는 쿼리 2개가 수행 된 것을 알 수 있습니다.

     

    만약 멤버가 다수인 경우 멤버를 조회하기 위한 쿼리 1개와 조회한 멤버의 수 만큼 팀명을 조회하기 위한 N개의 쿼리를 수행하게 됩니다.

     

    즉 지연 로딩의 경우 연관관계의 엔티티를 사용하는 시점에 N+1의 문제가 발생합니다.

     

    4. 해결 방법

    해결 방법으로는 Query 어노테이션에 Join Fetch를 사용하거나 EntityGraph 어노테이션을 추가 사용하는 방법 2가지가 존재 합니다.

     

    Join Fetch

    Query어노테이션에 join fetch를 사용하여 연관관계가 있는 엔티티를 직접 연결해 주는 방법입니다. 이 경우 JPQL은 멤버와 팀이  join된 쿼리를 실행하여 N+1 문제를 방지 할 수 있습니다.

     

    즉시 로딩인 경우

    즉시 로딩인 경우 Join Fetch를 설정한 메소드 AllByNameJoinFetch를 호출하면 Inner Join으로 Join된 쿼리를 실행하여 연관된 팀 엔티티를 포함하여 조회하는 것을 확인할 수 있습니다.

     

    지연 로딩인 경우

    지연 로딩의 경우 또한 즉시 로딩과 동일하게 Inner Join을 통해 연관된 팀 엔티티를 포함하여 조회하는 것을 확인할 수 있습니다.

     

    EntityGraph

    Query어노테이션은 멤버를 조회하는 JPQL을 작성합니다.

     

    그리고 EntityGraph어노테이션에는 멤버와 연관된 팀 엔티티의 변수명을 attributePaths 속성에 전달 합니다.

     

    이 경우 또한 멤버와 팀이 join된 쿼리를 실행하여 N+1 문제를 방지 할 수 있습니다.

     

    즉시 로딩인 경우

    즉시 로딩인 경우 EntityGraph어노테이션을 사용한 메소드 AllByNameEntityGraph를 호출하면 Left Outer Join으로 Join된 쿼리를 실행하여 연관된 팀 엔티티를 포함하여 조회하는 것을 확인할 수 있습니다.

     

    지연 로딩인 경우

    지연 로딩의 경우 또한 즉시 로딩과 동일하게 Left Outer Join을 통해 연관된 팀 엔티티를 포함하여 조회하는 것을 확인할 수 있습니다.

     

    6. Join Fetch, EntityGraph 차이

    Join Fetch와 EntityGraph는 N+1을 해결하기 위해 사용되는 방법입니다. 그러나 이 둘은 생성하여 실행하는 쿼리가 다릅니다.

     

    연관된 엔티티를 연결하는 것은 동일하나 Join Fetch의 경우는 Inner Join을 이용하여 연관관계를 연결하며, EntityGraph는 Left Outer Join을 이용하여 연관관계를 연결 합니다.

     

    사용된 예제 코드는 Git에 있습니다.

    반응형

    '프로그래밍 > JPA' 카테고리의 다른 글

    [JPA] Enum 사용  (0) 2022.01.14
    [JPA] 즉시로딩과 지연로딩 Eager And Lazy  (0) 2022.01.13
    [JPA] Attribute Converter  (0) 2022.01.12
    [JPA] Dirty Checking(변경 감지)  (0) 2021.12.24
    [JPA] 예외 처리  (0) 2021.11.05

    댓글

Designed by Tistory.