Home [jpa] n+1 문제 해결
Post
Cancel

[jpa] n+1 문제 해결

이전에 프로젝트를 진행하면서 발생했던 문제를 정리하기 위해 작성한다. 팀 프로젝트에서 QnA 게시판을 맡아서 개발하게되었다. QnA 게시판은 게시글을 작성할 때 해시태그를 함께 작성 할 수있고 게시글에 댓글을 작성할 수 있다. 각각의 테이블의 관계를 살펴보면

Untitled


하나의 게시글은 여러개의 댓글을 가질 수 있어 1:N 관계를 가진다.

Question.class

1
2
3
4
5
6
7
8
9
10
11
public class Question {
	
	// 나머지 컬럼 생략
		
	@OneToMany(mappedBy = "question" ,cascade=CascadeType.ALL , orphanRemoval = true)
	private List<QuestionHashTag> tagList = new ArrayList<>();
	
	@OneToMany(mappedBy = "question",cascade=CascadeType.ALL , orphanRemoval = true)
	private List<Comment> commentList;

}


Comment.class

1
2
3
4
5
6
7
8
9
public class Comment {

// .. 생략

  @ManyToOne
  @JoinColumn(name = "QUESTION_NO", nullable = false)
  private Question question;

}



Untitled

하나의 게시글은 여러개의 해시태그를 가질수 있고 하나의 해시태그는 여러개의 게시글에 사용될 수 있기 때문에 해시태그와 게시글은 다대다 관계를 가진다.


Question.class

1
2
3
4
5
6
7
8
9
10
11
public class Question {
	
	// 나머지 컬럼 생략
		
	@OneToMany(mappedBy = "question" ,cascade=CascadeType.ALL , orphanRemoval = true)
	private List<QuestionHashTag> tagList = new ArrayList<>();
	
	@OneToMany(mappedBy = "question",cascade=CascadeType.ALL , orphanRemoval = true)
	private List<Comment> commentList;

}


QuestionHashTag.class

1
2
3
4
5
6
7
8
9
10
11
12
public class QuestionHashTag {

//.. 생략

  @ManyToOne
  @JoinColumn(name = "question_no")
  private Question question;

  @ManyToOne
  @JoinColumn(name = "hashtag_no")
  private HashTag hashTag;
}


HashTag.class

1
2
3
4
5
6
7
8
public class HashTag {

//.. 생략

  @OneToMany(mappedBy = "hashTag" ,cascade=CascadeType.ALL , orphanRemoval = true)
  private List<QuestionHashTag> questions;

}


@ManyToMany 를 사용해 관계를 맺을 수 있지만 추후에 데이터(예 해시태그가 등록된 시간) 추가를 할 수 없다는 단점이 있어 중간 테이블을 생성해 @OneToMany@ManyToOne으로 관계를 풀어냈다.



문제 발생

게시글 전체 조회 기능을 개발할때 문제가 발생했다. 게시글 전체 조회할때 필요한 정보들은 게시글의 정보(제목, 내용, 작성자, 조회수, 작성날짜 등)와 댓글의 갯수 그리고 해시태그의 이름이 필요하다.

Untitled

게시글 조회기능을 개발하고 hibernate의 show_sql 속성을 사용해 JPA가 만들어주는 SQL문을 확인해보니 게시글 조회시 생각보다 너무 많은 쿼리를 생성하는것을 발견했다.

내 생각으로는 question테이블을 조회할 때 comment와 question_hash_tag를 조인해 하나의 쿼리를 날릴줄 알았다.

한번 쿼리를 분석해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Hibernate: 
    /* <criteria> */ select
        q1_0.question_no,
        q1_0.comment_count,
        q1_0.enroll_date,
        q1_0.member_no,
        q1_0.question_content,
        q1_0.question_title,
        q1_0.view_count,
        q1_0.writer 
    from
        question q1_0 
    order by
        q1_0.enroll_date desc 
    limit
        ?, ?

첫번째 쿼리는 최신 등록일 순서로 정렬하여 페이징처리된 결과를 가져온다.


1
2
3
4
5
Hibernate: 
  /* <criteria> */ select
      count(q1_0.question_no) 
  from
      question q1_0

