[JPA]/JPA

[JPA] N+1 문제 (즉시 로딩 / 지연 로딩 / 일반 Join / Fetch Join)

쿠릉쿠릉 쾅쾅 2022. 5. 2. 00:42
728x90

 

즉시 로딩 / 지연 로딩 / 일반 Join / Fetch Join

1. select / EAGER (즉시 로딩)

📌 Member

package prac.littleprac.domain;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "team_id")
    private Team team;


    // == 연관관계 메서드 ==//
    public void changeTeam(Team team) {
        team.getMembers().add(this);
        this.team = team;
    }

}

📌 Team

package prac.littleprac.domain;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Builder
    private Team(String name, List<Member> members) {
        this.name = name;
        this.members = members;
    }

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

📌 Test 코드

@Test
void 즉시로딩() {
    Team teamA = Team.builder()
            .name("teamA")
            .build();

    em.persist(teamA);

    Team teamB = Team.builder()
            .name("teamB")
            .build();

    em.persist(teamB);

    for(int i=0; i<3; i++) {
        Member member = Member.builder()
                .team(i%2==0? teamA : teamB)
                .name("member" + i)
                .build();
        em.persist(member);
    }

    em.flush();
    em.clear();

    String basicQuery = "select m from Member m";

    List<Member> result = em.createQuery(basicQuery, Member.class)
            .getResultList();
}
    select
        member0_.id as id1_2_,
        member0_.name as name2_2_,
        member0_.team_id as team_id3_2_ 
    from
        member member0_
        
        
    select
        team0_.id as id1_4_0_,
        team0_.name as name2_4_0_ 
    from
        team team0_ 
    where
        team0_.id=?
        
        
    select
        team0_.id as id1_4_0_,
        team0_.name as name2_4_0_ 
    from
        team team0_ 
    where
        team0_.id=?

Member를 즉시로딩 설정시 Member 조회 쿼리 1개와 연관관계인 Team 조회 쿼리 2개가 발생했다. (N+1 문제 발생)

 

2. select / LAZY (지연 로딩)

📌 Member

package prac.littleprac.domain;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    // == 연관관계 메서드 ==//
    public void changeTeam(Team team) {
        team.getMembers().add(this);
        this.team = team;
    }
}

📌 Team

package prac.littleprac.domain;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Builder
    private Team(String name, List<Member> members) {
        this.name = name;
        this.members = members;
    }

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

📌 Test 코드

@Test
void 지연로딩() {
    Team teamA = Team.builder()
            .name("teamA")
            .build();

    em.persist(teamA);

    Team teamB = Team.builder()
            .name("teamB")
            .build();

    em.persist(teamB);

    for(int i=0; i<3; i++) {
        Member member = Member.builder()
                .team(i%2==0? teamA : teamB)
                .name("member" + i)
                .build();
        em.persist(member);
    }

    em.flush();
    em.clear();

    String basicQuery = "select m from Member m";

    List<Member> result = em.createQuery(basicQuery, Member.class)
            .getResultList();
}
    select
        member0_.id as id1_2_,
        member0_.name as name2_2_,
        member0_.team_id as team_id3_2_ 
    from
        member member0_

지연 로딩 설정시 Member 조회시 Team 프록시 객체로 조회하기 때문에 Team 조회 쿼리가 따로 생성되지 않는다.
하지만 Member엔티티에 Team 엔티티를 조회할 떄 Team 조회 쿼리가 생성된다. 이 때 N+1 문제가 발생한다.

📌 Test 코드

@Test
void 지연로딩() {
    Team teamA = Team.builder()
            .name("teamA")
            .build();

    em.persist(teamA);

    Team teamB = Team.builder()
            .name("teamB")
            .build();

    em.persist(teamB);

    for(int i=0; i<3; i++) {
        Member member = Member.builder()
                .team(i%2==0? teamA : teamB)
                .name("member" + i)
                .build();
        em.persist(member);
    }

    em.flush();
    em.clear();

    String basicQuery = "select m from Member m";

    List<Member> result = em.createQuery(basicQuery, Member.class)
            .getResultList();

    for (Member member : result) {
        System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
    }
}
    select
        member0_.id as id1_2_,
        member0_.name as name2_2_,
        member0_.team_id as team_id3_2_ 
    from
        member member0_
        
        
    select
        team0_.id as id1_4_0_,
        team0_.name as name2_4_0_ 
    from
        team team0_ 
    where
        team0_.id=?
        
        
    select
        team0_.id as id1_4_0_,
        team0_.name as name2_4_0_ 
    from
        team team0_ 
    where
        team0_.id=?

Member 엔티티에서 Team 엔티티를 조회할 때 Team 조회 쿼리가 생성된다. (N+1 문제 발생)

 

3. 일반 Join (지연 로딩)

📌 Member

