앞서 살펴본 N + 1 문제 원인은 @oneToMany 관계로 reference 하고 있는 collection 내용을 가져오려고 접근할 때 생깁니다. 변수에 접근할 때마다 그때그때 하나씩 쿼리가 날아가는 문제입니다.

그럼 이 문제를 해결하기 위해 어떻게 하는 것이 좋을까요?

  • 먼저 생각해 볼 수 있는 방법은 @EntityGraph로 한 번에 다 가져올 수 있습니다.
  • join fetch을 사용하여 한 번에 쿼리할 수 있습니다.
  • hibernate의 default fetch를 사용하는 방법이 있습니다.

1. @EntityGraph 사용하는 방법

그냥 @oneToMany를 똭 eager로 바꿔 버리면 다시는 lazy로 가져올 수는 없겠죠?^^; 그래서 필요할 때만 eager로 가져오고 평소에는 lazy로 가져오는 방법이 있습니다. 바로 EntityGraph입니다.

@Getter@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@ToString
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Comment> comments = new ArrayList<>();

}

@Setter@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@ToString(exclude = "post")
public class Comment {

    @Id
    @GeneratedValue
    private Long id;

    private String comment;

    @ManyToOne
    private Post post;
}

public interface PostRepository extends JpaRepository<Post, Long> {

    @EntityGraph(attributePaths = {"comments"})
    List<Post> findAllWithComments();
}

기존 소스에 findAllWithComments()를 추가하고 @EntityGraph와 가져올 대상(collection 변수명)을 지정했습니다.

public interface PostRepository extends JpaRepository<Post, Long> {

    @EntityGraph(attributePaths = {"comments"})
    @Query("select p from Post p")
    List<Post> findAllWithComments();
}

@Service
public class PostService {

    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    @Transactional(readOnly = true)
    public List<String> findAllComments() {
        return getAllComments(postRepository.findAll());
    }

    @Transactional(readOnly = true)
    public List<String> findAllWithComments() {
        return getAllComments(postRepository.findAllWithComments());
    }

    /**
     * Lazy Load를 실행하기 위해 모든 comment 순회
     */
    private List<String> getAllComments(List<Post> posts) {
        return posts.stream()
                .map(a -> a.getComments().toString())
                .collect(Collectors.toList());
    }
}

@SpringBootTest
class PostRepositoryTest {

    @Autowired
    private PostService postService;
    @Autowired
    private PostRepository postRepository;

    @BeforeEach
    public void setup() {
        Post post = new Post();
        post.setTitle("첫 포스트");

        Comment comment1 = new Comment();
        comment1.setComment("첫 댓글~! ");
        comment1.setPost(post);
        post.getComments().add(comment1);

        Comment comment2 = new Comment();
        comment2.setComment("두번째야~");
        post.getComments().add(comment2);
        comment2.setPost(post);

        Post post2 = new Post();
        post2.setTitle("내가 2등~");

        Comment comment3 = new Comment();
        comment3.setComment("좋아요~");
        comment3.setPost(post2);
        post2.getComments().add(comment3);

        Comment comment4 = new Comment();
        comment4.setComment("감사합니다.");
        comment4.setPost(post2);
        post2.getComments().add(comment4);

        Comment comment5 = new Comment();
        comment5.setComment("다음글 기대할께요.");
        comment5.setPost(post2);
        post2.getComments().add(comment5);

        postRepository.save(post);
        postRepository.save(post2);
    }

    @Test
    void testNPlusOne() {
        // lazy loading
        System.out.println("===== LAZY");
        System.out.println("LAZY : " + postService.findAllComments());

        // EntityGraph
        System.out.println("===== EntityGraph");
        System.out.println("EntityGraph : " + postService.findAllWithComments());
    }
}

결과는 left outer join으로 실행되었습니다. 이 부분은 다음 편에서 비교할 부분이라 체크해 두세요.

===== LAZY
Hibernate: 
    select
        post0_.id as id1_3_,
        post0_.title as title2_3_ 
    from
        post post0_
