배경 및 목표

특정 페이지에서 DB CPU가 50~60%까지 상승하여 전체 서비스 응답에 영향을 주고 있었습니다. 모니터링 결과, 두 가지 원인이 확인되었습니다.

  • N건을 단건 쿼리로 조회 (루프 안에서 SELECT N번 실행)
  • 3초 주기 polling에서 불필요한 COUNT 쿼리가 매번 실행

개별 쿼리는 가벼워 보이지만, 동시 접속자가 많아지면 누적되어 심각한 부하를 만들고 있었습니다.

목표

  • 조회로 인한 Reader DB CPU 부하를 낮춰 전체 서비스 응답을 안정화한다.
  • 동시 접속자가 늘어도 쿼리 수가 누적되지 않도록 조회 횟수를 최소화한다.

해결 방법과 해결 후보군

1. IN절 벌크 조회 + 메모리 groupBy

N건 단건 쿼리를 1건의 IN절 벌크 쿼리로 변환했습니다.

// ❌ 기존: N번 단건 쿼리
memberIds.forEach { id ->
    val result = repository.findById(id)  // SELECT ... WHERE id = ?
}
 
// ✅ 개선: 1번 IN절 벌크
val results = repository.findAllByIdIn(memberIds)  // SELECT ... WHERE id IN (?, ?, ...)
val grouped = results.groupBy { it.memberId }      // 메모리에서 조합

해결 후보군: 조합을 어디서 할 것인가

후보설명채택 여부
DB에서 GROUP BY쿼리 한 번에 그룹핑까지❌ DB 부하를 줄이는 것이 목적인데 연산을 다시 DB에 부담
앱 메모리 groupBy벌크 조회 후 애플리케이션에서 조합✅ 이미 조회한 데이터 가공은 추가 DB 부하 0

DB 부하를 줄이는 것이 목적이었기 때문에, 이미 조회된 데이터를 애플리케이션 메모리에서 조합하는 방식을 채택했습니다.

2. COUNT 쿼리 제거

3초 주기 polling에서 페이지네이션을 위해 실행하던 COUNT 쿼리를 컬렉션의 .size()로 대체했습니다.

// ❌ 기존: 매 polling마다 COUNT 쿼리 실행
val totalCount = repository.count(condition)  // SELECT COUNT(*) ...
 
// ✅ 개선: 이미 조회된 데이터의 크기 활용
val totalCount = results.size

polling 주기가 3초로 짧았기 때문에, 이 COUNT 쿼리가 생각보다 큰 누적 부하를 만들고 있었습니다.

graph LR
    subgraph before["❌ 기존"]
        P1["단건 x N"] --> CPU1["201개 쿼리"]
        P2["COUNT (3초 polling)"] --> CPU1
        CPU1 --> R1["CPU 50~60%"]
    end
    subgraph after["✅ 개선"]
        S1["IN절 벌크 1개"] --> CPU2["3개 쿼리"]
        S2[".size()"] --> CPU2
        CPU2 --> R2["CPU 30~40%"]
    end

결과

지표기존개선
쿼리 수201개3개 (98% 감소)
Reader CPU50~60%30~40%

모니터링

  • 2개의 reader 인스턴스 기준 평균 CPU 사용량 5060% → 3040%로 감소 확인

  • Top5 성능 병목 쿼리에서 COUNT 쿼리 완전 제거, 개선된 쿼리 요청 횟수 확인