Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20240814 N+1 문제해결 본문

개발Article

[TIL]20240814 N+1 문제해결

최밤빵 2024. 8. 14. 22:49

N+1문제는 예전에 개발일지에 여러번 간략하게 다뤘던 내용이였는데, 잘 알지는 못하고 매니저님이 이 문제를 해결해보는게 좋은 경험이 될거라고하셔서 며칠 열심히 알아봤는데, 항상 그렇듯 개념은 알고있을 수 있어도, 이걸 적용해서 문제를 해결한다거나 하는건 나에게 어려운 일이다. 그래서 다른 기술매니저님께 이걸 이해하고싶다고 말씀드렸더니 일단 억지로 문제를 발생시키면 알수있을거라고 하셔서 억지로 문제를 발생시켜보긴 했다..! 

N+1 문제는 데이터베이스 쿼리 최적화와 관련하여 자주 언급되는 문제로, 특히 JPA를 사용하는 애플리케이션에서 많이 발생한다. N+1 문제는 의도치 않게 많은 수의 SQL 쿼리가 실행되는 문제를 의미하며, 이는 애플리케이션 성능을 크게 저하시킬 수 있다. 

🤓N+1 문제해결

N+1 문제란 데이터베이스에서 1개의 쿼리로 N개의 결과를 가져왔을 때, 각 결과에 대해 추가로 1개의 쿼리가 발생하는 상황을 말한다. 즉, 1개의 쿼리로 조회된 엔티티들이 N개라면, N개의 추가 쿼리가 실행되는 것을 의미한다. 이 문제는 성능 저하를 유발하며, 특히 데이터가 많을수록 심각한 성능 문제가 발생할 수 있다.

 

▶ N+1 문제 발생 원인

N+1 문제는 주로 엔티티를 조회할 때 연관된 엔티티를 지연 로딩(Lazy Loading) 방식으로 가져올 때 발생한다. JPA에서는 연관 관계가 설정된 엔티티를 기본적으로 지연 로딩 방식으로 가져오는데, 이때 연관된 엔티티를 참조할 때마다 추가 쿼리가 발생한다.

 

▶예시: Order와 OrderItem 간의 N+1 문제

지금 문제를 발생시키기 위해 Order와 OrderItem 엔티티 간의 관계를 만들어냈다. Order는 여러 개의 OrderItem을 포함할 수 있다. 만약 모든 Order를 조회하면서 각 Order에 속한 OrderItem들을 가져오려고 할 때, 다음과 같은 N+1 문제가 발생 수 있다.

// Order 엔티티
@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String orderNumber;

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> orderItems = new ArrayList<>();
}
// OrderItem 엔티티
@Entity
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String itemName;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
}
// N+1 문제가 발생하는 코드 예시
List<Order> orders = orderRepository.findAll(); // 1개의 쿼리 실행

for (Order order : orders) {
List<OrderItem> orderItems = order.getOrderItems(); // N개의 추가 쿼리 실행
}

→ 이 경우, orderRepository.findAll()로 모든 Order를 조회하는 쿼리 1개가 실행된다. 하지만 for 루프 안에서 각 Order의 orderItems를 접근할 때마다 추가 쿼리가 발생하여, 결과적으로 N개의 추가 쿼리가 실행된다. 이를 N+1 문제라고 한다.

 

▶ N+1 문제 해결 방법

N+1 문제를 해결하기 위해서는 여러 가지 방법이 있다. 각각의 방법에는 장단점이 있으며, 적절한 상황에 맞게 사용해야 한다.

 

▷ 페치 조인(Fetch Join)

페치 조인은 연관된 엔티티들을 한 번의 쿼리로 가져오는 방법이다. 이를 통해 N+1 문제를 해결할 수 있다. JPA에서는 @Query 어노테이션을 사용하거나 JPQL을 통해 페치 조인을 사용할 수 있다.

// Fetch Join을 사용한 해결 방법
@Query("SELECT o FROM Order o JOIN FETCH o.orderItems")
List<Order> findAllWithOrderItems();

 

→ 이렇게 하면 Order와 OrderItem을 한 번의 쿼리로 가져오게 된다.

 

장점:

쿼리 수가 줄어들어 성능이 향상된다.

한 번의 쿼리로 필요한 모든 데이터를 가져올 수 있다.

단점:

복잡한 쿼리가 생성될 수 있으며, 페이징 처리가 불가능할 수 있다.

데이터 양이 많을 경우 오히려 성능에 영향을 줄 수 있다.

 