2020-06-11 02:01:04.011 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_] : [BIGINT]) - [1]
2020-06-11 02:01:04.015 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_] : [VARCHAR]) - [첫 포스트]
2020-06-11 02:01:04.015 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_] : [BIGINT]) - [4]
2020-06-11 02:01:04.016 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_] : [VARCHAR]) - [내가 2등~]
2020-06-11 02:01:04.021 TRACE 31191 --- [    Test worker] org.hibernate.type.CollectionType        : Created collection wrapper: [com.icatapark.jpa.post.entity.Post.comments#1]
2020-06-11 02:01:04.022 TRACE 31191 --- [    Test worker] org.hibernate.type.CollectionType        : Created collection wrapper: [com.icatapark.jpa.post.entity.Post.comments#4]
Hibernate: 
    select
        comments0_.post_id as post_id3_2_0_,
        comments0_.id as id1_2_0_,
        comments0_.id as id1_2_1_,
        comments0_.comment as comment2_2_1_,
        comments0_.post_id as post_id3_2_1_ 
    from
        comment comments0_ 
    where
        comments0_.post_id=?
2020-06-11 02:01:04.030 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2020-06-11 02:01:04.038 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [2]
2020-06-11 02:01:04.038 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [첫 댓글~! ]
2020-06-11 02:01:04.039 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 02:01:04.040 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [1]
2020-06-11 02:01:04.040 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [2]
2020-06-11 02:01:04.046 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [3]
2020-06-11 02:01:04.046 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [두번째야~]
2020-06-11 02:01:04.047 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 02:01:04.047 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [1]
2020-06-11 02:01:04.047 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [3]
Hibernate: 
    select
        comments0_.post_id as post_id3_2_0_,
        comments0_.id as id1_2_0_,
        comments0_.id as id1_2_1_,
        comments0_.comment as comment2_2_1_,
        comments0_.post_id as post_id3_2_1_ 
    from
        comment comments0_ 
    where
        comments0_.post_id=?
2020-06-11 02:01:04.074 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [4]
2020-06-11 02:01:04.076 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [5]
2020-06-11 02:01:04.076 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [좋아요~]
2020-06-11 02:01:04.077 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.077 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.078 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [5]
2020-06-11 02:01:04.078 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [6]
2020-06-11 02:01:04.079 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [감사합니다.]
2020-06-11 02:01:04.079 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.080 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.080 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [6]
2020-06-11 02:01:04.081 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [7]
2020-06-11 02:01:04.081 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [다음글 기대할께요.]
2020-06-11 02:01:04.082 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.083 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.083 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [7]
LAZY : [[Comment(id=2, comment=첫 댓글~! ), Comment(id=3, comment=두번째야~)], [Comment(id=5, comment=좋아요~), Comment(id=6, comment=감사합니다.), Comment(id=7, comment=다음글 기대할께요.)]]
===== EntityGraph
Hibernate: 
    select
        post0_.id as id1_3_0_,
        comments1_.id as id1_2_1_,
        post0_.title as title2_3_0_,
        comments1_.comment as comment2_2_1_,
        comments1_.post_id as post_id3_2_1_,
        comments1_.post_id as post_id3_2_0__,
        comments1_.id as id1_2_0__ 
    from
        post post0_ 
    left outer join
        comment comments1_ 
            on post0_.id=comments1_.post_id
2020-06-11 02:01:04.130 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [1]
2020-06-11 02:01:04.131 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [2]
2020-06-11 02:01:04.132 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_0_] : [VARCHAR]) - [첫 포스트]
2020-06-11 02:01:04.133 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [첫 댓글~! ]
2020-06-11 02:01:04.134 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 02:01:04.134 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [1]
2020-06-11 02:01:04.135 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [2]
2020-06-11 02:01:04.135 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [1]
2020-06-11 02:01:04.135 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [3]
2020-06-11 02:01:04.136 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [두번째야~]
2020-06-11 02:01:04.137 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [1]
2020-06-11 02:01:04.137 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [1]
2020-06-11 02:01:04.138 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [3]
2020-06-11 02:01:04.138 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.139 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [5]
2020-06-11 02:01:04.140 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([title2_3_0_] : [VARCHAR]) - [내가 2등~]
2020-06-11 02:01:04.140 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [좋아요~]
2020-06-11 02:01:04.141 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.141 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 02:01:04.142 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [5]
2020-06-11 02:01:04.142 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.143 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [6]
2020-06-11 02:01:04.143 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [감사합니다.]
2020-06-11 02:01:04.143 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.144 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 02:01:04.145 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [6]
2020-06-11 02:01:04.145 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_3_0_] : [BIGINT]) - [4]
2020-06-11 02:01:04.146 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [7]
2020-06-11 02:01:04.146 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([comment2_2_1_] : [VARCHAR]) - [다음글 기대할께요.]
2020-06-11 02:01:04.147 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_1_] : [BIGINT]) - [4]
2020-06-11 02:01:04.147 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([post_id3_2_0__] : [BIGINT]) - [4]
2020-06-11 02:01:04.147 TRACE 31191 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0__] : [BIGINT]) - [7]
EntityGraph : [[Comment(id=2, comment=첫 댓글~! ), Comment(id=3, comment=두번째야~)], [Comment(id=5, comment=좋아요~), Comment(id=6, comment=감사합니다.), Comment(id=7, comment=다음글 기대할께요.)]]

그리고 EntityGraph에는 EntityGraphType이 존재하는데요. 아래와 같이 두 가지가 있습니다. 아무런 설정하지 않으면 default는 FETCH로 동작합니다.

  • FETCH: entity graph에 명시한 attribute는 EAGER로 패치하고, 나머지 attribute는 LAZY로 패치
  • LOAD: entity graph에 명시한 attribute는 EAGER로 패치하고, 나머지 attribute는 entity에 명시한 fetch type이나 디폴트 FetchType으로 패치 (e.g. @OneToMany는 LAZY, @ManyToOne은 EAGER 등이 디폴트이다.)

github source : https://github.com/aloftcat/blog-code/tree/master/jpa/NPlusOne

'Programming > JPA' 카테고리의 다른 글

N + 1 문제 해결 3  (0) 2020.06.15
N + 1 문제 해결 2  (0) 2020.06.11
N + 1 문제 원인  (0) 2020.06.05
왜 JPA를 써야할까?  (0) 2020.06.05
JPA 기본 Annotation 정리  (7) 2019.07.04

+ Recent posts