728x90
반응형
이전에 공부했던 내용에 이어서 항상 나오는 N+1의 이야기를 좀 작성해 보고자 한다. JPA 가 알아서 쿼리도 작성해 주고 다 편하다고 하는데 N+1 때문에 항상 문제가 된다며 이런저런 질문지에 오르내리곤 하는 것 같다. 이번에 N+1에 대해서 확실하게 정리해 두도록 하자.
JPA에서 N+1 문제 발생 이유와 해결 방법
1. N+1 문제란?
JPA에서 연관관계를 가진 데이터를 조회할 때, 의도치 않게 추가적인 쿼리가 다수 발생하는 문제를 N+1 문제라고 합니다.
- N+1이란?
- 1: 처음 실행하는 쿼리 (예: 회원 목록 조회)
- N: 연관된 엔티티를 조회하는 추가 쿼리 (예: 각 회원의 주문 목록 조회)
2. N+1 문제 발생 예시
예제 엔티티 (회원 & 주문 관계)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
}
N+1 문제 발생 코드
public List<Member> findAllMembers() {
return em.createQuery("SELECT m FROM Member m", Member.class).getResultList();
}
위 코드를 실행할 경우 발생하는 쿼리의 리스트
첫번째로 1
SELECT * FROM member; -- (1) 모든 회원 조회 (1개 쿼리 발생)
이제 다음에 이어지는 N
SELECT * FROM orders WHERE member_id = 1; -- (N) 각 회원별 주문 조회
SELECT * FROM orders WHERE member_id = 2;
SELECT * FROM orders WHERE member_id = 3;
→ 회원 수만큼 추가 조회 쿼리가 실행됨 → N+1 문제 발생!
3. N+1 문제 해결 방법
1) Fetch Join 사용 (가장 권장됨)
JPA의 fetch join을 사용하면 연관된 엔티티를 한 번의 SQL 조회로 함께 가져올 수 있음.
public List<Member> findAllMembersWithOrders() {
return em.createQuery(
"SELECT m FROM Member m JOIN FETCH m.orders", Member.class)
.getResultList();
}
코드 실행 시 실행되는 쿼리
SELECT m.*, o.* FROM member m
JOIN orders o ON m.id = o.member_id;
단 1개의 쿼리만 수행되며 데이터를 한 번에 가져올 수 있게 한다.
2) EntityGraph 사용
- @EntityGraph를 사용하여 fetch join을 JPA가 자동으로 수행하게 할 수 있음.
@EntityGraph(attributePaths = {"orders"})
@Query("SELECT m FROM Member m")
List<Member> findAllWithOrders();
3) Batch Size 조정 (IN 절 사용)
- @BatchSize를 활용하여 한 번의 IN 절로 여러 개의 데이터를 조회할 수 있음.
@Entity
public class Member {
@OneToMany(mappedBy = "member")
@BatchSize(size = 10)
private List<Order> orders;
}
또는 application.properties에서 설정 가능
spring.jpa.properties.hibernate.default_batch_fetch_size=100
실행되는 SQL:
SELECT * FROM orders WHERE member_id IN (1, 2, 3, ..., 10);
4) QueryDSL 활용
- QueryDSL을 사용하면 fetch join과 동적 쿼리를 쉽게 적용 가능.
public List<Member> findAllWithOrders() {
return queryFactory.selectFrom(member)
.leftJoin(member.orders, order).fetchJoin()
.fetch();
}
4. N+1 문제 발생을 줄이는 개선 방법
✅ N+1 문제 예방을 위한 체크리스트
- Lazy Loading vs. Eager Loading 설정 주의
- @OneToMany(fetch = FetchType.LAZY) 기본 사용 → 필요할 때만 조회
- @OneToMany(fetch = FetchType.EAGER)는 가급적 지양 (자동 N+1 발생 가능)
- Fetch Join 적극 활용
- JOIN FETCH 사용하여 한 번의 쿼리로 연관 데이터를 가져오기
- Hibernate Batch Fetch Size 조정
- spring.jpa.properties.hibernate.default_batch_fetch_size 설정 활용
- @BatchSize(size = N) 설정하여 IN 절로 조회
- 성능 테스트 및 SQL 로그 확인
- Hibernate SQL 로그를 활성화하여 불필요한 쿼리가 실행되는지 확인
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
5. QueryDSL, Native Query 활용
- 복잡한 쿼리는 QueryDSL 또는 Native Query로 직접 최적화
5. 결론
- N+1 문제는 Lazy Loading으로 인해 추가 쿼리가 발생하는 현상
- 해결 방법:
- fetch join (가장 추천)
- @EntityGraph (자동 fetch join)
- Batch Fetch Size 조정 (IN 절 활용)
- QueryDSL, Native Query 활용 (고급 최적화)
JPA를 사용할 때는 항상 SQL 로그를 확인하고, 필요에 따라 fetch join과 batch size 조정을 활용하여 최적화하는 것이 중요합니다! 🚀
728x90
반응형
댓글