[TIL] N+1 문제 (feat.Fetch Join)

이형석·2024년 9월 20일

JPA의 시작과 끝이라고 불리우는 만큼 중요한 개념인 N+1 문제에 대해 간단명료하게 정리해보려 한다.

LAZY를 적용한 이후의 상황에 대한 내용이다.

문제상황

1. @ManyToOne 연관관계의 필드 조회

즉, 단일객체 필드를 조회하는 경우

class Member{
	Long member_id;
	@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
	Team team;
}

만약 Member를 조회한다면,
Member를 조회하는 쿼리와 Member의 team을 조회하는 쿼리, 이렇게 1 + 1 번의 쿼리가 날아갈 것이다.

2. @OneToMany 연관관계의 필드 조회

즉, 컬렉션 필드를 조회하는 경우

class Team{
	Long team_id;
    //@OneToMany는 FetchType.LAZY가 기본값임
	@OneToMany(mappedBy = "team", casecade = CasecadeType.ALL, OrphanRemoval = true)
    List<Member> members = new LinkedList<>();
}

만약 Team을 조회한다면,
Team을 조회하는 쿼리와 Team의 List<Member> members을 조회하는 쿼리들, 이렇게 1 + members.size() 번의 쿼리가 날아갈 것이다.

해결방법

1. @ManyToOne 연관관계의 필드 조회

Fetch Join을 이용하여 해결

interface MemberRepository extends JpaRepository<Member, Long>{
  	//단일 조회 _위 문제의 상황
	@Query("select m from Member m join fetch m.team where where m.id = :memberId")
	Member findMember(@Param("memberId") Long memberId);
	//전체 조회
  	@Query("select m from Member m join fetch m.team")
    List<Member> findAllMembers();
}

1 + 1 번의 쿼리가, 조인을 통해 1 번의 쿼리로 감소

2. @OneToMany 연관관계의 필드 조회

마찬가지로 Fetch Join을 이용하여 해결
단, distinct 사용 필요

interface TeamRepository extends JpaRepository<Team, Long>{
	//단일 조회 _위 문제의 상황
  	@Query("select distinct t from Team t join fetch t.members where t.id = :teamId")
  	Team findTeam(@Param("teamId") Long teamId);
  	//전체 조회
  	@Query("select distinct t from Team t join fetch t.members")
  	List<Team> findAllTeams();
}

1 + members.size() 번의 쿼리가, 조인을 통해 1 번의 쿼리로 감소

궁금증 해결

1. 위 JPQL 쿼리문에서, m.id와 t.id에서 id는 db의 컬럼명인가?

JPQL이므로 당연히 필드명임

2. Fetch Join이란?

* 일반 조인과의 차이를 통해 설명

일반조인 사용

//일반조인
@Query("select distinct t from Team t join t.members")
List<Team> findAllTeams();
//발생쿼리
select distinct team.id
from team join member 
on team.id = member.team_id

문제 : 조인은 했지만 Team의 컬럼만 select되고, member의 컬럼은 select되지 않음.
(직접 SQL을 사용해 조인한다면, SELECT절에 사용할 컬럼을 모두 적시해줌. 하지만 위 JPQL을 이용한 쿼리문에는 Team만 select하고 있음.)

Fetch 조인 사용

//Fetch 조인
@Query("select distinct t from Team t join fetch t.members")
List<Team> findAllTeams();
//발생쿼리
select distinct team.id, member.id
from team join member 
on team.id = member.team_id

해결 : Fetch 조인 사용시, Team의 컬럼들과 Member의 컬럼들 모두 select됨

3. @OneToMany에서 distinct를 사용하는 이유

1대다 조회시(조인시) "1"이 "다"를 여러개 가지므로 다음과 같은 결과가 나옴.

결과 테이블 ex)
팀		소속멤버
team1	member1
team1	member2
team1	member3

"1"의 엔터티가 중복되는 현상 발생. 따라서 JPQL의 distinct를 사용해 중복 엔터티를 묶어줌.

* SQL의 distinct와 차이점
SQL의 distinct는 같은 튜플을 제거해줌. 하지만 위의 결과 테이블엔 같은 튜플은 없음.
JPQL의 distinct는 같은 엔터티를 하나로 묶어주는 기능을 함.

4. FetchType.EAGER로 해결하면 안되나?

사실 EAGER의 작동방식도 LAZY와 마찬가지로 쿼리가 여러번에 걸쳐 날아감.
Team을 조회할 때, Team을 조회하는 쿼리와 List<Member>를 조회하는 쿼리들 이렇게 나뉘어서 연속으로 실행되는 것 뿐임.

5. Member와, Member의 Post들과, 그 Post의 Comment들을 한 번의 쿼리로 찾고 싶다면?

즉, 연관관계가 두 번에 걸쳐진 엔터티까지 모두 한 번에 찾고 싶다면?

@Query("select m from Member m join fetch m.posts p join fetch p.comments")
List<Member> findAllMembers();

위와 같이 연속으로 Fetch Join을 걸어주면 됨

주의할 점

@OneToMany 관계에서 페이징시에 Fetch Join 사용시 문제 발생

다음 포스팅 참고
[TIL] 페이징 _Spring Data Jpa


참고자료
Fetch Join(패치 조인)과 일반 Join의 차이점
JPA N+1 문제와 해결법 총정리
[10분 테코톡] 수달의 JPA N+1 문제
ChatGPT

profile
금융IT 개발자

0개의 댓글