두번째 쿼리는 페이징 처리에 필요한 게시글의 개수를 구하는 쿼리이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Hibernate: 
    select
        cl1_0.question_no,
        cl1_0.comment_no,
        cl1_0.comment_content,
        cl1_0.enroll_date,
        cl1_0.member_no,
        m1_0.member_no,
        m1_0.member_address,
        m1_0.member_birth,
        m1_0.member_email,
        m1_0.member_gender,
        m1_0.member_id,
        m1_0.member_name,
        m1_0.member_nickname,
        m1_0.member_phone,
        m1_0.member_profile,
        m1_0.member_pwd,
        st1_0.skin_no,
        st1_0.skin_name 
    from
        comment cl1_0 
    left join
        member m1_0 
            on m1_0.member_no=cl1_0.member_no 
    left join
        skin_type st1_0 
            on st1_0.skin_no=m1_0.skin_id 
    where
        cl1_0.question_no=?

세번째 쿼리부터 문제가 발생한다. 이 쿼리는 특정 게시글에 대한 모든 댓글과 각 댓글을 작성한 회원의 정보를 함께 조회한다. 내가 필요한 정보는 특정 게시글에 달린 댓글의 갯수만 필요한것인데 불필요한 정보까지 조회한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
Hibernate: 
    select
        tl1_0.question_no,
        tl1_0.question_hashtag_no,
        ht1_0.hashtag_no,
        ht1_0.hashtag_name 
    from
        question_hash_tag tl1_0 
    left join
        hash_tag ht1_0 
            on ht1_0.hashtag_no=tl1_0.hashtag_no 
    where
        tl1_0.question_no=?

네번째 쿼리는 특정 게시글에 대한 모든 해시태그 정보를 가져온다. 이후의 쿼리는 10개의 게시글에 대한 댓글 쿼리와 해시태그 쿼리가 추가로 발생한다.

한번 게시글을 조회할때 (게시글 조회 쿼리 X 댓글 조회 쿼리 X 해시태그 조회 쿼리) 가 발생한다. 게시글을 전체 조회할 때 마다 한 페이지에 보이는 모든 게시글과 연관된 댓글과 해시태그를 모두 조회한다. 이렇게 1번 조회해야할 것을 N개 종류의 데이터를 각각을 추가로 조회하는 문제를 N+1문제 라고 한다.



해결 방법

Question과 연관된 QuestionHashTag와 Comment는 모두 @OneToMany로 연관관계가 맺어져 있다.

1
2
3
4
5
6
7
8
9
10
11
12
public class Question {
	
	// 나머지 컬럼 생략
	
	
	@OneToMany(mappedBy = "question" ,cascade=CascadeType.ALL , orphanRemoval = true)
	private List<QuestionHashTag> tagList = new ArrayList<>();
	
	@OneToMany(mappedBy = "question",cascade=CascadeType.ALL , orphanRemoval = true)
	private List<Comment> commentList;

}

@OneToMany는 기본적으로 지연 로딩 방식을 사용한다.(Lazy loading)

지연 로딩 방식은 연관된 엔티티를 실제로 사용할 때까지 데이터베이스에서 가져오지 않고, 필요할 때 가져오도록 하는 방식이다.

따라서 게시글의 대한 댓글과 해시태그들이 필요한 시기에 쿼리를 발생하기 때문에 N+1이 발생했다고 볼 수 있다.

그렇다면 게시글을 조회할때 연관된 데이터를 모조리 조회하는 즉시조회(Eager Loading)으로 변경하면 해결 될까?



즉시조회

1
2
3
4
5
@OneToMany(mappedBy = "question" ,cascade=CascadeType.ALL , orphanRemoval = true, fetch = FetchType.EAGER)
private List<QuestionHashTag> tagList = new ArrayList<>();

@OneToMany(mappedBy = "question",cascade=CascadeType.ALL , orphanRemoval = true, fetch = FetchType.EAGER)
private List<Comment> commentList;

fetch = FetchType.EAGER 를 사용해 즉시로딩으로 바꿔주었다.

