kkkkkkkkkkkk
kkkkk
kkkkkkkkkkkk
전체 방문자
오늘
어제
  • 분류 전체보기
    • CS & OS
    • Algorithms
    • Laguage
    • Book
      • 객체지향의 사실과 오해
      • Effective Java
      • Spring boot 와 AWS로 혼자 구현하는 ..
      • 도메인 주도 계발 시작하기
    • DB
    • Spring
    • Spring Boot
    • JPA
    • Git
    • Clean Code
    • HTTP

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 응집도
  • 결합도
  • 책임
  • 역할
  • 설계 원칙
  • 객체지향 프로그래밍

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
kkkkkkkkkkkk

kkkkk

[Spring JPA] N + 1의 문제점이 무엇이고 어떻게 해결 해야할까?
JPA

[Spring JPA] N + 1의 문제점이 무엇이고 어떻게 해결 해야할까?

2022. 5. 15. 14:23

N + 1의 문제점에 대해 알아보기 전에 지연로딩 과 즉시로딩 을 알아야한다.

글 뜻대로 풀어서 생각해보면 지연로딩은 쿼리를 지연시켜서 로딩시킬 것이고 즉시로딩은 쿼리를 즉시 로딩시킬것이라고 생각이 된다.

JPA 에 적용 시켜 즉시로딩과 지연로딩을 알아보자.

Member 엔티티에 @ManyToOne N : 1 의 관계를 맺은 Team 엔티티가 존재한다 가정해보자.

  • Member
@Getter
@NoArgsConstructor
@Entity
@Table(name = "members")
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

// 빌더 생략
}

조회를 할 때 Member 엔티티의 속성만 조회하고 싶다. 테스트를 진행해서 쿼리문을 확인해보자.

@Test
void MemberRepositoryTest() {
    // given
    Team team = Team.builder()
            .name("kkk")
            .build();

    Team saveTeam = teamRepository.save(team);

    Member member = Member.builder()
            .name("kyungbeom")
            .team(saveTeam)
            .build();

    Member saveMember = memberRepository.save(member);

    // when
    List<Member> all = memberRepository.findAll();
}

쿼리 동작

Hibernate: 
    insert 
    into
        teams
        (name, team_id) 
    values
        (?, ?)
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        members
        (name, team_id, member_id) 
    values
        (?, ?, ?)
Hibernate: 
    select
        member0_.member_id as member_i1_1_,
        member0_.name as name2_1_,
        member0_.team_id as team_id3_1_ 
    from
        members member0_
Hibernate: 
    select
        team0_.team_id as team_id1_2_0_,
        team0_.name as name2_2_0_ 
    from
        teams team0_ 
    where
        team0_.team_id=?

Team 데이터와 Member 데이터를 저장하는 insert문이 2방 나가고 Member 속성만 조회하고 싶은데 의도치 않게 Team 엔티티까지 select 문이 나갔다.

왜? Team 엔티티까지 select 문이 나간걸까? @ManyToOne 의 어노테이션을 살펴보자.

 

 

fetch() 속성을 잘보면 기본값이 EAGER 로 설정되어 있다. EAGER 는 즉시 로딩을 뜻하며 연관관계가 맺어진 엔티티의 정보들을 전부 로딩 시켜준다.

EAGER 말고 다른 타입은 없을까? Fetch 타입을 살펴보자.

 

LAZY 가 있다. LAZY 는 지연 로딩 을 뜻하며 연관관계가 맺어진 엔티티의 정보들을 지연 로딩 시켜준다.

fetch 속성을 LAZY 로 설정하여 동작을 해보자.

Hibernate: 
    insert 
    into
        teams
        (name, team_id) 
    values
        (?, ?)
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        members
        (name, team_id, member_id) 
    values
        (?, ?, ?)
Hibernate: 
    select
        member0_.member_id as member_i1_1_,
        member0_.name as name2_1_,
        member0_.team_id as team_id3_1_ 
    from
        members member0_

Member 엔티티 속성만 select 하는 query가 동작하는 것을 볼 수 있다.

위의 fetch 설정으로 쿼리 동작을 살펴볼 수 있었고 즉시로딩의 N + 1의 문제점을 볼 수 있었다.

그렇다면 이 문제점을 해결하기 위해 무엇을 해야할까?

일반 조인과 다르게 패치 조인을 해야한다. 패치 조인 쿼리를 작성하여 동작해보자.

@Query("select m from Member m join fetch m.team")
    List<Member> findAll();

