ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • N + 1 정복하기 <3. Fetch Join>
    프레임워크/Spring Boot 2024. 2. 21. 02:57

     

    시리즈1. https://iwsaitw.tistory.com/entry/N-1-정복하기

    시리즈2. https://iwsaitw.tistory.com/entry/N-1-정복하기-2-FetchTypeEAGER 

     

     

    (소스코드: https://github.com/blog-example/-JPA-N_Plus_1)

    ORM을 사용하면 만나는 흔한 이슈 중 하나인 N + 1 !

    명확하게 설명할 수 있을 정도로 머리속에 집어넣어보겠습니다.

     

    이번 글에서는 fetch join을 활용해서 N + 1을 해결하는 과정을 살펴보겠습니다.

     

     


     

     

    1. JPQL? Fetch Join?

    Fetch Join 을 이야기 하기에 앞서서 JPQL을 먼저 이야기할 필요가 있습니다.

    gpt가 뭐라고 정의하는지 한 번 보겠습니다.

     

     

    JPQL은 개발자가 원하는 동작을 직접 데이터베이스에 전달할 수 있도록 JPA에서 제공하는 기능입니다. Fetch Join은 JPQL이 할 수 있는 기능 중에 연관된 엔티티나 컬렉션을 SQL의 JOIN을 사용해서 한 번의 쿼리로 조회하는 기능입니다.

    즉 Fetch Join을 사용한다는 것은 JQPL의 기능 중 Fetch Join을 사용한다는 의미입니다.

    String jpql = SELECT e FROM Employee e JOIN FETCH e.department WHERE e.salary > 50000

     

    이전 글에서 살펴보았 듯이, N + 1은 필요한 데이터를 두 번의 쿼리로 나눠서 로드하면서 발생하는 이슈이고, 이를 JPQL을 사용해서 한 번의 쿼리로 로드하도록 만들어서 이슈를 해결할 것 입니다.

     

     

    2. JPQL을 적용하는 방법들

    1.  Repository에서 @Query 어노테이션 사용

    JPA가 default로 제공하던 findAll의 동작을 변경하는 방식입니다.

    @Query 어노테이션을 추가해주고, 원하는 내용의 JPQL을 추가해줍니다.

    public interface PostRepository extends JpaRepository<Post, Long> {
      @Query(value = "SELECT p FROM Post p JOIN FETCH p.comments")
      List<Post> findAll();
    }

     

    아주 간단하게 문제가 해결이 되었습니다.

    이제는 데이터베이스에서 데이터를 fetch할 때 join을 사용해서 한번에 로드하는 것을 확인할 수 있습니다.

     

     

    2.  Service에서 Entity Manager 사용

    이번에는 service 레이어에서 N + 1을 해결해보겠습니다.

    위의 @Query 예제와 사용하는 모양만 조금 달라졌지 비슷한 느낌의 코드 진행입니다.

      @PersistenceContext
      private final EntityManager entityManager;
    
      ...생략
    
      public List<PostDto> getPosts() {
        String jqpl = "SELECT p FROM Post p JOIN FETCH comments";
        List<Post> posts = entityManager.createQuery(jqpl, Post.class).getResultList(); // 기존 findAll을 대체
    
        List<PostDto> postDtos = new ArrayList<>();
        for (int i = 0; i < posts.size(); i++) {
          Post post = posts.get(i);
          List<Comment> comments = post.getComments();
    
          List<CommentDto> commentDtos = new ArrayList<>();
          for (Comment comment: comments) {
            commentDtos.add(CommentDto.of(comment, post.getPostId()));
          }
    
          postDtos.add(PostDto.of(post, commentDtos));
        }
    
        return postDtos;
      }

     

    마찬가지로 join을 통해서 한번에 데이터를 로딩하고 있습니다.

     

     


     

     

    JPQL을 사용하니 상당히 간단하게 N + 1 이슈가 해결되었습니다.

    기본적인 접근은 N + 1은 JPA가 연관관계가 있는 데이터를 로딩할 때 최적화를 위해 2단계의 쿼리로 나눠서 데이터를 가져오기 때문에 발생하는 것이고, 이것을 해결하기 위해서 1번의 쿼리로 데이터를 로딩하도록 강제하는 방향으로 진행됩니다.

     

    이 글에서 다룬 내용은 페이징 처리를 하지 않기 때문에 간단하게 해결이 됩니다.

    하지만 페이징이 추가 된다면 복잡도가 올라가기 때문에, 페이징 처리가 되는 케이스는 뒤에서 다시 다루어보겠습니다.

     

    감사합니다.

     

Designed by Tistory.