실행해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Hibernate: 
    /* <criteria> */ select
        q1_0.question_no,
        q1_0.comment_count,
        q1_0.enroll_date,
        q1_0.member_no,
        q1_0.question_content,
        q1_0.question_title,
        q1_0.view_count,
        q1_0.writer 
    from
        question q1_0 
    order by
        q1_0.enroll_date desc 
    limit
        ?, ?
Hibernate: 
    select
        tl1_0.question_no,
        tl1_0.question_hashtag_no,
        ht1_0.hashtag_no,
        ht1_0.hashtag_name 
    from
        question_hash_tag tl1_0 
    left join
        hash_tag ht1_0 
            on ht1_0.hashtag_no=tl1_0.hashtag_no 
    where
        tl1_0.question_no=?
Hibernate: 
    select
        cl1_0.question_no,
        cl1_0.comment_no,
        cl1_0.comment_content,
        cl1_0.enroll_date,
        cl1_0.member_no,
        m1_0.member_no,
        m1_0.member_address,
        m1_0.member_birth,
        m1_0.member_email,
        m1_0.member_gender,
        m1_0.member_id,
        m1_0.member_name,
        m1_0.member_nickname,
        m1_0.member_phone,
        m1_0.member_profile,
        m1_0.member_pwd,
        st1_0.skin_no,
        st1_0.skin_name 
    from
        comment cl1_0 
    left join
        member m1_0 
            on m1_0.member_no=cl1_0.member_no 
    left join
        skin_type st1_0 
            on st1_0.skin_no=m1_0.skin_id 
    where
        cl1_0.question_no=?
        
// 추가 쿼리 반복

아쉽게도 이전과 똑같이 N+1이 발생한다.

이유는 JPQL에서 발생한다.

1
Page<Question> questionPage = questionRepository.findAll(pageable); // 모든 게시글 조회

JPQL의 findAll()을 사용했는데 내부적으로 select q from Question q; 쿼리를 만들어 사용한다.

즉 Question 조회 쿼리문과 즉시 로딩이 걸려있는 댓글과 해시태그는 따로 쿼리문이 발생한다.

JPQL은 내부에서 join을 하지않고 우선적으로 쿼리를 만들다 보니 연관관계가 걸려있어도 join이 바로 걸리지 않는다.


그렇다면 내가 JPQL에 명시적으로 join을 작성해주면 해결될까?



fetch join

JPQL에서 성능 최적화를 위해 제공하는 조인의 종류이다.


1
2
3
4
❓ 일반 join과 fetch join의 차이
일반 join은 지연 로딩을 사용하여 필요할 때마다 데이터를 로드하고, 
fetch join은 즉시 로딩을 사용하여 한 번에 모든 데이터를 로드한다.
그렇기 때문에 일반 join을 사용하면 N+1이 발생할 수 있다.


1
2
3
4
5
@Query("SELECT DISTINCT q FROM Question q " +
       "LEFT JOIN FETCH q.questionHashTags qt" +
       "LEFT JOIN FETCH pt.hashTag" +
       "LEFT JOIN FETCH q.comments")
Page<Question> findAllQuestionsWithTagsAndComments(Pageable pageable);

Join 뒤에 Fetch 키워드를 붙여 사용한다. 그리고 중복된 결과를 제거하기 위해 DISTINCT 를 붙여줬다.



@EntityGraph를 사용해서 fetch join을 할 수 있다.

1
2
@EntityGraph(attributePaths = {"questionHashTags.hashTag","comments"})
Page<Question> findAllQuestionsWithTagsAndComments(Pageable pageable);

@EntityGraph의 attributePaths에 쿼리 수행시 바로 가져올 필드명을 지정하면 Lazy가 아닌 Eager 조회로 가져오게 된다. JPQL을 사용하는것보다 작성하는게 간편하다.


한번 실행해 보자

Untitled


이번에는 예외가 발생했다.


1
hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags

MultipleBagFetchException은 Hibernate에서 발생하는 예외로, 쿼리가 동시에 여러개의 bag컬렉션을 가져오려는 시도를 가져오려는 경우 사용되는 예외라고 한다.


1
2
3
💡 bag 컬렉션이란?
중복을 허용하고 순서가 저장되지 않은 컬렉션.
하지만 자바에서는 bag 컬렉션이 없어 List를 사용한다.


