[Spring/Springboot] N+1 문제 해결 방안 - Batch Size/2차 캐시/Subselect Fetching/DTO
2024. 12. 22. 07:17ㆍCS/Spring
1. Batch Size 설정
- Hibernate에서는 @BatchSize 어노테이션이나 hibernate.default_batch_fetch_size 설정을 통해 배치 크기(Batch Size)를 지정하여 N+1 문제를 해결할 수 있습니다.
- 배치 크기를 설정하면 한 번에 가져올 연관 엔티티의 개수를 지정하여, 지연 로딩(Lazy Loading)을 사용할 때도 여러 엔티티를 한 번에 가져올 수 있습니다.
@Entity
public class Review {
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 10) // 한번에 10개씩 로딩
private Store store;
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 10)
private Member member;
}
- 위와 같이 @BatchSize(size = 10)을 설정하면, 10개의 Review 엔티티를 조회할 때 연관된 Store와 Member 엔티티도 각각 10개씩 한 번에 가져오게 됩니다.
- 즉, 10개의 Review가 필요한 Store 엔티티 10개를 한 번에 로딩하기 때문에 추가 쿼리가 줄어듭니다.
- 장점
- N+1 문제를 해결하면서도 지연 로딩(Lazy Loading)의 유연성을 유지할 수 있습니다.
- 코드 수정 없이 엔티티 설정만으로 적용할 수 있습니다.
- 단점
- 배치 크기 설정이 적절하지 않으면 오히려 메모리 사용량이 증가할 수 있습니다.
2. Secondary Cache (2차 캐시) 사용
- JPA는 기본적으로 1차 캐시(영속성 컨텍스트 수준 캐시)만 사용하지만, 2차 캐시를 추가로 설정하면 데이터베이스 조회 횟수를 줄여 N+1 문제를 해결하는 데 도움이 될 수 있습니다.
- 설정 방법
- Hibernate의 경우, 2차 캐시를 활성화하고 엔티티에 캐시를 적용하는 방식으로 설정할 수 있습니다
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Store {
// 엔티티 필드
}
- @Cacheable과 @Cache 어노테이션을 사용하면 해당 엔티티가 2차 캐시에 저장됩니다. 이후 조회할 때는 캐시에 저장된 데이터를 우선적으로 조회하므로 데이터베이스 쿼리를 줄일 수 있습니다.
- 장점
- 빈번히 조회되는 데이터를 캐시에 저장하여 데이터베이스 부하를 줄일 수 있습니다.
- 데이터가 변경되지 않는 읽기 전용 엔티티에 적합합니다.
- 단점
- 데이터 변경이 빈번한 경우 캐시 갱신으로 인해 오히려 성능에 악영향을 미칠 수 있습니다.
- 캐시 설정이 복잡하고 관리가 필요합니다.
3. Subselect Fetching (서브 셀렉트 패칭)
- Hibernate의 @Fetch(FetchMode.SUBSELECT)를 사용하면, 특정 엔티티를 조회할 때 필요한 연관 엔티티들을 한 번의 서브 쿼리로 가져오는 방식으로 N+1 문제를 해결할 수 있습니다.
@Entity
public class Review {
@ManyToOne(fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
private Store store;
@ManyToOne(fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
private Member member;
}
- FetchMode.SUBSELECT는 한 번에 조회하는 메인 엔티티 개수가 일정 이상일 때, 그 연관 엔티티들을 서브 쿼리로 가져옵니다.
- 예를 들어, Review 엔티티를 여러 개 조회하면 Store와 Member를 각각 서브 쿼리로 한 번에 로드합니다.
- 장점
- N+1 문제를 해결하고 불필요한 데이터베이스 쿼리를 줄일 수 있습니다.
- 조회 성능을 최적화할 수 있습니다.
- 단점
- 특정 상황에서만 효과적입니다. 연관 엔티티 수가 너무 적거나 많은 경우에는 성능 저하가 발생할 수 있습니다.
4. DTO를 사용한 직접 조회 (JPA Projection)
public interface ReviewRepository extends JpaRepository<Review, Long> {
@Query("SELECT new com.example.dto.ReviewDto(r.id, r.body, s.name, m.name) " +
"FROM Review r " +
"JOIN r.store s " +
"JOIN r.member m " +
"WHERE s.name = :storeName")
List<ReviewDto> findReviewDtoByStoreName(@Param("storeName") String storeName);
}
- 위 코드에서 ReviewDto는 필요한 필드만 포함한 DTO입니다.
- JOIN 구문을 사용하여 연관 엔티티를 조인하지만, DTO로 필요한 필드만 선택하여 가져옵니다.
- 이를 통해 Review의 연관된 Store와 Member 필드를 로딩하지 않고 필요한 데이터만 가져와 N+1 문제를 방지할 수 있습니다.
- 장점
- 데이터베이스에서 필요한 필드만 가져오므로 성능 최적화에 매우 효과적입니다.
- 엔티티와 DTO를 분리하여 더 명확한 쿼리 구성이 가능합니다.
- 단점
- 모든 데이터를 엔티티가 아닌 DTO로 가져오므로, 영속성 컨텍스트의 관리 대상에서 벗어납니다.
- 유지보수가 어려울 수 있으며, 코드량이 많아질 수 있습니다.
'CS > Spring' 카테고리의 다른 글
[Spring/Springboot] Spring Data JPA - Paging/Slice (0) | 2024.12.23 |
---|---|
[Spring/Springboot] @RestControllerAdvice/API 응답 통일/에러 핸들러 (0) | 2024.12.22 |
[Spring/Springboot] QueryDSL이란? (0) | 2024.12.22 |
[Spring/Springboot] @EntityGraph (0) | 2024.12.22 |
[Spring/Springboot] Fetch Join (0) | 2024.12.15 |