findAll() 메서드의 패치조인을 사용하여 JPQL를 작성한다.

쿼리 동작 결과

Hibernate: 
    select
        member0_.member_id as member_i1_1_0_,
        team1_.team_id as team_id1_2_1_,
        member0_.name as name2_1_0_,
        member0_.team_id as team_id3_1_0_,
        team1_.name as name2_2_1_ 
    from
        members member0_ 
    inner join
        teams team1_ 
            on member0_.team_id=team1_.team_id

일반 조인과 패치 조인의 차이점을 알아보기 위해 일반 조인도 동작해보자.

@Query("select m from Member m join m.team")
    List<Member> findAll();

동작 쿼리 결과

Hibernate: 
    select
        member0_.member_id as member_i1_1_,
        member0_.name as name2_1_,
        member0_.team_id as team_id3_1_ 
    from
        members member0_ 
    inner join
        teams team1_ 
            on member0_.team_id=team1_.team_id
Hibernate: 
    select
        team0_.team_id as team_id1_2_0_,
        team0_.name as name2_2_0_ 
    from
        teams team0_ 
    where
        team0_.team_id=?

Member 테이블과 Team 테이블 조인을 하고 Team 테이블에 select문이 더 나가는 것을 확인할 수 있다.

쿼리의 동작 결과를 보면 조인과 패치 조인의 차이는 조회하고자하는 엔티티의 정보가 사용되는 컬럼이 차이점이 보이고 select문이 한번더 동작되는 차이점을 확인 할 수 있다.

이처럼 일반 조인은 조회 엔티티만 조회 하여 영속화 시키고 패치 조인은 조회 엔티티와 연관관계를 맺은 모든 엔티티 들을 영속화 시킨다는 차이점이라 정리 할 수 있고 패치 조인으로 해서 N + 1 의 문제점을 해결 할 수 있다고 정리할 수 있다.

  • 일반 조인과 패치 조인의 차이점을 잘 설명해주는 블로그 참조
    • https://cobbybb.tistory.com/18

N + 1의 문제점이 무엇이고 어떻게 해결 해야할까?

N + 1의 문제점에 대해 알아보기 전에 지연로딩 과 즉시로딩 을 알아야한다.

글 뜻대로 풀어서 생각해보면 지연로딩은 쿼리를 지연시켜서 로딩시킬 것이고 즉시로딩은 쿼리를 즉시 로딩시킬것이라고 생각이 된다.

JPA 에 적용 시켜 즉시로딩과 지연로딩을 알아보자.

Member 엔티티에 @ManyToOne N : 1 의 관계를 맺은 Team 엔티티가 존재한다 가정해보자.

  • Member
@Getter
@NoArgsConstructor
@Entity
@Table(name = "members")
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

// 빌더 생략
}

조회를 할 때 Member 엔티티의 속성만 조회하고 싶다. 테스트를 진행해서 쿼리문을 확인해보자.

@Test
void MemberRepositoryTest() {
    // given
    Team team = Team.builder()
            .name("kkk")
            .build();

    Team saveTeam = teamRepository.save(team);

    Member member = Member.builder()
            .name("kyungbeom")
            .team(saveTeam)
            .build();

    Member saveMember = memberRepository.save(member);

    // when
    List<Member> all = memberRepository.findAll();
}

쿼리 동작

Hibernate: 
    insert 
    into
        teams
        (name, team_id) 
    values
        (?, ?)
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        members
        (name, team_id, member_id) 
    values
        (?, ?, ?)
Hibernate: 
    select
        member0_.member_id as member_i1_1_,
        member0_.name as name2_1_,
        member0_.team_id as team_id3_1_ 
    from
        members member0_
Hibernate: 
    select
        team0_.team_id as team_id1_2_0_,
        team0_.name as name2_2_0_ 
    from
        teams team0_ 
    where
        team0_.team_id=?

Team 데이터와 Member 데이터를 저장하는 insert문이 2방 나가고 Member 속성만 조회하고 싶은데 의도치 않게 Team 엔티티까지 select 문이 나갔다.

왜? Team 엔티티까지 select 문이 나간걸까? @ManyToOne 의 어노테이션을 살펴보자.

fetch() 속성을 잘보면 기본값이 EAGER 로 설정되어 있다. EAGER 는 즉시 로딩을 뜻하며 연관관계가 맺어진 엔티티의 정보들을 전부 로딩 시켜준다.

EAGER 말고 다른 타입은 없을까? Fetch 타입을 살펴보자.

