본문 바로가기
spring | spring boot

[SpringBoot] JPA의 1+N 문제와 해결방법 알아보기

by socialcomputer 2025. 11. 24.

이번엔 JPA에서 일어나는 N+1 문제를 알아보려고 한다

제목엔 1+N이라 적었는데 좀 더 직관적 이해가 되는 것 같아 저렇게 했다ㅎㅎ

 

1+N 문제가 일어나는 경우

JPA 리포지토리를 사용해 메서드를 호출할 때, 일대다 관계를 가진 엔터티를 조회할 경우, 의도한 첫번째 쿼리 외에 추가로 N개의 쿼리가 발생하는 문제다. 

Team과 Member가 있다. 이 관계는 1 : N 관계다. 예를 들어 10팀이 있고 각  팀은 여러 멤버를 가진다.

팀을 조회하는 상황을 보자

 

case 1. 즉시로딩 EAGER

@OneToMany(fetch = FetchType.EAGER)

findAll()을 실행하면

1. select * from team 쿼리를 날리고

2. 패치타입이 eager 니까 member도 가져옴

3. 팀 전체 조회 1번 + 각 팀마다 멤버 조회해서 10번 = 총 11번의 조회를 한다

-- 1.
select * from team;

-- 2.
select * from member where team.id = 1;
select * from member where team.id = 2;
...
select * from member where team.id = 10;

 

case 2. 지연 로딩 LAZY

@OneToMany(fetch = FetchType.LAZY)

findAll()을 실행하면

1. select * from team 쿼리를 날리고

2. 패치타입이 lazy니까 멤버를 아직 조회하지 않음

3. 비지니스 로직에서 팀마다 총인원을 구하는 경우 -> 1팀 ~ 10팀까지 조회함

4. 결국 1 + 10 = 11번의 조회를 실행

 

해결방법

그렇다면 이런 문제를 어떻게 해결할까? 

방식은 여러가지가 있다. 

1. Fetch Join

1+N 번애 걸쳐 따로 조회하지 않고 두 테이블을 join해서 한번에 조회하면 된다

애초에 다 불러오는 것이다.

JPQL을 직접 작성 join fetch t.members

@Query("select t from Team t join fetch t.members")
List<Team> findAllJoinFetch();
SELECT t.*, m.*
FROM team t
INNER JOIN member m ON t.id = m.team_id;

inner join을 사용한다

그러나 단점도 있다

  • 1:N 관계가 두 개 이상인 경우 사용 불가 -> 한개 컬렉션만 가능
  • JPA Paging 사용 불가능 -> 전체 데이터를 가져오게 되어 데이터가 많으면 out of memory로 서버가 터짐

 

2. @EntityGraph (간편한 어노테이션)

JPQL을 직접 짜지 않고, 메서드 위에 어노테이션으로 가져올 필드명을 지정하는 방법

@EntityGraph(attributePaths = {"members"})
@Query("select t from Team t")
List<Team> findAllEntityGraph();
SELECT t.*, m.*
FROM Team t
LEFT OUTER JOIN Member m ON t.id = m.team_id;
  • left otuer join을 사용 -> null 데이터가 포함될 수 있음 -> inner join보다 성능 안좋음
  • fetch join 처럼 페이징시 메모리 이슈가 발생

 

3. Batch Size (in 쿼리 최적화)

통합해서 1번이 아니라, n번 실행될 것을 in 절을 사용하여 획기적으로 줄이는 방법

(1 + N -> 1 + N/BatchSize)

YAML 파일에 설정

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000

지연 로딩 상태에서, t.getMembers()를 호출할 때 team 1000개까지 한꺼번에 가져옴

팀이 총 2000개라고 예시를 두면, 이때 실행되는 sql

-- 1. 팀 전체 조회
SELECT * FROM Team;

-- 2. 필요할 때 IN 쿼리로 한 번에 조회
SELECT * FROM Member WHERE team_id IN (1, 2, 3, ..., 1000);

SELECT * FROM Member WHERE team_id IN (1001, 1002, 1003, ..., 2000);
  • batch size가 너무 크면 성능이슈가 있을 수 있으니 보통 100~1000개로 설정
  • 페이징 문제 해결 -> Fetch Join의 단점인 컬렉션 페이징 OutOfMemory 문제를 해결

 

 

댓글