본문 바로가기
Querydsl

1+N 문제의 모든것

by asdft 2024. 2. 13.

개요

JPQL과 Querydsl을 공부하면서 1+N 문제에 대해서 한번쯤은 들어봤을 것이라고 생각한다.

그럼 제일 많은 답변은 "연관관계를 EAGER가 아닌 LAZY로 설정하라!" 이였다.

하지만 이 말만 듣고 "1+N문제 해결" == "연관관계를 LAZY로 설정" 으로 알고 있는 사람들이 많았고 나도 그중 하나였다.

그래서 이에 대한 개념을 바로잡고자 한다.


들어가기에 앞서,

 

N+1 문제란?

연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다. 이를 N+1 문제라고 한다.

 

@Entity
@Getter
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
  }

위와 같은 예시가 있다고 해보자.

 

먼저, 연관관계(LAZY / EAGER)에 따라 쿼리가 나가는 과정을 먼저 살펴보자

 

즉시 로딩(EARGR로 설정)

1. 멤버 전체를 조회하기 위해 JPQL 실행 select m from member m

2. JPQL은 EAGER와 무관하게 SQL로 그대로 번역 -> select m.* from member

3. JPQL 결과가 member만 조회하고, team은 조회하지 않음

4. member와 team이 즉시 로딩으로 설정되어 있기 때문에 연관된 팀을 각각 쿼리를 날려서 추가 조회 (N+1)

 

 

지연 로딩(LAZY로 설정)

1. 멤버 전체를 조회하기 위해 JPQL 실행 select m from member m

2. JPQL은 EAGER와 무관하게 SQL로 그대로 번역 -> select m.* from member

3. JPQL 결과가 member만 조회하고, team은 조회하지 않음

4. member와 team이 지연 로딩으로 설정되어 있기 때문에 가짜 프록시 객체를 넣어두고, 실제 회원은 팀은 조회하지 않음

5. 실제 team을 사용하는 시점에 쿼리를 날려서 각각 조회(N+1)

 

즉 LAZY와 EAGER 방식 모두 시점의 차이만 있을 뿐, 엔티티를 참조하기 위해서는 쿼리를 날려서 조회를 해줘야 한다.
이 때문에, 연관관계를 LAZY 타입으로 설정하더라도 엔티티를 직접 참조해야 한다면, N+1 문제가 발생할 수 있는
것이다.

 

 

fetch join 또는 엔티티 그래프(EAGER, LAZY 상관 없음)

1. 멤버와 팀을 한번에 조회하기 위해 JPQL+fetch join 실행 select m from member m join fetch m.team

2. JPQL에서 fetch join을 사용했으므로 SQL은 멤버와 팀을 한 쿼리로 조회 -> select m.*, t.* from member join team ...

3. JPQL 결과가 member와 team을 한꺼번에 조회함

4. member와 team이 fetch join으로 한번에 조회되었으므로 N+1 문제가 발생하지 않음

 

  select / EAGER
(즉시 로딩)
select / LAZY
(지연 로딩)
일반 Join
(지연 로딩)
Fetch Join
(즉시 로딩)
 
User 호출 시점 User 쿼리 User 쿼리 User 쿼리 User , Team
(Fetch Join 쿼리로 한번에 조회)
Team 각각 쿼리
Team  사용 시점   Team 각각 쿼리 Team 각각 쿼리  
총 쿼리 수 N+1 회 N+1 회 N+1 회 1회

<JPQL만 이용해서 쿼리를 날렸을때로 가정>

 

사실, 일반 JOIN은 SELECT 시점에 User만 조회하는 것이기 때문에, 일반 select user와 같다.

 

  • 일반 Join : join 조건을 제외하고 실제 질의하는 대상 Entity에 대한 컬럼만 SELECT
  • Fetch Join : 실제 질의하는 대상 Entity와 Fetch join이 걸려있는 Entity를 포함한 컬럼 함께 SELECT

Join과 FetchJoin에 대해 더 자세히 알아보고 싶다면 아래 글을 참고하자

https://cobbybb.tistory.com/18

 

[JPA] 일반 Join과 Fetch Join의 차이

JPA를 사용하다 보면 바로 N+1의 문제에 마주치고 바로 Fetch Join을 접하게 됩니다. 처음 Fetch Join을 접했을 때 왜 일반 Join으로 해결하면 안되는지에 대해 명확히 정리가 안된 채로 Fetch Join을 사용했

cobbybb.tistory.com

 

 

실무에서 기본은 연관관계 설정을 LAZY 로만 사용 한다. 그렇게 해야 member만 필요해서 조회할 때, member만 조회가 되기 때문이다.
그런데 때때로 특정 기능에서는 member와 team이 함께 필요한 경우, 이때 그 상황에 맞는 로직에서 fetch join을
사용하면 n+1 문제를 해결할 수 있다.