본문 바로가기
Java/JPA

[JPA_Basic] N+1 문제

by HJ0216 2023. 9. 10.

이 글은 김영한의 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 수강하며 정리한 글입니다.

 

👉 기본 환경

- Language: Java

- DB: H2 Database

- IDE: IntelliJ

 

 

⭐ N + 1 문제

: 연관관계가 설정된 엔티티 사이에서 하나의 엔티티를 조회했을 때, 조회된 엔티티의 개수(N 개)만큼 연관된 엔티티를 조회하기 위해 추가적인 쿼리가 발생하는 문제

    - 1: 하나의 엔티티를 조회하기 위한 쿼리의 개수

    - N: 연관된 데이터를 조회하기 위한 추가적인 쿼리의 개수

 

 

⌨️ 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
public class Member extends BaseEntity {
    
    // 생략
 
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
 
    @Column(name = "USERNAME")
    private String name;
 
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 
    // 생략
 
}
 
 
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
public class Main {
    public static void main(String[] args) {
 
        // 생략
 
        try {
 
            Team teamA = new Team();
            teamA.setName("teamA");
            em.persist(teamA);
 
            Team teamB = new Team();
            teamB.setName("teamB");
            em.persist(teamB);
 
            Member member1 = new Member();
            member1.setName("user1");
            member1.setTeam(teamA);
            em.persist(member1);
 
            Member member2 = new Member();
            member2.setName("user2");
            member2.setTeam(teamB);
            em.persist(member2);
 
            em.flush();
            em.clear();
 
            List<Member> members = em.createQuery("select m from Member m", Member.class)
                    .getResultList();
 
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
 
        emf.close();
 
    }
 
}
 
 

 

 

🖨️발생한 쿼리

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
Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_3_,
            member0_.INSERT_MEMBER as insert_m2_3_,
            member0_.createDate as createda3_3_,
            member0_.UPDATE_MEMBER as update_m4_3_,
            member0_.lastModifiedDate as lastmodi5_3_,
            member0_.USERNAME as username6_3_,
            member0_.TEAM_ID as team_id7_3_ 
        from
            Member member0_
 
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_5_0_,
        team0_.INSERT_MEMBER as insert_m2_5_0_,
        team0_.createDate as createda3_5_0_,
        team0_.UPDATE_MEMBER as update_m4_5_0_,
        team0_.lastModifiedDate as lastmodi5_5_0_,
        team0_.name as name6_5_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
 
Hibernate: 
    select
        team0_.TEAM_ID as team_id1_5_0_,
        team0_.INSERT_MEMBER as insert_m2_5_0_,
        team0_.createDate as createda3_5_0_,
        team0_.UPDATE_MEMBER as update_m4_5_0_,
        team0_.lastModifiedDate as lastmodi5_5_0_,
        team0_.name as name6_5_0_ 
    from
        Team team0_ 
    where
        team0_.TEAM_ID=?
 
 

 

 

📡 원인

1. Member 객체를 조회하기 위한 쿼리 발생

2. FetchType.EAGER의 경우, 필드값이 채워져 있어야하므로,

    - TeamA를 가져오기 위한 Query1

    - TeamB를 가져오기 위한 Query2

 

 

📰 해결 방법

1. FetchType.LAZY 선언

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
public class Member extends BaseEntity {
    
    // 생략
 
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
 
    @Column(name = "USERNAME")
    private String name;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 
    // 생략
 
}
 
 

🖨️발생한 쿼리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.MEMBER_ID as member_i1_3_,
            member0_.INSERT_MEMBER as insert_m2_3_,
            member0_.createDate as createda3_3_,
            member0_.UPDATE_MEMBER as update_m4_3_,
            member0_.lastModifiedDate as lastmodi5_3_,
            member0_.USERNAME as username6_3_,
            member0_.TEAM_ID as team_id7_3_ 
        from
            Member member0_
 
 

 

2. fetch join 활용

* fetch join: 연관된 엔티티나 컬렉션을 함께 로딩하여 성능을 최적화하는 기능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
public class Member extends BaseEntity {
    
    // 생략
 
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
 
    @Column(name = "USERNAME")
    private String name;
 
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
 
    // 생략
 
}
 
 

🖨️발생한 쿼리

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
Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.MEMBER_ID as member_i1_3_0_,
            team1_.TEAM_ID as team_id1_5_1_,
            member0_.INSERT_MEMBER as insert_m2_3_0_,
            member0_.createDate as createda3_3_0_,
            member0_.UPDATE_MEMBER as update_m4_3_0_,
            member0_.lastModifiedDate as lastmodi5_3_0_,
            member0_.USERNAME as username6_3_0_,
            member0_.TEAM_ID as team_id7_3_0_,
            team1_.INSERT_MEMBER as insert_m2_5_1_,
            team1_.createDate as createda3_5_1_,
            team1_.UPDATE_MEMBER as update_m4_5_1_,
            team1_.lastModifiedDate as lastmodi5_5_1_,
            team1_.name as name6_5_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.TEAM_ID
 
 

 

 

cf. @ManyToOne, @OneToOne: default - FetchType.EAGER

▶ FetchType.LAZY 필수 기재

 

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

[JPA_Basic] 값 타입과 불변 객체  (0) 2023.09.14
[JPA_Basic] Cascade  (0) 2023.09.11
[JPA_Basic] Proxy  (0) 2023.09.08
[JPA_Basic] @MappedSuperclass  (0) 2023.09.05
[JPA_Basic] 상속관계 매핑  (0) 2023.08.23