더 자세히 말하면 복수의 컬렉션을 fetch join해서 조회하려하면 카테시안 곱에 의해 중복 데이터가 발생할 수 있기 떄문이다. 카테시안 곱은 데이터베이스에서 매우 많은 데이터를 가져오게 되어 성능과 메모리 사용에 부담을 줄 수있기 때문에 Hibernate는 이러한 상황을 방지하기 위해 MultipleBagFetchException을 던진다.


1
2
💡 카테시안 곱이란?
테이블에 대한 모든 데이터를 전부 결합하여 Table에 존재하는 행 갯수를 곱한 만큼의 결과값이 반환되는 것


내 경우에는 questionHastagscomments List 컬렉션 2개를 조회하려다 보니 카테시안 곱이 발생할수 있어 예외를 발생시킨것이다.

1
2
3
4
5
@OneToMany(mappedBy = "question" ,cascade=CascadeType.ALL , orphanRemoval = true)
private List<QuestionHashTag> questionHastags;
    
@OneToMany(mappedBy = "question",cascade=CascadeType.ALL , orphanRemoval = true)
private List<Comment> comments;



MultipleBagFetchException 해결법


Set으로 변경하기

아주 간단한 방법으로는 List를 Set으로 변경하면 MultipleBagFetchException를 방지할 수 있다. Set은 중복을 허용하지 않기 때문에 카테시안 곱을 방지할 수 있다.

한번 변경해보자

1
2
3
4
5
@OneToMany(mappedBy = "question" ,cascade=CascadeType.ALL , orphanRemoval = true)
private Set<QuestionHashTag> questionHastags;
    
@OneToMany(mappedBy = "question",cascade=CascadeType.ALL , orphanRemoval = true)
private Set<Comment> comments;


컬레션들을 Set으로 변경하고 실행해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2024-07-14T14:54:42.477+09:00  WARN 25880 --- [nio-9090-exec-1] org.hibernate.orm.query                  
: HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
Hibernate: 
    /* SELECT
        DISTINCT q 
    FROM
        Question q 
    LEFT JOIN
        
    FETCH
        q.questionHashTags qt 
    LEFT JOIN
        
    FETCH
        qt.hashTag 
    LEFT JOIN
        
    FETCH
        q.comments 
    order by
        q.enrollDate desc */ select
            distinct q1_0.question_no,
            q1_0.comment_count,
            c1_0.question_no,
            c1_0.comment_no,
            c1_0.comment_content,
            c1_0.enroll_date,
            c1_0.member_no,
            q1_0.enroll_date,
            q1_0.member_no,
            q1_0.question_content,
            qht1_0.question_no,
            qht1_0.question_hashtag_no,
            ht1_0.hashtag_no,
            ht1_0.hashtag_name,
            q1_0.question_title,
            q1_0.view_count,
            q1_0.writer 
        from
            question q1_0 
        left join
            question_hash_tag qht1_0 
                on q1_0.question_no=qht1_0.question_no 
        left join
            hash_tag ht1_0 
                on ht1_0.hashtag_no=qht1_0.hashtag_no 
        left join
            comment c1_0 
                on q1_0.question_no=c1_0.question_no 
        order by
            q1_0.enroll_date desc
            
// 추가 쿼리 없음    

예외가 발생하지 않고 join이 잘된것처럼 보인다. 하지만 자세히 보면 페이징 처리를 위한 limit 구문이 보이지 않는다.

결과창에 hibernate가 경고문을 보여주는데

1
2
2024-07-14T14:54:42.477+09:00  WARN 25880 --- [nio-9090-exec-1] org.hibernate.orm.query                  
: HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

컬렉션 fetch를 통한 결과를 메모리에서 페이징처리를 한다는 뜻이다.

다시 말해 데이터베이스에 있는 모든 게시글을 조회하고 메모리에서 페이징 처리를 한다는 뜻이다. 만약 데이터 베이스에 게시글이 100만개 있다고 가정하면 100만개의 게시글을 메모리로 불러와 페이징 처리를 할텐데 그렇다면 OOM이 발생할 것 이다. 이를 hibernate에서 경고해주는것이다.

pagination을 사용할 경우에는 fetch join을 함께 사용하지 못한다. 결국 구글링을 통해 @BatchSize 라는 걸 알아냈다.



