[Spring]N+1 문제

2025. 5. 8. 15:32·Spring

JPA의 N+1 문제는 쿼리 폭증으로 DB 부하를 일으키고, 결국 사용자 경험과 시스템 안정성을 해칠 수 있다.

이 글은 N+1 문제의 원인과 실무 해결법을 코드 예시와 함께 정리해볼려고 한다.


✅ N+1 문제란?

  • N+1 문제는 JPA 같은 ORM에서 연관 관계를 조회할 때 불필요한 쿼리가 반복 발생하는 현상이다. 예를 들어, 하나의 엔티티를 조회한 뒤 연관된 데이터를 조회하려고 하면, 연관 데이터 개수만큼 추가 쿼리가 날아간다. 

 

왜 생기나?

  • 객체지향 언어는 메모리 내에서 참조로 쉽게 데이터를 탐색하지만, 관계형 DB는 쿼리로 데이터를 가져와야 한다.
  • 특히 @OneToMany 관계에서 FetchType.LAZY 설정 시, 프록시 객체가 생성되고 이 객체를 조회할 때마다 추가 쿼리가 발생한다.

 

 

✅ N+1 문제 예시

  • n+1 문제를 고의로 발생시키기 위해 아래와 같은 코드를 작성해보았다.
  • 국가(Country) 와 도시(City)는 1:n 양방향 관계이다.(실제로는 단방향+단방향)

 

♐ CountryEntity

@Entity
@Getter
@Setter
public class CountryEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String country;

    @OneToMany(mappedBy = "countryEntity")
    private List<CityEntity> cityEntities = new ArrayList<>();
}

 

♐ CityEntity

@Entity
@Getter
@Setter
public class CityEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String city;

    @ManyToOne
    private CountryEntity countryEntity;
}

 

 

CountryService에서 모든 국가를 조회하는 메서드를 호출한다고 해보자

 

♐ CountryService

public List<CountryEntity> readCountry() {
    return countryRepository.findAll();
}

 

 

그리고 컨트롤러에서 이 데이터를 뷰로 넘긴다

 

♐ ReadController

@GetMapping("/read/one/child")
public String readOneChild(Model model) {

    model.addAttribute("COUNTRYLIST", countryService.readCountry());

    return "readOneChild";
}

 

 

데이터를 삽입하고 Country 의 city 목록을 가져온다.

데이터를 삽입하고 Country 의 city 목록을 가져온다.

  1. CountryEntity 목록을 조회하는 1개의 쿼리.
  2. 각 CountryEntity의 cityEntities를 조회하는 3개의 추가 쿼리.

 

 

총 1+3=4개의 쿼리가 발생했다. 국가가 100개라면? 101개 쿼리가 날아간다. 

 

 

✅ N+1 문제 해결방법

  • N+1 문제를 해결하려면 쿼리 수를 줄이고 데이터를 효율적으로 가져와야 한다.

1️⃣ Fetch Join

  • CountryRepository에 Fetch Join 추가하였다.

♐ CountryRepository

@Query("SELECT DISTINCT c FROM CountryEntity c JOIN FETCH c.cityEntities")
List<CountryEntity> findAllWithCities();

 

이렇게 적용하니 join 을 통해 쿼리 하나로 처리됐다.

 

select distinct ce1_0.id,ce2_0.countryEntity_id,ce2_0.id,ce2_0.city,ce1_0.country 
from CountryEntity ce1_0 
join CityEntity ce2_0 
on ce1_0.id=ce2_0.countryEntity_id

 

 

2️⃣ BatchSize

  • default_batch_fetch_size는 Lazy Loading 시 IN 절로 연관 데이터를 묶음 단위로 조회한다.
  • Fetch Join의 페이징 제한을 해결할 수 있다.

application.yml

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

 

해당 batchSize를 설정해주면 , CountryEntity 3개를 조회한 뒤, cityEntities를 IN 절로 한 번에 조회한다.