package prac.littleprac.domain;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    // == 연관관계 메서드 ==//
    public void changeTeam(Team team) {
        team.getMembers().add(this);
        this.team = team;
    }
}

📌 Team

package prac.littleprac.domain;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Builder
    private Team(String name, List<Member> members) {
        this.name = name;
        this.members = members;
    }

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

📌 테스트 코드

@Test
void 일반조인() {
    Team teamA = Team.builder()
            .name("teamA")
            .build();

    em.persist(teamA);

    Team teamB = Team.builder()
            .name("teamB")
            .build();

    em.persist(teamB);

    for(int i=0; i<3; i++) {
        Member member = Member.builder()
                .team(i%2==0? teamA : teamB)
                .name("member" + i)
                .build();
        em.persist(member);
    }

    em.flush();
    em.clear();

    String basicQuery = "select m from Member m join m.team t";

    List<Member> result = em.createQuery(basicQuery, Member.class)
            .getResultList();
}
    select
        member0_.id as id1_2_,
        member0_.name as name2_2_,
        member0_.team_id as team_id3_2_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.id

Member 엔티티만 호출시점에서는 Member 조회 쿼리만 생성된다.

📌 테스트 코드

@Test
void 일반조인() {
    Team teamA = Team.builder()
            .name("teamA")
            .build();

    em.persist(teamA);

    Team teamB = Team.builder()
            .name("teamB")
            .build();

    em.persist(teamB);

    for(int i=0; i<3; i++) {
        Member member = Member.builder()
                .team(i%2==0? teamA : teamB)
                .name("member" + i)
                .build();
        em.persist(member);
    }

    em.flush();
    em.clear();

    String basicQuery = "select m from Member m join m.team t";

    List<Member> result = em.createQuery(basicQuery, Member.class)
            .getResultList();

    for (Member member : result) {
        System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
    }
}
    select
        member0_.id as id1_2_,
        member0_.name as name2_2_,
        member0_.team_id as team_id3_2_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.id
            
            
    select
        team0_.id as id1_4_0_,
        team0_.name as name2_4_0_ 
    from
        team team0_ 
    where
        team0_.id=?
        
member.getTeam().getName() = teamA
        
        
    select
        team0_.id as id1_4_0_,
        team0_.name as name2_4_0_ 
    from
        team team0_ 
    where
        team0_.id=?
        
member.getTeam().getName() = teamB
member.getTeam().getName() = teamA

Member 엔티티에서 Team 엔티티를 사용 시점에서 Team 조회 쿼리가 생성되어 N+1개 문제가 발생했다.

 

4. Fetch Join (즉시 로딩)

📌 Member

package prac.littleprac.domain;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    // == 연관관계 메서드 ==//
    public void changeTeam(Team team) {
        team.getMembers().add(this);
        this.team = team;
    }
}

📌 Team

package prac.littleprac.domain;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Builder
    private Team(String name, List<Member> members) {
        this.name = name;
        this.members = members;
    }

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

📌 테스트 코드

@Test
void 페치조인() {
    Team teamA = Team.builder()
            .name("teamA")
            .build();

    em.persist(teamA);

    Team teamB = Team.builder()
            .name("teamB")
            .build();

    em.persist(teamB);

    for(int i=0; i<3; i++) {
        Member member = Member.builder()
                .team(i%2==0? teamA : teamB)
                .name("member" + i)
                .build();
        em.persist(member);
    }

    em.flush();
    em.clear();

    String basicQuery = "select m from Member m join fetch m.team t";

    List<Member> result = em.createQuery(basicQuery, Member.class)
            .getResultList();

    for (Member member : result) {
        System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
    }
}
    select
        member0_.id as id1_2_0_,
        team1_.id as id1_4_1_,
        member0_.name as name2_2_0_,
        member0_.team_id as team_id3_2_0_,
        team1_.name as name2_4_1_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_id=team1_.id
            
member.getTeam().getName() = teamA
member.getTeam().getName() = teamB
member.getTeam().getName() = teamA

페치 조인은 연관관계를 즉시 로딩처럼 쿼리 한 번에 다 끌어오기 때문에 N+1 문제를 해결할 수 있다.

 

✔ 정리

 


👀 참고 자료

https://www.inflearn.com/questions/30446

 

제가 이해한 것이 정확한지 궁금합니다. - 인프런 | 질문 & 답변

1. 이렇게 표로 정리된 내용이 맞나요? [사진] - 질문 & 답변 | 인프런...

www.inflearn.com

 

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

 

JPA 모든 N+1 발생 케이스과 해결책

N+1이 발생하는 모든 케이스 (즉시로딩, 지연로딩)에서의 해결책과 그 해결책에서의 문제를 해결하는 방법에 대해 이야기 하려합니다 😀

velog.io

 

728x90