JPA COUNT 쿼리 개선하기

신연우·2023년 11월 18일
0

WIL

목록 보기
9/13

배경

업무를 하던 도중 갑자기 서버의 DB CPU가 급상승하고 있다는 alert을 받게 되었습니다. DB CPU에 부하를 주고 있는 요인이 무엇인지 분석해보니 동일한 조건의 COUNT 쿼리가 짧은 시간에 반복해서 발생한 것이 원인인 것을 파악했습니다.

해당 쿼리를 실행하는 메서드를 살펴보니 반환형이 JPA의 Page<T> 형태라서 실행할 때마다 row를 가져오는 SELECT 쿼리와 함께 COUNT 쿼리도 같이 실행되고 있었습니다.

문제 해결

데이터 가져올 때 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을 상속받도록 구현합니다.

COUNT 쿼리 캐싱하기

기존에 제공하던 API에서 COUNT 쿼리의 결과를 내려주고 있었습니다. 그래서 COUNT 쿼리 값을 아예 제공하지는 않을 수 없으므로 COUNT 쿼리 결과를 캐싱하기로 했습니다.

짧은 시간에 같은 조건으로 여러 번 호출되는 것이 문제기 때문에 맨 처음에 호출될 때 한 번 성공하면 그 결과를 캐시에 넣어서 다음 번에는 DB에 쿼리를 호출하지 않고 캐시에 담겨 있는 정보를 사용하는 방법으로 쿼리 호출량을 줄였습니다.

캐시에 있는 값을 사용하면 실제 데이터와 정합이 안 맞을 수 있으므로 TTL은 짧게 30초 정도로 부여했습니다.

인사이트

  • JPA Repository 메서드의 반환형이 Page<T>면 COUNT 쿼리도 같이 실행된다. 이를 List<T>와 같이 다른 반환형으로 변경하면 COUNT 쿼리는 실행되지 않습니다.
  • JPA Specification을 사용하는 경우 별도로 SimpleJpaRepository를 상속하는 Repository를 구현하여 반환형을 변경할 수 있습니다.
  • 장기적으로는 커서 페이지네이션을 도입하거나 COUNT가 필요한 곳에서만 COUNT를 실행할 수 있도록 API를 분리해서 설계하는 것이 필요하지 않을까 싶습니다.

참고한 글

profile
남들과 함께하기 위해서는 혼자 나아갈 수 있는 힘이 있어야 한다.

0개의 댓글