ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • N + 1 정복하기 <4. @BatchSize>
    프레임워크/Spring Boot 2024. 2. 21. 03:40

     

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

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

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

     

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

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

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

     

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

    이글은 3번 글에서 이어집니다. 3번 글을 보고 오시면 이해에 도움이 될 것 같습니다!

     

     


     

    1. Batch 란?

    BatchSize라는 단어를 보면 batch process가 먼저 떠오릅니다.

    컴퓨터 공학 전반에서 batch 처리라는 개념은 단 건의 행위를 묶음으로 묶어서 처리하며 효율을 증가시키는 방식입니다.

    그렇다면 N + 1에서 발생하는 N개의 처리를 묶음으로 처리할 것으로 예상해볼 수 있을 것 같습니다.

     

     

    2. @BatchSize

    아주 간단하게 @BatchSize 어노테이션을 엔티티에 추가해주면 필요한 작업은 완료됩니다.

    @Getter
    @NoArgsConstructor
    @Entity
    @Table(name = "posts")
    public class Post {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name = "post_id")
      private long postId;
    
      @Column(name = "title")
      private String title;
    
      @Column(name = "content")
      private String content;
    
      @BatchSize(size = 5) // 어노테이션 추가
      @OneToMany(mappedBy = "post")
      private List<Comment> comments = new ArrayList<>();
    
      public void addComment(Comment comment) {
        if (!comments.contains(comment)) {
          comments.add(comment);
          comment.addPost(this);
        }
      }
    }

     

    실행시 발생하는 쿼리를 확인해보겠습니다. N + 1이 발생하던 쿼리와 비교해보았을 때 2가지 눈에 띄는 차이를 발견할 수 있습니다.

    하나는 N + 1번 발생하던 쿼리가 3 + 1번으로 현격하게 줄어들었다는 것입니다.

    다른 하나는 where 절에서 = 으로 비교하던 부분에 IN 절이 추가된 부분입니다.

     

     

    3. BatchSize는 완화 전략

    앞선 글에서 살펴보았던 Fetch Join 같은 경우에는 N + 1 번의 쿼리가 1번의 쿼리로 해결이 되었습니다.

    하지만 BatchSize는 어떤가요? N번 발생하던 쿼리가 상당히 줄어들었지만 여전히 1번의 쿼리로 해결되지는 않습니다.

    정확하게 이야기하면 ⌈N / size⌉ + 1 으로 표현할 수 있겠습니다.

     

    Fetch Join 같은 경우에는 한 번에 모든 데이터를 올리는 장점을 가지고 있지만 Lazy 로딩이 주는 이점은 포기하고 있습니다.

    하지만 BatchSize를 FetchType.LAZY와 함께 사용하면 여전히 Lazy 로딩의 이점을 누릴 수 있습니다.

      public List<PostDto> getPosts() {
        List<Post> posts = postRepository.findAll();
    
        List<PostDto> postDtos = new ArrayList<>();
    //    for (int i = 0; i < posts.size(); i++) {  // comment에 접근하는 부분 주석 처리
    //      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;
      }

     

    @BatchSize를 사용했지만 comment에 접근하는 코드가 없기 때문에, 추가로 comment를 로드하지 않습니다.

     

    N + 1을 해결하는 한 가지 방법으로 @BatchSize를 고려할 수 있겠습니다만, 공식문서에서는 그래도 JOIN FETCH를 사용하는 것이 N + 1을 해결하기 위한 much better alternative 라고 안내하고 있습니다.

    https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching-strategies-fetch-mode-select-mapping-example

     

    4. Size 결정하기

    size라는 값을 개발자가 지정해줘야하기 때문에 적절한 사이즈를 설정하는 것이 중요합니다.

    너무 큰 사이즈는 한 번에 큰 데이터를 메모리에 올리게 되어 jvm 동작에 비효율을 초래하거나 데이터베이스 쿼리 성능에 부하를 줄 수 있습니다. 반대로 너무 작은 사이즈는 네트워크 통신을 증가시킬 수 있다는 점을 고려하면 좋겠습니다.

     

    지금 예제는 11개의 더미 데이터를 사용하고 있기 때문에 size를 5로 한다면 2번의 in 절과 1번의 equal로 나눠 데이터를 가져옵니다.

     

    size를 11로 한다면 1번의 in절로 해결이 가능합니다.

     

     

    5. default_batch_fetch_size

    @BatchSize 어노테이션 이외에도 application.yml에서 default_batch_fetch_size라는 속성을 통해서 전역적으로 BatchSize를 적용하여 사용할 수도 있습니다.

      // application.yml
      jpa:
        show-sql: true
        hibernate:
          ddl-auto: update
        properties:
          hibernate:
            default_batch_fetch_size: 7 # 전역 설정

     

    설정 적용의 우선순위는 @BatchSize > default_batch_fetch_size 순서입니다.

    전역 설정을 하였더라도 @BatchSize를 추가하면 어노테이션의 사이즈를 우선합니다.

     

     


     

    어느 정도 사이즈를 설정하는 것이 좋을지는 경험이 필요한 부분일 것 같습니다.

    이번에는 @BatchSize는 Lazy 로딩의 이점을 가져가면서 N + 1 이슈를 완화할 수 있는 전략이라는 점을 기억하고 가면 좋겠습니다.

     

     

    감사합니다.

     

     

     


    참고

    https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching-strategies-fetch-mode-select-mapping-example

     

     

Designed by Tistory.