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 의 문제점을 해결 할 수 있다고 정리할 수 있다.
- 일반 조인과 패치 조인의 차이점을 잘 설명해주는 블로그 참조
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 의 문제점을 해결 할 수 있다고 정리할 수 있다.
- 일반 조인과 패치 조인의 차이점을 잘 설명해주는 블로그 참조
'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 |