BatchSize

BatchSize는 JPA에서 연관된 엔티티를 한 번에 배치로 로딩하여 데이터베이스에 대한 쿼리 수를 줄이는 데 사용되는 것으로 여러 개의 SELECT 쿼리를 단일 IN 쿼리로 변환하여 성능을 최적화한다.

@BatchSize를 설정하면 설정한 크기만큼의 엔티티를 한 번에 로드하여 데이터베이스 쿼리 수를 줄일 수 있다.

1
2
3
4
5
6
7
@BatchSize(size = 10)
@OneToMany(mappedBy = "question" ,cascade=CascadeType.ALL , orphanRemoval = true)
private List<QuestionHashTag> questionHashTags;

@BatchSize(size = 10)
@OneToMany(mappedBy = "question",cascade=CascadeType.ALL , orphanRemoval = true)
private List<Comment> comments;

한 페이지에 게시글의 갯수는 10개이므로 size를 10으로 설정해 주었다.


이제 실행해보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Hibernate: 
    /* <criteria> */ select
        q1_0.question_no,
        q1_0.comment_count,
        q1_0.enroll_date,
        q1_0.member_no,
        q1_0.question_content,
        q1_0.question_title,
        q1_0.view_count,
        q1_0.writer 
    from
        question q1_0 
    order by
        q1_0.enroll_date desc 
    limit
        ?, ?
Hibernate: 
    /* <criteria> */ select
        count(q1_0.question_no) 
    from
        question q1_0
Hibernate: 
    select
        qht1_0.question_no,
        qht1_0.question_hashtag_no,
        ht1_0.hashtag_no,
        ht1_0.hashtag_name 
    from
        question_hash_tag qht1_0 
    left join
        hash_tag ht1_0 
            on ht1_0.hashtag_no=qht1_0.hashtag_no 
    where
        qht1_0.question_no in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        c1_0.question_no,
        c1_0.comment_no,
        c1_0.comment_content,
        c1_0.enroll_date,
        c1_0.member_no,
        m1_0.member_no,
        m1_0.member_address,
        m1_0.member_birth,
        m1_0.member_email,
        m1_0.member_gender,
        m1_0.member_id,
        m1_0.member_name,
        m1_0.member_nickname,
        m1_0.member_phone,
        m1_0.member_profile,
        m1_0.member_pwd,
        st1_0.skin_no,
        st1_0.skin_name 
    from
        comment c1_0 
    left join
        member m1_0 
            on m1_0.member_no=c1_0.member_no 
    left join
        skin_type st1_0 
            on st1_0.skin_no=m1_0.skin_id 
    where
        c1_0.question_no in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

// 추가 쿼리 없음

정상적으로 페이징 처리가 되었고 해시태그와 댓글에 in 을 사용해 한번에 필요한 데이터를 가져온다. 무엇보다 추가 쿼리가 없어졌다.

이렇게 다양한 N+1을 해결하는 다양한 방법을 찾아봤다. 만약 페이징 처리가 필요없는 작업이었다면 Fetch join으로 해결이 가능할 것이다.

이렇게 페이징 처리와 컬렉션을 2개이상 사용한다면 BatchSize를 사용해 한번에 필요한 데이터를 가져오는 방법을 사용하면 N+1을 해결할 수 있다.



추가로 comment를 조회하는 쿼리를 보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Hibernate: 
    select
        c1_0.question_no,
        c1_0.comment_no,
        c1_0.comment_content,
        c1_0.enroll_date,
        c1_0.member_no,
        m1_0.member_no,
        m1_0.member_address,
        m1_0.member_birth,
        m1_0.member_email,
        m1_0.member_gender,
        m1_0.member_id,
        m1_0.member_name,
        m1_0.member_nickname,
        m1_0.member_phone,
        m1_0.member_profile,
        m1_0.member_pwd,
        st1_0.skin_no,
        st1_0.skin_name 
    from
        comment c1_0 
    left join
        member m1_0 
            on m1_0.member_no=c1_0.member_no 
    left join
        skin_type st1_0 
            on st1_0.skin_no=m1_0.skin_id 
    where
        c1_0.question_no in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

