업무를 하던 도중 갑자기 서버의 DB CPU가 급상승하고 있다는 alert을 받게 되었습니다. DB CPU에 부하를 주고 있는 요인이 무엇인지 분석해보니 동일한 조건의 COUNT 쿼리가 짧은 시간에 반복해서 발생한 것이 원인인 것을 파악했습니다.
해당 쿼리를 실행하는 메서드를 살펴보니 반환형이 JPA의 Page<T>
형태라서 실행할 때마다 row를 가져오는 SELECT 쿼리와 함께 COUNT 쿼리도 같이 실행되고 있었습니다.
우선 불필요한 COUNT 쿼리가 실행되고 있으므로 COUNT 쿼리가 실행되지 않게 만드는 것이 첫 번째로 해결해야 하는 문제였습니다. JPA를 사용하는 경우 반환형이 Page<T>
가 아니라면 COUNT 쿼리가 실행되지 않으므로 반환형을 변경하려고 했습니다.
그런데 해당 메서드에서 JPA의 Specification을 사용하고 있었습니다. JpaSpecificationExecutor
을 보면 findAll
메서드가 모두 Page<T>
를 반환하는 식으로 구현되어 있습니다. 그래서 반환형을 List로 바꿀 수 있는 방법을 찾아봤습니다.
@NoRepositoryBean
interface JpaSpecToListRepository<T> {
fun findAllAsList(spec: Specification<T>, pageable: Pageable): List<T>
}
@NoRepositoryBean
class JpaSpecToListRepositoryImpl<T, ID> : SimpleJpaRepository<T, ID>, JpaSpecToListRepository<T> {
constructor(domainClass: Class<T>, em: EntityManager) : super(domainClass, em)
constructor(entityInformationJpaEntityInformation<T, *>, em: EntityManager) : super(entityInformation, em)
@Transactional(readOnly = true)
override fun findAllAsList(spec: Specification<T>, pageable: Pageable): List<T> {
return findAll(spec)
}
}
위와 같이 SimpleJpaRepository
와 새로 만든 JpaSpecToListRepository
를 구현하는 구현체를 하나 만들어주면 된다.
@EnableJpaRepository(
repositoryBaseClass = JpaSpecToListRepositoryImpl::class
)
이후 @EnableJpaRepository
어노테이션을 통해 JPA Repository 빈들이 모두 JpaSpecToListRepositoryImpl
을 상속받도록 구현합니다.
기존에 제공하던 API에서 COUNT 쿼리의 결과를 내려주고 있었습니다. 그래서 COUNT 쿼리 값을 아예 제공하지는 않을 수 없으므로 COUNT 쿼리 결과를 캐싱하기로 했습니다.
짧은 시간에 같은 조건으로 여러 번 호출되는 것이 문제기 때문에 맨 처음에 호출될 때 한 번 성공하면 그 결과를 캐시에 넣어서 다음 번에는 DB에 쿼리를 호출하지 않고 캐시에 담겨 있는 정보를 사용하는 방법으로 쿼리 호출량을 줄였습니다.
캐시에 있는 값을 사용하면 실제 데이터와 정합이 안 맞을 수 있으므로 TTL은 짧게 30초 정도로 부여했습니다.
Page<T>
면 COUNT 쿼리도 같이 실행된다. 이를 List<T>
와 같이 다른 반환형으로 변경하면 COUNT 쿼리는 실행되지 않습니다.SimpleJpaRepository
를 상속하는 Repository를 구현하여 반환형을 변경할 수 있습니다.