JPA의 시작과 끝이라고 불리우는 만큼 중요한 개념인 N+1 문제에 대해 간단명료하게 정리해보려 한다.
LAZY를 적용한 이후의 상황에 대한 내용이다.
즉, 단일객체 필드를 조회하는 경우
class Member{ Long member_id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_id") Team team; }만약 Member를 조회한다면,
Member를 조회하는 쿼리와 Member의 team을 조회하는 쿼리, 이렇게 1 + 1 번의 쿼리가 날아갈 것이다.
즉, 컬렉션 필드를 조회하는 경우
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() 번의 쿼리가 날아갈 것이다.
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 번의 쿼리로 감소
마찬가지로 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 번의 쿼리로 감소
JPQL이므로 당연히 필드명임
* 일반 조인과의 차이를 통해 설명
일반조인 사용
//일반조인 @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됨
1대다 조회시(조인시) "1"이 "다"를 여러개 가지므로 다음과 같은 결과가 나옴.
결과 테이블 ex)
팀 소속멤버
team1 member1
team1 member2
team1 member3
"1"의 엔터티가 중복되는 현상 발생. 따라서 JPQL의 distinct를 사용해 중복 엔터티를 묶어줌.
* SQL의 distinct와 차이점
SQL의 distinct는 같은 튜플을 제거해줌. 하지만 위의 결과 테이블엔 같은 튜플은 없음.
JPQL의 distinct는 같은 엔터티를 하나로 묶어주는 기능을 함.
사실 EAGER의 작동방식도 LAZY와 마찬가지로 쿼리가 여러번에 걸쳐 날아감.
Team을 조회할 때, Team을 조회하는 쿼리와 List<Member>를 조회하는 쿼리들 이렇게 나뉘어서 연속으로 실행되는 것 뿐임.
즉, 연관관계가 두 번에 걸쳐진 엔터티까지 모두 한 번에 찾고 싶다면?
@Query("select m from Member m join fetch m.posts p join fetch p.comments")
List<Member> findAllMembers();
위와 같이 연속으로 Fetch Join을 걸어주면 됨
다음 포스팅 참고
[TIL] 페이징 _Spring Data Jpa
참고자료
Fetch Join(패치 조인)과 일반 Join의 차이점
JPA N+1 문제와 해결법 총정리
[10분 테코톡] 수달의 JPA N+1 문제
ChatGPT