tech/Spring

[Spring] N + 1 문제

sunm2n 2025. 7. 18. 14:50

N+1 문제란?

 

N+1 문제란, 하나의 쿼리(1)를 실행한 이후에, 연관된 엔티티를 가져오기 위해 추가로 N개의 쿼리가 실행되는 현상이다.
이로 인해 총 1 + N개의 쿼리가 실행되어 성능 저하가 발생한다.

 

예를 들어, Member와 Team이라는 두 개의 엔티티가 있고, Member는 Team에 대해 다대일(N:1) 관계를 가지고 있다고 가정하면

 

@Entity
public class Member {
  @Id
  private Long id;

  private String name;

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

@Entity
public class Team {
  @Id
  private Long id;

  private String name;
}

 

그리고 다음과 같은 코드가 있다고 가정한다.

List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class).getResultList();

for (Member member : members) {
  System.out.println(member.getTeam().getName()); // LAZY 로딩
  }

 

 

위 코드는 다음과 같은 방식으로 동작한다.

 

 

1. 먼저 Member 리스트를 가져오는 쿼리 1회 실행

SELECT * FROM member;

 

2. 이후 member.getTeam().getName()이 호출될 때마다 Team 정보를 가져오기 위한 쿼리가 멤버 수 만큼 N번 실행

SELECT * FROM team WHERE id = ?;  // N번 반복

 

 

 

문제점

 

  • 쿼리 수가 많아져 DB 부하 증가
  • 특히 N의 값이 클수록 성능이 급격하게 저하
  • 사용자 수가 많은 페이지(예: 관리자 회원 목록)에선 치명적인 성능 이슈 유발

 

 

해결방법

 

1. Fetch Join 사용

SELECT m FROM Member m JOIN FETCH m.team

 

 

JPA가 한 번의 쿼리로 Member와 Team을 함께 조회하도록 합니다. 즉, 조인 후 한 번의 쿼리로 모든 데이터를 가져옴

 

 

 

2. EntityGraph 사용 (Spring Data JPA 전용)

@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

 

 

 

명시적으로 어떤 연관 관계를 즉시 로딩(EAGER) 해야 하는지 지정 가능

 

 

 

3. BatchSize 설정 (LAZY 로딩이 필요할 경우)

@BatchSize(size = 100)
@ManyToOne(fetch = FetchType.LAZY)
private Team team;

 

또는 application.yml에서 글로벌하게 설정 가능하다.

spring:
jpa:
properties:
hibernate.default_batch_fetch_size: 100

 

 

 

4. DTO 직접 조회

필요한 필드만 DTO로 조회하여 N+1 문제를 원천 차단

@Query("SELECT new com.example.dto.MemberDto(m.name, t.name) FROM Member m JOIN m.team t")
List<MemberDto> findMemberDtos();

 

장점: 필요한 데이터만 가져온다. (성능 최적화)

단점: 복잡한 DTO가 많아지면 관리가 힘들 수 있다.

 

 

 

 

5. 쿼리 튜닝 + 페이징 전략

 

예: 먼저 ID만 조회하고, 그 ID 목록을 기준으로 연관 엔티티를 다시 조회

두 번의 쿼리지만 N+1이 아닌 1+1 구조로 제어

// 1차: ID 목록만 조회
SELECT m.id FROM Member m WHERE ...

// 2차: ID IN (...) 조건으로 fetch join
SELECT m FROM Member m JOIN FETCH m.team WHERE m.id IN (:ids)

 

 

복잡한 데이터 페이징 시에 많이 쓰인다. (@Query, QueryDSL 등)

 

 

 

6. Hibernate 초기화 강제 (명시적 로딩)

Hibernate.initialize(member.getTeam());

 

트랜잭션 내에서 미리 로딩시키는 방식

코드에서 명시적으로 로딩 시점을 제어할 수 있음

하지만 fetch join처럼 자동 최적화되지 않기 때문에 신중히 사용

 

 

 

결론

 

Fetch Join : 가장 직관적

EntityGraph : Spring Data 전용

BatchSize : in-query 최적화

DTO 조회 : 재사용성이 떨어질 수 있음

ID → 상세조회 : 성능 튜닝 목적

Hibernate.initialize : 트랜잭션 제어 필요

 

'tech > Spring' 카테고리의 다른 글

[Spring] @Builder(toBuilder = true)  (1) 2025.07.24
[Spring] 좋은 AppConfig 설계  (0) 2025.07.19
[Spring] Config  (0) 2025.07.09
[Spring] SOLID 원칙을 잘 지키고 있는가  (0) 2025.07.04
[Spring] long VS Long  (1) 2025.07.03