Spring Data JPA를 사용할 때 흔히 발생하는 N+1 문제의 원인을 파악하고, 다양한 해결 방법을 통해 성능을 최적화하는 방법을 알아봅니다.
JPA에서 엔티티 간의 관계를 설정할 때, 기본적으로 지연 로딩(Lazy Loading) 전략을 사용합니다. 이는 연관된 엔티티가 실제로 사용될 때까지 데이터베이스에서 로딩하지 않는다는 의미인데요. 문제는 하나의 쿼리로 부모 엔티티를 조회하고, 이후에 각각의 자식 엔티티에 접근할 때 발생합니다.
예를 들어, Team과 Member 엔티티가 있고, 하나의 Team에 여러 Member가 속해 있다고 가정해 볼게요.
java @Entity public class Team { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name;
@OneToMany(mappedBy = "team")
private List<Member> members;
}
@Entity public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
Team 엔티티를 조회한 후, 각 Team에 속한 Member들을 순회하며 접근하면, 각 Member를 로딩하기 위해 추가적인 쿼리가 발생합니다. Team의 수가 N개라면, Member를 조회하기 위한 쿼리는 N번 실행되겠죠. 최초 Team 조회 쿼리 1번 + Member 조회 쿼리 N번, 그래서 N+1 문제라고 부르는 겁니다.
다음과 같은 코드가 N+1 문제를 유발할 수 있습니다.
java List teams = teamRepository.findAll(); // 1개의 쿼리 (Team 조회) for (Team team : teams) { System.out.println(team.getMembers().size()); // 각 Team마다 쿼리 발생 (N개의 쿼리) }
가장 일반적이고 효과적인 해결 방법은 Fetch Join을 사용하는 것입니다. JPQL에서 JOIN FETCH 구문을 사용하여 연관된 엔티티를 한 번에 로딩할 수 있습니다.
java @Query("SELECT t FROM Team t JOIN FETCH t.members") List findAllWithMembers();
위 코드는 Team과 Member를 한 번의 쿼리로 모두 가져오기 때문에 N+1 문제를 해결할 수 있습니다.
EntityGraph는 JPA 2.1부터 지원하는 기능으로, 특정 엔티티를 조회할 때 함께 로딩할 연관 엔티티를 지정할 수 있습니다.
java @EntityGraph(attributePaths = "members") @Query("SELECT t FROM Team t") List findAllWithMembersUsingEntityGraph();
@EntityGraph 어노테이션을 사용하여 members 속성을 함께 로딩하도록 지정했습니다.
하이버네이트(Hibernate)에서는 hibernate.default_batch_fetch_size 설정을 통해 배치 사이즈를 지정할 수 있습니다. 이 설정을 사용하면, 지정된 사이즈만큼 한 번에 연관 엔티티를 로딩하여 N+1 문제를 완화할 수 있습니다.
application.properties 또는 application.yml 파일에 다음과 같이 설정합니다.
properties spring.jpa.properties.hibernate.default_batch_fetch_size=100
이 설정은 IN 쿼리를 사용하여 지정된 크기만큼 연관 엔티티를 로딩합니다.
@BatchSize 어노테이션을 사용해서 특정 엔티티 관계에 대한 배치 사이즈를 설정할 수도 있습니다.
java @Entity public class Team { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name;
@OneToMany(mappedBy = "team")
@BatchSize(size = 100)
private List<Member> members;
}
각 해결 방법은 상황에 따라 장단점이 있습니다.
| 해결 방법 | 장점 | 단점 |
|---|---|---|
| Fetch Join | 가장 직관적이고 효과적 | 복잡한 쿼리에는 사용하기 어려울 수 있음 |
| EntityGraph | 유연하게 연관 엔티티 지정 가능 | 쿼리가 복잡해질 수 있음 |
| Batch Size | 기존 코드 수정 없이 적용 가능 | 데이터 양에 따라 성능 개선 효과가 다를 수 있음 |
일반적으로는 Fetch Join을 사용하는 것이 가장 간단하고 효과적입니다. 하지만 복잡한 쿼리에서는 EntityGraph나 Batch Size를 고려해볼 수 있습니다.
Spring Data JPA를 사용하면서 N+1 문제는 피할 수 없는 숙제와 같아요. 하지만 오늘 소개한 다양한 해결 방법을 통해 성능을 크게 향상시킬 수 있습니다. 프로젝트의 상황에 맞는 최적의 방법을 선택해서 적용해보세요. N+1 문제 해결, 이제 더 이상 두려워하지 않아도 되겠죠?
| Spring Batch 입문 가이드 (0) | 2026.02.24 |
|---|---|
| Spring AOP 활용법 (0) | 2026.02.24 |
| Spring Security JWT 인증 구현 (0) | 2026.02.23 |
| Spring Boot 3 새로운 기능 정리 (0) | 2026.02.23 |
| JWT Access Token과 Refresh Token 완벽 이해하기 (1) | 2025.12.13 |