나는 게시글의 대한 댓글 갯수만 필요한것 뿐인데 필요하지도 않은 데이터를 가져온다.


댓글의 갯수를 매핑 컬렉션의 size() 로 구했기 때문에 JPA가 comment의 엔티티에접근해 필요없는 데이터까지 모조리 가져온것을 발견했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// service단

int commentCount = question.getComments().size(); // 매핑 컬렉션에 접근

return QuestionResponse.builder()
        .questionNo(question.getQuestionNo())
        .questionTitle(question.getQuestionTitle())
        .questionContent(question.getQuestionContent())
        .viewCount(question.getViewCount())
        .writer(question.getWriter())
        .enrollDate(question.getEnrollDate())
        .tagList(question.getQuestionHashTags())
        .commentCount(commentCount) 
        .build();
}

그래서 매핑 컬렉션에 접근하는 방법 말고 서브쿼리로 댓글의 갯수만 가져올수 있는 방법을 생각해봤다.

1
2
3
4
5
6
//@BatchSize(size = 10)
@OneToMany(mappedBy = "question",cascade=CascadeType.ALL , orphanRemoval =true)
private List<Comment> comments;

@Formula("(select count(*) from comment c where c.question_no = QUESTION_NO)")
private int commentCount;

@Formula 어노테이션을 사용해 게시글 조회시 서브쿼리를 사용할 수 있게 해줬다.


서비스단 코드도 수정해줬다.

1
2
3
4
5
6
7
8
9
10
11
12
13
//int commentCount = question.getComments().size();

return QuestionResponse.builder()
        .questionNo(question.getQuestionNo())
        .questionTitle(question.getQuestionTitle())
        .questionContent(question.getQuestionContent())
        .viewCount(question.getViewCount())
        .writer(question.getWriter())
        .enrollDate(question.getEnrollDate())
        .tagList(question.getQuestionHashTags())
        .commentCount(question.getCommentCount()) // 서브쿼리를 통해 댓글수 가져오기
        .build();
}


이제 다시 게시글 조회를 해보면

Hibernate: 
    /* <criteria> */ select
        q1_0.question_no,
        (select --서브쿼리로 댓글갯수 가져오기
            count(*) 
        from
            comment c 
        where
            c.question_no = q1_0.QUESTION_NO),
        q1_0.enroll_date,
        q1_0.member_no,
        q1_0.question_content,
        q1_0.question_title,
        q1_0.view_count,
        q1_0.writer 
    from
        question q1_0 
    order by
        q1_0.enroll_date desc 
    limit
        ?, ?
Hibernate: 
    /* <criteria> */ select
        count(q1_0.question_no) 
    from
        question q1_0
Hibernate: 
    select
        qht1_0.question_no,
        qht1_0.question_hashtag_no,
        ht1_0.hashtag_no,
        ht1_0.hashtag_name 
    from
        question_hash_tag qht1_0 
    left join
        hash_tag ht1_0 
            on ht1_0.hashtag_no=qht1_0.hashtag_no 
    where
        qht1_0.question_no in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        
// 추가 쿼리 없음       

조회시 발생한 쿼리가 확연하게 줄어든게 보인다. 게시글에 달린 댓글의 갯수는 @Fomula서브쿼리를 통해 조회했고 해시태그는 @BatchSize를 사용해서 한번에 조회해왔다.


마지막으로 포스트맨을 사용해서 요청 1000 개를 날려 개선 전과 후를 비교해보겠다.

결과


개선 전

Untitled

개선 후

Untitled

1000개를 모두 날리는데 걸리는 시간은 비슷하지만 한 개의 요청을 처리하는데에 걸리는속도는 2배 정도 차이가 난다.


서브쿼리를 사용하면 성능상 안좋다는걸 들었다.이 후에는 서브쿼리를 사용하지 않고 다른 방법으로 count를 구하는 방법을 고민해보겠다.






Reference

https://jojoldu.tistory.com/457 https://www.inflearn.com/course/lecture?courseSlug=ORM-JPA-Basic&unitId=21743 https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85#%EC%A6%89%EC%8B%9C%EB%A1%9C%EB%94%A9

This post is licensed under CC BY 4.0 by the author.

[java] try with resource

[test] 동시성 테스트에 관하여