728x90
개요
정산 세부 내역 리스트를 가져오는 JPA 코드를 작성하던 중, 성능 문제를 발견했습니다.
@Query("SELECT sd FROM SettlementDetail sd WHERE sd.settlement.providerId = :providerId AND sd.settlement.settlementTime = :settlementTime AND sd.isDeleted = false")
List<SettlementDetail> findByProviderIdAndSettlementTimeAndIsDeletedFalse(@Param("providerId") Long providerId, @Param("settlementTime") Long settlementTime);
// 초기 쿼리
select ... from p_settlement_details sd1_0
join p_settlements s1_0 on s1_0.settlement_id=sd1_0.settlement_id
where s1_0.provider_id=? and s1_0.settlement_time=? and sd1_0.is_deleted=false
// 추가 쿼리
select ... from p_payments p1_0 where p1_0.payment_id = any (?)
// 환불 정보 조회
select ... from p_refunds r1_0 where r1_0.payment_id=?
그러나 로그를 보면 위와 같이 N+1문제가 발생했습니다.
- N+1 문제
- SettlementDetail 리스트를 가져온 후, 각 SettlementDetail에 대해 Payment와 Refund 정보를 개별적으로 조회하고 있습니다.
- 성능 저하
- 다수의 추가 쿼리로 인해 전체적인 조회 성능이 저하될 수 있습니다.
따라서 @Query 어노테이션을 사용한 JPQL이 아닌 Query DSL을 사용해야 했습니다. CustomRepository, custom을 구현한 구현체 Impl을 구성했습니다.
문제 상황
Spring Boot 애플리케이션에서 SettlementDetail 엔티티에 대한 커스텀 쿼리를 구현하려 했으나, 애플리케이션 시작 시 다음과 같은 오류가 발생했습니다.
Caused by: org.springframework.data.mapping.PropertyReferenceException: No property 'providerId' found for type 'SettlementDetail'
at app//org.springframework.data.mapping.PropertyPath.<init>(PropertyPath.java:94)
at app//org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:455)
...
또한 gradle 초기화 후 빌드하면 다음과 같은 에러 메시지가 발생했습니다.
PaymentApplicationTests > contextLoads() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:795
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:795
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:795
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:795
Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1806
Caused by: org.springframework.data.repository.query.QueryCreationException at QueryCreationException.java:101
Caused by: java.lang.IllegalArgumentException at PartTreeJpaQuery.java:107
Caused by: org.springframework.data.mapping.PropertyReferenceException at PropertyPath.java:94
JPA 리포지토리 메서드가 엔티티 클래스에 존재하지 않는 속성에 접근해서 PropertyReferenceException가 발생했습니다.
해결 시도
- @NoRepositoryBean 어노테이션을 SettlementDetailCustom 인터페이스에 추가해 보았으나
문제가 해결되지 않았습니다. - 커스텀 구현 클래스의 이름을 SettlementDetailCustomImpl에서 SettlementDetailRepositoryImpl로 변경했습니다.
@NoRepositoryBean은 기본적인 레포지토리 인터페이스를 상속해 재사용할 목적으로 사용되며, 해당 인터페이스가 직접인스턴스화되지 않도록 방지합니다.
이를 통해 중복 코드를 줄이고, 공통 기능을 상속받아 여러 리포지토리에서 쉽게 사용할 수 있도록 도와줍니다.
커스텀 구현 클래스의 이름을 SettlementDetailRepositoryImpl로 변경한 후 문제가 해결되었습니다.
배운점
Spring Data JPA의 명명 규칙
- 리포지토리 인터페이스 이름 + "Impl"의 형태로 구현 클래스 이름을 지정해야 자동으로 인식됩니다.
- 올바른 명명 규칙을 따르면 별도의 설정 없이 커스텀 구현체가 자동으로 인식됩니다.
- 명명 규칙을 따르지 않으면 Spring이 커스텀 구현을
인식하지 못합니다. - 인터페이스와 구현 클래스의 이름이 정확히 일치해야 합니다(대소문자 구분).
- 기본적으로 구현 클래스는 리포지토리 인터페이스와 같은 패키지에 위치해야 합니다.
- 기본 접미사 "Impl" 대신 다른 접미사를 사용하고 싶다면 다음과 같이 설정할 수 있습니다.
- @EnableJpaRepositories(repositoryImplementationPostfix = "CustomImpl")
'TIL,일일 회고' 카테고리의 다른 글
[TIL, 일일 회고] 2024.10.19 - 데이터베이스 스키마 추가하기 (0) | 2024.10.19 |
---|---|
[TIL, 일일 회고] 2024.10.18 - 서버 사이드 렌더링 적용하기 (0) | 2024.10.18 |
[TIL, 일일 회고] 2024.10.16 - 1 대 N VS 중간 테이블 고민 (0) | 2024.10.16 |
[TIL, 일일 회고] 2024.10.15 - 테이블에서 컬럼 추가하기 (IntelliJ) (0) | 2024.10.15 |
[TIL, 일일 회고] 2024.10.14 - Grafana에서 CPU 사용량이 50% 이상일 때 자동으로 Slack에 알림이 전송하기 (0) | 2024.10.14 |