LAZY 가 있다. LAZY 는 지연 로딩 을 뜻하며 연관관계가 맺어진 엔티티의 정보들을 지연 로딩 시켜준다.

fetch 속성을 LAZY 로 설정하여 동작을 해보자.

Hibernate: 
    insert 
    into
        teams
        (name, team_id) 
    values
        (?, ?)
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        members
        (name, team_id, member_id) 
    values
        (?, ?, ?)
Hibernate: 
    select
        member0_.member_id as member_i1_1_,
        member0_.name as name2_1_,
        member0_.team_id as team_id3_1_ 
    from
        members member0_

Member 엔티티 속성만 select 하는 query가 동작하는 것을 볼 수 있다.

위의 fetch 설정으로 쿼리 동작을 살펴볼 수 있었고 즉시로딩의 N + 1의 문제점을 볼 수 있었다. 그렇다면 이 문제점을 해결하기 위해 무엇을 해야할까?

일반 조인과 다르게 패치 조인을 해야한다. 패치 조인 쿼리를 작성하여 동작해보자.

@Query("select m from Member m join fetch m.team")
    List<Member> findAll();

findAll() 메서드의 패치조인을 사용하여 JPQL를 작성한다.

쿼리 동작 결과

Hibernate: 
    select
        member0_.member_id as member_i1_1_0_,
        team1_.team_id as team_id1_2_1_,
        member0_.name as name2_1_0_,
        member0_.team_id as team_id3_1_0_,
        team1_.name as name2_2_1_ 
    from
        members member0_ 
    inner join
        teams team1_ 
            on member0_.team_id=team1_.team_id

일반 조인과 패치 조인의 차이점을 알아보기 위해 일반 조인도 동작해보자.

@Query("select m from Member m join m.team")
    List<Member> findAll();

동작 쿼리 결과

Hibernate: 
    select
        member0_.member_id as member_i1_1_,
        member0_.name as name2_1_,
        member0_.team_id as team_id3_1_ 
    from
        members member0_ 
    inner join
        teams team1_ 
            on member0_.team_id=team1_.team_id
Hibernate: 
    select
        team0_.team_id as team_id1_2_0_,
        team0_.name as name2_2_0_ 
    from
        teams team0_ 
    where
        team0_.team_id=?

Member 테이블과 Team 테이블 조인을 하고 Team 테이블에 select문이 더 나가는 것을 확인할 수 있다.

쿼리의 동작 결과를 보면 조인과 패치 조인의 차이는 조회하고자하는 엔티티의 정보가 사용되는 컬럼이 차이점이 보이고 select문이 한번더 동작되는 차이점을 확인 할 수 있다.

이처럼 일반 조인은 조회 엔티티만 조회 하여 영속화 시키고 패치 조인은 조회 엔티티와 연관관계를 맺은 모든 엔티티 들을 영속화 시킨다는 차이점이라 정리 할 수 있고 패치 조인으로 해서 N + 1 의 문제점을 해결 할 수 있다고 정리할 수 있다.

  • 일반 조인과 패치 조인의 차이점을 잘 설명해주는 블로그 참조
    • https://cobbybb.tistory.com/18

'JPA' 카테고리의 다른 글

[Jpa] Deprecated 된 getById() 대안 getReferenceById()  (0) 2022.08.02
[Jpa] Dirty Checking  (0) 2022.07.19
[Jpa] Projections  (0) 2022.07.18
[Spring JPA] 트랜잭션은 언체크, 체크 예외에 대해 어떻게 커밋과 롤백을 처리할까?  (1) 2022.05.18
[Spring JPA] CASCADE 는 무엇일까?  (0) 2022.05.15
[Spring JPA] 프록시 객체는 어떻게 동작할까?  (0) 2022.05.12
[Spring JPA] 객체지향의 상속관계 매핑은 어떤 전략을 사용할까?  (0) 2022.05.12
[Spring JPA] 객체의 연관관계는 어떻게 관계를 맺는것이 좋을까?  (0) 2022.05.10
    'JPA' 카테고리의 다른 글
    • [Spring JPA] 트랜잭션은 언체크, 체크 예외에 대해 어떻게 커밋과 롤백을 처리할까?
    • [Spring JPA] CASCADE 는 무엇일까?
    • [Spring JPA] 프록시 객체는 어떻게 동작할까?
    • [Spring JPA] 객체지향의 상속관계 매핑은 어떤 전략을 사용할까?
    kkkkkkkkkkkk
    kkkkkkkkkkkk

    티스토리툴바