쿠릉쿠릉 쾅쾅
쿠릉쿠릉 쾅쾅
쿠릉쿠릉 쾅쾅
250x250
전체 방문자
오늘
어제
  • 분류 전체보기
    • HTML CSS
    • 잡담
    • 프로그래밍 꿀팁 사이트
    • 코딩 도서
    • [자바]
      • 디자인 패턴
      • 자바의 정석 - 3판
      • 자바
      • 자바 문법
    • git
    • [TDD]
    • 개발 서적 독후감
      • 클린 코더
      • 토비 스프링3
      • 객체지향의 사실과 오해
      • 모던 자바 인 액션
      • 엘레강트 오브젝트
    • CS
      • 운영체제
      • HTTP
    • [SQL]
      • SQL 기초
      • 혼자공부하는SQL
    • [ Spring ]
      • REST API
      • Spring Toy
      • Spring 에러
      • Spring
      • Spring 입문
      • Spring 핵심 원리
      • SpringMVC 1편
      • SpringMVC 2편
      • Spring Boot를 이용한 RESTful We..
      • Batch
    • [JPA]
      • JPA
      • JPA 에러
      • JPA 프로그래밍 - 기본편
      • 스프링 부트와 JPA 활용 1 - 웹 애플리케이..
      • 실전! 스프링 부트와 JPA 활용2 - API 개..
      • 실전! 스프링 데이터 JPA
      • 실전! Querydsl
    • 인텔리제이
    • [DB]
      • DB
      • H2
    • Gradle
    • 면접
    • [알고리즘]
      • 알고리즘
      • 자료구조
      • 자바 알고리즘 공부
    • [프로젝트]
    • 쿠릉식 객체지향 사고
    • 리눅스

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • java
  • 함수형인터페이스
  • 재귀
  • 깃허브
  • springboot
  • querydsl
  • 자바
  • Spring
  • 알고리즘
  • GitHub
  • 백준
  • MVC
  • Git
  • 스프링부트
  • SQL
  • JPA
  • 자료구조
  • http
  • 스프링
  • REST API

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
쿠릉쿠릉 쾅쾅

쿠릉쿠릉 쾅쾅

[JPA] N+1 문제 (즉시 로딩 / 지연 로딩 / 일반 Join / Fetch Join)
[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

'[JPA] > JPA' 카테고리의 다른 글

[JPA] 프록시의 필드값을 조회 때는 get 방식으로 조회하자!  (0) 2022.06.07
[JPA] @DataJpaTest + 테스트 DB 변경  (0) 2022.05.15
[JPA] 엔티티의 필드 컬렉션을 생성과 동시에 초기화 하는 이유  (0) 2022.04.06
    '[JPA]/JPA' 카테고리의 다른 글
    • [JPA] 프록시의 필드값을 조회 때는 get 방식으로 조회하자!
    • [JPA] @DataJpaTest + 테스트 DB 변경
    • [JPA] 엔티티의 필드 컬렉션을 생성과 동시에 초기화 하는 이유
    쿠릉쿠릉 쾅쾅
    쿠릉쿠릉 쾅쾅
    깃허브 주소 : https://github.com/kureung

    티스토리툴바