1 (CountryEntity) + 1 (CityEntity) = 2개.

Fetch Join보다 쿼리가 1개 더 발생하지만, 데이터 전송량은 중복 없이 효율적이다.

 

그치만 단점도 존재한다.

 

💡 BatchSize의 단점

  • 임의의 값 설정 문제: BatchSize는 개발자가 경험적으로 설정한다. (한마디로 임시방편 느낌)너무 작으면 쿼리 수가 늘어나고, 너무 크면 메모리 부담이 커진다. 예를 들어, size=10으로 설정하면 3개 국가를 10개씩 처리할 필요는 없지만, 국가가 1000개라면 여러 IN 절 쿼리가 발생해 비효율적이다.
  • DB 부하 불균형: IN 절로 큰 데이터셋을 조회하면 DB 인덱스 활용이 비효율적일 수 있다. 특히 city_entity 테이블이 크면 쿼리 성능이 떨어진다.
  • DBMS 제약: MySQL 등은 IN 절에 포함할 수 있는 값에 제한(보통 1000개)이 있다. BatchSize가 이를 초과하면 쿼리가 실패하거나 여러 쿼리로 나뉜다.

 

3️⃣ @EntityGraph

  • @EntityGraph는 Lazy Loading을 동적으로 Eager Loading으로 전환해 연관 데이터를 한 번에 조회한다.

♐ CountryRepository

public interface CountryRepository extends JpaRepository<CountryEntity, Long> {
    @EntityGraph(attributePaths = {"cityEntities"})
    List<CountryEntity> findAll();
}

 

Fetch Join과 유사한 단일 쿼리로 CountryEntity와 CityEntity 조회하고 여러 @OneToMany 관계를 동시에 처리할 수 있어 유연하다.

 

✅ 정리

  • N+1 문제: 연관 관계 조회 시 쿼리가 불필요하게 많아진다.
  • 문제 상황: france, korea, usa 조회 시 1+3=4개 쿼리 발생.
  • 해결법:
    • Fetch Join: 단일 쿼리로 해결, 페이징은 조심.(자주 사용)
    • BatchSize: IN 절로 쿼리 수 감소, 페이징 가능.
    • @EntityGraph: 간편하고 유연한 연관 데이터 조회.

'Spring' 카테고리의 다른 글

[Spring Security] 스프링 시큐리티와 JWT로 인증 구현하기  (2) 2025.05.15
[Spring] 트랜잭션 내부 호출 문제  (1) 2025.05.01
[Spring]Builder 패턴 vs 생성자, 무엇이 더 나을까?  (3) 2025.04.28
[Spring]오프셋 페이징보다 커서 페이징을 써야하는 이유  (1) 2025.04.25
[Spring/JPQL] JPQL이 무엇인지 알고 사용하자  (1) 2025.04.18
'Spring' 카테고리의 다른 글
  • [Spring Security] 스프링 시큐리티와 JWT로 인증 구현하기
  • [Spring] 트랜잭션 내부 호출 문제
  • [Spring]Builder 패턴 vs 생성자, 무엇이 더 나을까?
  • [Spring]오프셋 페이징보다 커서 페이징을 써야하는 이유
코딩로봇
코딩로봇
금융 IT 개발자
  • 코딩로봇
    쟈니의 일지
    코딩로봇
  • 전체
    오늘
    어제
    • 분류 전체보기 (137) N
      • JavaScript (8)
      • SQL (10)
      • 코딩테스트 (30) N
        • Java (15)
        • SQL (13) N
      • Java (10)
      • 프로젝트 (22) N
        • 트러블슈팅 (7)
        • 프로젝트 회고 (13) N
      • git,Github (2)
      • TIL (36) N
      • Spring (17)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    스파르타 코딩 #부트캠프 #첫ot
    java #arraylist #list #배열
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
코딩로봇
[Spring]N+1 문제
상단으로

티스토리툴바