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 |