▷엔터티 그래프(Entity Graph)

엔터티 그래프를 사용하여 특정 엔티티의 연관 관계를 명시적으로 지정할 수 있다. JPA는 엔터티 그래프를 통해 지연 로딩을 무시하고 페치 조인을 수행한다.

// 엔터티 그래프를 사용한 해결 방법
@Entity
@NamedEntityGraph(name = "Order.withItems", attributeNodes = @NamedAttributeNode("orderItems"))
public class Order {
    // 엔티티 필드 및 메서드
}

// 엔터티 그래프를 사용하는 메서드
@Query("SELECT o FROM Order o")
@EntityGraph("Order.withItems")
List<Order> findAllWithItemsUsingEntityGraph();

 

장점:

코드 가독성이 높아지고 유지보수가 쉬워진다.

필요한 경우에만 명시적으로 페치 전략을 지정할 수 있다.

단점:

설정이 복잡할 수 있으며, 여러 엔티티를 다룰 때 관리가 어려울 수 있다.

 

▷ BatchSize 설정

@BatchSize 애너테이션을 사용하거나 JPA 설정 파일에 hibernate.default_batch_fetch_size 속성을 지정하여, 한 번에 로드할 엔티티의 수를 지정할 수 있다. 이를 통해 N+1 문제를 일부 해결할 수 있다.

// BatchSize를 사용한 해결 방법
@Entity
@BatchSize(size = 10)
public class Order {
    // 엔티티 필드 및 메서드
}

 

장점:

페이징을 지원하면서도 쿼리 성능을 최적화할 수 있다.

비교적 설정이 간단하다.

단점:

데이터베이스 드라이버와 설정에 따라 성능에 차이가 발생할 수 있다.

최적의 BatchSize 값을 찾기 어려울 수 있다.

 

▶해결 방법에 대한 예시 코드와 결과

위에서 설명한 각 해결 방법의 코드 예시와 함께 해결이 어떻게 되었는지 설명하자면,

 

▷ Fetch Join 예시

@Query("SELECT o FROM Order o JOIN FETCH o.orderItems")
List<Order> findAllWithOrderItems

→ 이 코드를 실행하면 단일 쿼리로 모든 Order와 연관된 OrderItem을 가져온다. 결과적으로 N+1 문제를 해결할 수 있으며, 데이터베이스에 대한 쿼리 수가 1개로 줄어들어 성능이 크게 향상된다.

 

▷엔터티 그래프 예시

@Query("SELECT o FROM Order o")
@EntityGraph("Order.withItems")
List<Order> findAllWithItemsUsingEntityGraph();

→ 이 코드는 Order와 연관된 OrderItem을 엔터티 그래프를 사용하여 페치한다. 실행 결과 N+1 문제가 해결되며, 필요할 때만 명시적으로 페치 전략을 지정할 수 있어 유연하다.

 

▷ BatchSize 설정 예시

@Entity
@BatchSize(size = 10)
public class Order {
    // 엔티티 필드 및 메서드
}

→ BatchSize를 설정한 경우, 1개의 메인 쿼리와 OrderItem에 대해 추가로 몇 개의 배치 쿼리가 실행된다. N개의 개별 쿼리 대신 몇 개의 배치 쿼리로 최적화할 수 있으며, 페이징 기능도 사용할 수 있다.

 

▶ 정리

 N+1 문제는 성능에 큰 영향을 미치므로, 적절한 해결 방법을 선택하는 것이 중요하다. 페치 조인, 엔터티 그래프, BatchSize 등 다양한 해결 방법을 직접 적용해보면서 각각의 장단점을 체감할 수 있었다. 기술 매니저님의 조언으로 현재 진행 중인 프로젝트에 N+1 문제를 억지로 발생시키고 이를 해결하는 과정을 통해, 이 문제가 실제로 어떤 상황에서 발생하는지, 또 어떻게 해결될 수 있는지를 실감하며 배울 수 있을줄 알았는데.. 왜 나는지도 알겠고 어디서 해결이 된 걸 보는건지도 아직 모르겠다 ㅠㅠㅠ.... 언제 감잡지..?

'개발Article' 카테고리의 다른 글

[TIL]20240816 제이미터와 N+1문제  (0) 2024.08.16
[TIL]20240815 스프링 스케줄러  (0) 2024.08.15
[TIL]20240813 Nginx  (0) 2024.08.13
[TIL]20240812 성능최적화  (0) 2024.08.12
[WIL]20240811 Redis  (0) 2024.08.11