프로젝트를 진행하다가 드디어 N + 1을 마주치는 상황이 생겼습니다. 사실 이 문제가 정확히 언제 발생할 수 있는지 이해가 안 되었는데, 이렇게 쉽게 마주쳐서 기뻤습니다 후후
문제 상황
여기 장바구니 엔티티가 있다.
장바구니는 User, Store, Product를 @ManyToOne으로 가지고 있다.
여기서 FetchType을 명시하지 않았으므로, 자동으로 LAZY가 설정된다.
아래는 이제 Response DTO를 만들기 위해서 만든 메서드이다.
여기서 Cart와 연관된 Product, Store를 get 한다.
이제 여기서 N + 1 문제가 발생한다.
대략 아래 log는, 위 메서드가 실행되면서 만들어지는 쿼리이다.
2024-08-30T22:52:07.102+09:00 INFO 5292 --- [nio-8080-exec-1] s.j.domain.cart.service.CartService : User ID: 1의 장바구니를 조회합니다.
Hibernate:
select
c1_0.cart_id,
c1_0.cart_total_price,
c1_0.created_at,
... 중략
c1_0.user_id
from
p_carts c1_0
where
c1_0.user_id=?
and c1_0.is_deleted=false
Hibernate:
select
p1_0.product_id,
p1_0.created_at,
... 중략
from
p_products p1_0
join
p_stores s1_0
on s1_0.store_id=p1_0.store_id
left join
p_users u1_0
on u1_0.user_id=s1_0.user_id
left join
user_roles r1_0
on u1_0.user_id=r1_0.user_user_id
where
p1_0.product_id=?
쿼리가 두 개가 생겼다.
문제 원인
N + 1 문제
N+1 문제는 데이터베이스와 객체 관계 매핑(ORM) 프레임워크(예: Hibernate)에서 자주 발생하는 성능 문제 중 하나이다.
이 문제는 기본적으로 다음과 같은 방식으로 발생한다:
- 1개의 쿼리로 N개의 객체(예: 사용자 리스트)를 데이터베이스에서 조회한다.
- 조회한 각 객체에 대해 추가로 N개의 쿼리가 실행되어 연관된 다른 객체(예: 각 사용자의 주문 리스트)를 조회한다.
즉, N개의 객체를 조회할 때 1번의 메인 쿼리와 추가로 N번의 서브 쿼리가 실행되며, 결과적으로 총 N+1번의 쿼리가 발생하게 된다.
위 상황의 경우, Cart 엔티티를 조회하기 위한 1개의 쿼리와, 이 엔티티와 연관된 Product와 Store, User의 데이터를 얻기 위해 1개의 쿼리가 더 실행됐다. (단, 여기서는 @ManyToOne 연관 관계였으므로 1개만 더 쿼리 됨)
문제 해결
N + 1 문제 해결에는 여러 방법이 있다.
- JOIN FETCH 사용:
- 연관된 엔티티를 한 번의 쿼리로 함께 가져올 수 있도록 JOIN FETCH를 사용한다.
- @EntityGraph 사용:
- EntityGraph를 사용하여 특정 엔티티와 연관된 엔티티를 한 번에 로드하도록 설정할 수 있다.
- Batch Fetching
- Hibernate 설정에서 배치 단위로 fetch 하도록 활성화하여 한 번에 여러 연관된 엔티티를 가져오도록 할 수 있다.
@EntityGraph 사용
이 중에서 @EntityGraph을 사용했다. JOIN FETCH는 쿼리가 길어지고, 이게 더 가독성이 좋기 때문이다.
@EntityGraph(attributePaths = {"user", "product", "store"})
@Query("SELECT c FROM Cart c WHERE c.user.userId = :userId AND c.user.isDeleted = false")
List<Cart> findCartsByUserId(@Param("userId") Long userId);
아래는 똑같은 메서드를 실행했을 때 결과이다.
2024-08-30T23:13:01.112+09:00 INFO 26052 --- [nio-8080-exec-1] s.j.domain.cart.service.CartService : User ID: 1의 장바구니를 조회합니다.
Hibernate:
select
c1_0.cart_id,
c1_0.cart_total_price,
c1_0.created_at,
c1_0.created_by,
c1_0.deleted_at,
c1_0.deleted_by,
c1_0.is_deleted,
... 중략
from
p_carts c1_0
join
p_products p1_0
on p1_0.product_id=c1_0.product_id
join
p_stores s2_0
on s2_0.store_id=c1_0.store_id
join
p_users u2_0
on u2_0.user_id=c1_0.user_id
where
c1_0.user_id=?
and u2_0.is_deleted=false
한 번에 모든 관련 데이터를 fetch 했기 때문에 쿼리가 한 번만 만들어졌다.
마치며
항상 이론적으로만 듣던 N + 1문제를 인식하고, '이거 N + 1 문제 생기지 않나?'를 꾸준히 생각하다가, 이렇게 직접 마주쳐서 해결하니 뿌듯했다.
'Development > Diary' 카테고리의 다른 글
[Diary][Spring] N + 1 문제 해결을 위한 @EntityGraph를 사용할 때 주의할 점 (0) | 2024.09.05 |
---|---|
[Diary][Spring Security] UserDetails를 어떻게 캐싱할까? (0) | 2024.09.01 |
[Diary][Spring] Spring Security에서 Role과 Authority의 차이가 뭘까? (0) | 2024.08.29 |
[Diary][Spring] 식별,비식별 관계? 관계의 방향? 외래 키의 주인? (1) | 2024.08.27 |
[Diary] @SpringBootApplication의 @ComponentScan 범위 (0) | 2024.08.26 |