반응형
JPA에서 골치아픈 N+1 문제 해결방법을 몇가지 정리한다.
N+1 문제 => ORM에서 많이 발생하는 문제
- Entity에 대해 하나의 쿼리로 N개의 레코드를 가져왔을 때, 연관관계 Entity를 가져오기 위해 쿼리를 N번 추가적으로 수행하는 문제
N+1 문제에 대해 흔히들 하는 오해
- N+1 문제는 EAGER Fetch 전략때문에 발생한다?
- X, LAZY로 설정했더라도 연관 Entity를 참조하면 그 순간 추가적인 쿼리가 수행됨
- findAll()메서드는 N+1 문제를 발생시키지 않는다?
- Fetch전략을 적용해서 연관 Entity를 가져오는 것은 오직 단일 레코드에 대해서만 적용
- 단일 레코드 조회가 아닌 경우(JPQL을 수행하는 경우, findAll() 메서드 역시 이 경우)
- 해당 JPQL을 먼저 수행(Entity에 설정된 Fetch 전략 적용 안됨)
- 반환된 레코드 하나 하나에 대해 Entity 설정된 Fetch 전략을 적용해서 연관 Entity 가져옴
- 그렇기 때문에 findAll() 메서드 호출도 역시 이 과정에서 N+1 문제 발생 가능
2020/12/21 - [프로그래밍 노트/JPA] - [JPA] 일대다(1:N), 다대일(N:1) 단방향매핑 관련 고찰
이 전 포스팅처럼 Member, MemberDetail이 양방향 연관관계를 갖고있는 상태이며, N+1 문제를 발생시키기위해 임의로 데이터를 넣어둔상태이다.
findAll()메서드를 실행해서 N+1 문제를 발생시켜보자.
@Test
void memberDetailFindTest(){
memberDetailRepository.findAll();
}
우리가 원하는 것은 member와 memberDetail이 조인되어서 한방 쿼리가 나갈 것 같지만, 실제로는 아래와 같은 쿼리가 날라간다.
select ... from member_detail memberdeta0_
select ... from member member0_ where member0_.member_id=?
select ... from member member0_ where member0_.member_id=?
select ... from member member0_ where member0_.member_id=?
select ... from member member0_ where member0_.member_id=?
- memberDetail을 가져온다.
- memberDetail에 연관된 member를 가져온다.(memberDetail에 저장된 member들을 가져온다. 데이터에선 4개이므로, 4번의 select가 날라간다.)
N+1 문제 해결하기
그렇다면, 이제 N+1 문제를 해결해보자.
N+1문제를 해결하기 위해서는 fetchJoin을 사용해야 하는데, 해결방법은 아래 세가지 이다. 차례대로 적용해보자.
- JPQL 사용
- QueryDSL 사용
- EntityGraph 사용
1. JPQL 사용
@Repository
public interface MemberDetailRepository extends JpaRepository<MemberDetail, Long>, MemberDetailRepositoryCustom {
@Query("select m from MemberDetail m inner join fetch m.member")
List<MemberDetail> findAll_JPQL();
}
@Test
void memberDetailFindTest(){
memberDetailRepository.findAll_JPQL();
}
select
memberdeta0_.member_detail_id as member_d1_1_0_,
member1_.member_id as member_i1_0_1_,
memberdeta0_.description as descript2_1_0_,
memberdeta0_.member_id as member_i4_1_0_,
memberdeta0_.type as type3_1_0_,
member1_.create_date as create_d2_0_1_,
member1_.name as name3_0_1_
from
member_detail memberdeta0_
inner join
member member1_
on memberdeta0_.member_id=member1_.member_id
2. QueryDSL 사용
QueryDSL 사용방법은 생략
@RequiredArgsConstructor
public class MemberDetailRepositoryImpl implements MemberDetailRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<MemberDetail> findAll_QueryDsl() {
return queryFactory.selectFrom(memberDetail)
.innerJoin(member).on(memberDetail.member.eq(member))
.fetchJoin().fetch();
}
}
@Test
void memberDetailFindTest(){
memberDetailRepository.findAll_QueryDsl();
}
select
memberdeta0_.member_detail_id as member_d1_1_0_,
member1_.member_id as member_i1_0_1_,
memberdeta0_.description as descript2_1_0_,
memberdeta0_.member_id as member_i4_1_0_,
memberdeta0_.type as type3_1_0_,
member1_.create_date as create_d2_0_1_,
member1_.name as name3_0_1_
from
member_detail memberdeta0_
inner join
member member1_
on memberdeta0_.member_id=member1_.member_id
3. EntityGraph 사용
@Repository
public interface MemberDetailRepository extends JpaRepository<MemberDetail, Long>, MemberDetailRepositoryCustom {
@EntityGraph(attributePaths = {"member"}, type = EntityGraph.EntityGraphType.LOAD)
List<MemberDetail> findAll();
}
@Test
void memberDetailFindTest(){
memberDetailRepository.findAll();
}
select
memberdeta0_.member_detail_id as member_d1_1_0_,
member1_.member_id as member_i1_0_1_,
memberdeta0_.description as descript2_1_0_,
memberdeta0_.member_id as member_i4_1_0_,
memberdeta0_.type as type3_1_0_,
member1_.create_date as create_d2_0_1_,
member1_.name as name3_0_1_
from
member_detail memberdeta0_
inner join
member member1_
on memberdeta0_.member_id=member1_.member_id
이로써 N+1 문제 해결
반응형
'프로그래밍 노트 > JPA' 카테고리의 다른 글
[JPA] 일대다(1:N), 다대일(N:1) 단방향매핑 관련 고찰 (3) | 2020.12.21 |
---|---|
[QueryDSL] QueryDSL로 SQL Replace 사용하기 (0) | 2020.11.06 |
[JPA] 다양한 연관관계_일대다(1:N) (2) | 2019.10.27 |
[JPA] 다양한 연관관계_다대일(N:1) (0) | 2019.10.27 |
[JPA] 연관관계 매핑 기초(단방향/양방향) (5) | 2019.10.10 |