-
N+1 문제
JPA를 사용할 때 성능 저하를 일으키는 대표적인 문제 중 하나
주로 연관된 엔티티를 조회할 때 발생
의도치 않게 너무 많은 쿼리가 실행되는 상황- 첫 번째 1: 처음 부모 엔티티를 조회하는 쿼리
- 나머지 N: 부모 엔티티와 연관된 자식 엔티티를 개별적으로 조회하는 쿼리
IF) Member 엔티티와 Order 엔티티가 @OneToMany로 연관 관계를 가진다고 가정
@Entity public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) private List<Order> orders = new ArrayList<>(); } @Entity public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String product; @ManyToOne(fetch = FetchType.LAZY) private Member member; }
List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class) .getResultList(); for (Member member : members) { System.out.println(member.getOrders().size()); // 각 회원의 주문 수를 출력 }
쿼리 동작 과정
- 부모 엔티티(Member) 조회
- 1개 쿼리
SELECT * FROM Member;
출력 예)
Hibernate: SELECT member0_.id AS id1_0_, member0_.name AS name2_0_ FROM Member member0_
- 각 부모 엔티티와 연관된 자식 엔티티(Order)를 조회
- N개 쿼리
두 번째 쿼리: 회원(Member) 조회
SELECT * FROM Order WHERE member_id = 2;
출력 예)
Hibernate: SELECT orders0_.id AS id1_1_, orders0_.product AS product2_1_, orders0_.member_id AS member_i3_1_ FROM Order orders0_ WHERE orders0_.member_id = 2
•
•
•
JPA에서 N+1 문제가 발생하면 회원 수(부모 엔티티 수)만큼 쿼리가 추가로 실행
쿼리 실행
- SELECT * FROM Member (회원 조회)
- 각 Member마다 SELECT * FROM Order WHERE member_id = ? 실행 (회원마다 주문 조회)
문제의 원인
- 지연 로딩(Lazy Loading)
- @OneToMany, @ManyToOne과 같은 연관 관계에서 기본적으로 지연 로딩을 사용하면, 연관된 엔티티를 사용할 때 추가 쿼리가 실행
- JPQL의 동작 방식
- JPQL로 부모 엔티티만 조회하면, 자식 엔티티는 로딩되지 않고 프록시로 대체
- 실제 접근 시 쿼리가 실행
해결 방법
1. 페치 조인(Fetch Join) 사용
- 부모와 자식을 한 번에 조회
List<Member> members = em.createQuery( "SELECT m FROM Member m JOIN FETCH m.orders", Member.class) .getResultList();
출력 예)
SELECT m.*, o.* FROM Member m JOIN Order o ON m.id = o.member_id;
- 한 번의 쿼리로 Member와 Order를 모두 조회
2. @EntityGraph 사용
- JPA에서 엔티티 그래프를 정의해 해결
@Entity @NamedEntityGraph( name = "Member.withOrders", attributeNodes = @NamedAttributeNode("orders") ) public class Member { ... }
List<Member> members = em.createNamedQuery("SELECT m FROM Member m", Member.class) .setHint("javax.persistence.fetchgraph", em.getEntityGraph("Member.withOrders")) .getResultList();
3. 배치 크기 설정
- JPA의 글로벌 설정 또는 연관 관계에 배치 크기를 지정
- @BatchSize 또는 hibernate.default_batch_fetch_size 설정
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY) @BatchSize(size = 100) private List<Order> orders;
- 실행 쿼리
- 처음 Member 조회 쿼리 1번
- Order 조회 쿼리 1번으로 100명에 대한 데이터를 가져옴
N+1 문제를 그대로 둔다면
- 성능 저하
- 쿼리 실행 횟수가 많아질수록 데이터베이스 부하
- 유지 보수 어려움
- 코드 변경 없이 연관된 데이터가 늘어나면 성능 문제
- 실무 적합성
- 실제 프로젝트에서는 대규모 데이터 조회가 빈번히 발생
'CS > 웹개발' 카테고리의 다른 글
TDD (0) 2025.01.23 Docker (0) 2025.01.20 CI/CD (1) 2025.01.16 JPA Dirty Checking (0) 2025.01.13 google.com을 검색했을 때 일어나는 과정 (0) 2025.01.08 댓글