이전 포스팅에서 톰캣 스레드 풀을 늘리기 전에, 병목 지점은 데이터베이스 커넥션을 획득하는 부분에 있다는 것을 파악했었습니다.
이번 포스팅에서는 데이터베이스 커넥션 풀의 크기를 조절과 캐시를 적용하는 각각의 과정을 담았습니다.
데이터베이스 커넥션 풀 크기 조절하기
HikariCP 설정 값 변경하기

커넥션 풀 크기를 조절하는 방법은 이미지처럼 application.yml
에서 간단하게 설정할 수 있습니다.
hikariCP 설정 옵션에 대한 설명입니다.
- maximum-pool-size: 커넥션 풀의 최대 크기를 지정합니다. 커넥션 풀 크기만큼 커넥션이 담기면 idle 상태의 커넥션은 존재하지 않게 된다고 합니다.
- minimum-idle(default: same as maximumPoolSize): 커넥션 풀에 idle 상태로 있을 커넥션의 최소 개수를 지정합니다. 위에서 설명했듯이 서버에 부하가 많을 때 새 커넥션을 획득하는 것은 서버에 더 큰 부담으로 이어져 역효과를 일으킵니다.
따라서 HikariCP는최고의 성능
을 내기 위해선maximum-pool-size
값과 동일한 값으로 설정해야 한다고 권장합니다.
(기본값은 maximum-pool-size으로 되어 있기 때문에 풀 크기가 증가하면 같이 증가합니다.)
참고로 이전 포스팅에서 설명했듯이 HikariCP 설정을 통해 최대 사이즈를 늘려도 그 개수만큼 즉시 증가시키지 않습니다. 커넥션을 열고 닫을 때 컴퓨팅 자원이 사용되기 때문에 HikariCP가 실제 사용량에 맞춰서 점진적으로 늘립니다.
만약 즉시 커넥션 풀에 커넥션들을 채워야 한다면 minimum-idle
값을 maximum-pool-size
값과 동일하게 설정해야 합니다.
커넥션 풀 크기: 5개로 설정
CPU 2개, 8GB 메모리 스펙의 서버에선 최적의 커넥션 풀 크기는 5(=2 * 2 + 1)입니다.
최적이라는 값으로 설정했을 때 응답 속도는 어떻게 됐을까요?

커넥션 풀 크기를 기본값(10개)에서 절반으로 줄이니 오히려 커넥션을 대기하는 요청 수가 117개 => 189개
로 61.5%(= (189 - 117) / 117)
만큼 많아졌습니다.
이유를 알기 위해 커넥션 사용 시간과 커넥션 획득 시간을 봅시다.

이전 포스팅에서 설명했듯이, SSD를 사용할 땐 I/O 작업에 의한 스레드의 차단이 줄어들기 때문에 커넥션 풀 크기를 CPU 코어 개수와 근접하게 설정할수록 처리 속도가 더 빨라진다고 했습니다.
결과를 보니 이전 값 50ms => 31ms
정도로 38% 감소해서 실제로 더 빠른 처리하는 것을 확인할 수 있었습니다.
하지만, 커넥션 풀 크기가 줄은 만큼 커넥션 획득 시간은 790ms => 1004 ms
정도로 26%
늘어나서 전체 응답 시간은 더 느려졌습니다.
핀포인트로 가장 느린 응답을 한 트랜잭션을 볼까요?

가장 느린 응답 속도는 2900ms => 3972ms
로 커넥션 풀 크기가 10개일 때보다 36% 느려진 것을 확인할 수 있었습니다.
따라서 커넥션 풀의 크기를 무작정 CPU 코어 개수에 맞춘다고 해결되는 것은 아닌 것으로 결론을 낼 수 있습니다.
커넥션 풀 크기: 20개로 설정
그렇다면 이번엔 커넥션 풀의 크기를 기본값에서 2배로 늘리면 어떻게 될까요?
위의 결과와 정 반대로 CPU 코어 개수보다 훨씬 많은 수의 연결은 시분할 시스템에 의해 스레드 처리가 더 느려질 것으로 예상할 수 있습니다.
한 마디로 커넥션 사용 시간은 늘어나지만, 커넥션 획득 시간을 줄어들 것입니다.

커넥션 풀 크기를 20으로 조정하니 10개일 때 보단 117개 => 168개
로 43.5%
만큼 많아졌습니다.
5개일 때보단 20개가 189개 => 168개
로 11%
정도 줄어들었습니다.
확실히 부하가 많을 때는 커넥션 풀 크기를 늘리는 것이 조금 더 낫다고 볼 수 있겠네요.

커넥션 사용 시간은 기본값(10개) 일 때보다 20개일 때가 50ms => 117ms
로 134% 느려졌습니다. 예상대로 시분할 시스템에 의해 스레드 수가 적을 때보다 많을 때가 더 느리다는 것을 명확하게 보여줍니다.
커넥션 획득 시간은 790ms => 880ms
로 11% 느려졌습니다.
요약하면 사용 시간은 늘었지만 획득 시간은 커넥션 풀 크기가 5개일 때보단 20개일 때가 1004ms => 880ms
14% 빨라진 것을 알 수 있었습니다.

가장 느린 응답 속도는 2900ms => 2748ms
로 커넥션 풀 크기가 10개일 때보다 5%
빨라진 것을 확인할 수 있습니다.
커넥션 풀 크기가 5개일 때보단 3972ms => 2748ms
로 30%
빨라졌습니다.
결론적으로 커넥션 풀 크기를 2배로 늘렸을 때 가장 느린 응답 속도가 5% 빨라진 것을 봤을 때만 보면, 커넥션 풀 크기의 조절은 유의미한 결과를 냈다고 보기 어렵습니다.
캐시에 대해 알아보기
캐시를 적용하면 여러 번 조회할 때 매번 데이터베이스 커넥션을 기다릴 필요는 없습니다.
처음 요청은 캐시 미스로 인해 응답 속도가 느려도, 이후 반복되는 요청에는 메모리에 캐시 된 응답을 반환하기 때문에 데이터베이스 접근 자체를 줄이게 됩니다.
현재 시스템은 분산 서버 환경이 아닌, 단일 서버 환경이므로 로컬 캐시에 대해 알아보겠습니다.
캐시

스프링에서 기본으로 지원하는 여러 종류의 캐시들은 Cache
인터페이스의 구현체들인데요,
Cache
인터페이스를 통해 추상화가 잘 되어있기 때문에 구현 기술에 종속적이지 않다는 장점이 있습니다.
public interface Cache {
String getName();
@Nullable
ValueWrapper get(Object key);
void put(Object key, @Nullable Object value);
void evict(Object key);
void clear();
/**
* A (wrapper) object representing a cache value.
*/
@FunctionalInterface
interface ValueWrapper {
/**
* Return the actual value in the cache.
*/
@Nullable
Object get();
}
// ...
}
캐시 인터페이스에서 메서드 몇 개만 소개하겠습니다.
getName()
: 캐시의 이름을 가져옵니다.get()
:ValueWrapper
클래스를 통해 캐시에서key
에 해당하는value
를 가져옵니다.put()
:key - value
로 캐시를 저장할 때 사용합니다.evict()
:key
를 통해 매핑되는특정 캐시
를 제거할 때 사용합니다.clear()
: 캐시 된 모든 데이터들을 제거합니다.

그림으로 표현하면 캐시에는 이름
이 있고 해당 이름의 캐시에는 key-value
구조로 저장됩니다.
예를 들어 유저 별로 특정 응답을 캐시해야 한다면, key
에는 유저의 아이디(userId), value
에는 캐시 할 데이터를 저장합니다.
그리고 이런 종류의 캐시에 myCache
라는 이름을 붙여서 Cache
객체로 감싼 것입니다.
캐시 매니저
위의 캐시 객체를 다루는 캐시 매니저(CacheManager)에 대해서도 알아보겠습니다.

스프링에서 지원하는 캐시 매니저도 CacheManager
인터페이스로 제공을 하는데요,
구현 기술에 따라서 사용할 수 있는 CacheManager 구현체들이 여러 개가 있습니다.
대표적으로 Caffeine
캐시를 사용하는 CaffeineCacheManager
, 여러 CacheManager
를 혼용해서 사용할 수 있는 CompositeCacheManager
가 있습니다.

CacheManager
인터페이스는 간단하게 getCache()
, getCacheNames()
로 되어있습니다.


InMemory
캐시를 사용하는 CaffeineCacheManager
, SimpleCacheManager
는 ConcurrentHashMap
을 사용해서 구현되어 있습니다.
기본적으로 캐시를 저장하는 ConcurrentHashMap
에는 최대 16개의 Cache
객체만 저장할 수 있으며 캐시를 찾을 때는 문자열로 된 캐시 이름으로 찾을 수 있습니다.

그림으로 보면 CacheManager
는 여러 Cache
객체를 관리합니다.
Cache
들은 사용자 정보
를 캐시 하거나 인기 뉴스
같은 데이터를 캐시 하는 다양하게 존재할 수 있는 거죠.
스프링에서 캐시가 동작하는 방식
스프링에서는 AOP가 적용된 @Cacheable
, @CachePut
, @CacheEvict
등 애너테이션을 이용해 선언적으로 캐싱 동작을 처리할 수 있는데요,
구체적으로 어떻게 동작하는지 간단하게 알아보겠습니다.
CacheAspectSupport
:@Cacheable
같은 애너테이션을 파싱하고, 애너테이션이 붙어있는 메서드의 처리를 담당합니다.CacheInterceptor
:CacheAspectSupport
의 자식 클래스로CacheAspectSupport
의 부모 클래스인AbstractCacheInvoker
에서get
,put
,evict
등 호출을 담당합니다.
위 두 클래스에 의해 선언적으로 캐싱이 동작하는 과정은 아래와 같습니다.
@Cacheable
같은 애너테이션이 붙은 메서드가 호출되면CacheAspectSupport
내부에서CacheManager(CacheResolver)
를 통해Cache
를 찾고Cache
를 찾으면 key로 요청된 연산(get, put, evict)을CacheInterceptor
가 수행합니다.
(key에 SpEL을 파싱하는 부분은CacheAspectSupport
가 처리)
로컬 캐시 비교 (EhCache vs Caffeine Cache)
로컬 캐시로 사용할 수 있는 몇 가지 구현체들 중 EhCache와 Caffeine Cache를 알아보겠습니다.
- EhCache
- 직렬화된 데이터 객체를 저장하는 방식의
In-Memory
캐시 구현체입니다. - OffHeap 공간(JVM GC가 적용되지 않는 공간)에 메모리 저장소를 사용할 수 있습니다. (ehcache3)
- 디스크 공간을 이용해 캐시를 저장할 수 있습니다. (ehcache3)
- 직렬화된 데이터 객체를 저장하는 방식의
- Caffeine Cache
- Google에서 제공하는 오픈 소스 라이브러리인 Guava Cache와 ConcurrentHashMap을 개선한 ConcurrentLinkedHashMap을 바탕으로 개선된
In-Memory
캐시 구현체입니다. - 고성능, 최적의 캐싱 라이브러리라고 소개합니다.
- 최적의 적중률을 제공하는 WIndow TunyLfu 제거 정책을 사용합니다. (더 자세한 내용은 https://github.com/ben-manes/caffeine/wiki/Efficiency 이곳에서 확인할 수 있습니다.)
- Google에서 제공하는 오픈 소스 라이브러리인 Guava Cache와 ConcurrentHashMap을 개선한 ConcurrentLinkedHashMap을 바탕으로 개선된
아래 벤치마크 자료들은 MacBook Pro i7-4870HQ CPU @ 2.50GHz (4 core) 16 GB Yosemite 환경에서 실행한 자료입니다.
연산 능력 벤치마크
왼쪽 위: Compute / 오른쪽 위: READ 100%
왼쪽 아래: READ 75% WRITE 25% / 오른쪽 아래: WRITE 100%

Caffeine Cache 사용하기
벤치마크 자료를 보고 로컬 캐시들 중 성능이 가장 좋은 Caffeine Cache를 사용하기로 결정했습니다.
Caffeine Cache를 적용하기 위해 먼저 무엇을 캐싱할 것인지 정해야 합니다.
캐시 종류와 메타 정보
캐시 타입
public enum CacheType {
LOCAL,
GLOBAL,
}
캐시 종류는 현재 로컬 캐시만 사용할 예정입니다. 하지만 추후에 Redis 등을 이용한 글로벌 캐시 또는 로컬 캐시와 글로벌 캐시를 함께 사용할 수도 있으니 캐시 종류를 enum
으로 관리합니다.
캐시 메타 정보
@Getter
@RequiredArgsConstructor
public enum CacheGroup {
DAILY_REPORT_STATUS("dailyReportStatus", Duration.ofSeconds(24 * 60 * 60), 10_000, CacheType.LOCAL),
WEEKLY_REPORT_STATUS("weeklyReportStatus", Duration.ofSeconds(7 * 24 * 60 * 60), 10_000, CacheType.LOCAL),
DAILY_REPORT("dailyReport", Duration.ofSeconds(24 * 60 * 60), 10_000, CacheType.LOCAL),
WEEKLY_REPORT("weeklyReport", Duration.ofSeconds(7 * 24 * 60 * 60), 10_000, CacheType.LOCAL),
;
private final String cacheName;
private final Duration expiredAfterWrite;
private final long maximumSize;
private final CacheType cacheType;
}
캐시를 적용할 데이터의 성격에 따라 캐시 그룹을 enum
으로 관리합니다.
cacheName
:CacheManager
가Cache
를 찾을 때 사용할 캐시의 이름을 정합니다.expiredAfterWrite
: 캐시에 등록된 후부터 언제 삭제될지를 지정합니다.
- 캐시 무효화 관련 내용은 아래에서 다루겠습니다.
maximumSize
: 저장할 최대 캐시 엔트리 수를 지정합니다.CacheType
: 캐시 종류를 지정합니다.
의존성 추가

스프링 프레임워크가 제공하는 spring-boot-starter-cache
와 Caffein Cache 구현체를 제공하는 라이브러리 의존성을 추가합니다.
Configuration

yml
로 간단하게 Caffeine
캐시를 설정할 수 있지만 캐시의 만료 시간, 캐시 엔트리의 수 등 개별적으로 관리할 수는 없습니다.
따라서 저는 별도의 설정 클래스를 작성했습니다.
caffeineCacheManager
:CacheManager
를Bean
으로 등록합니다. 이때Cache
도 함께 등록합니다.toCaffeineCache
:CacheGroup
을 Caffeine Cache 구현체로 변환합니다. 캐시 만료 시간과 최대 크기, 캐시 히트 같은 통계 정보 등을 설정합니다.
SimpleCacheManager vs CaffeineCacheManager
캐시 매니저를 등록할 때 SimpleCacheManager
, CaffeineCacheManager
둘 중에 어느 것을 사용해야 할지 고르기 위해 정리했습니다.
결론적으로 CaffeineCacheManager
를 사용합니다.
SimpleCacheManager
:- 스프링에서 제공하는 기본 캐시 매니저입니다. 특정 캐시에 종속되지 않기 때문에 캐시의 종류(직접 구현한 캐시, Caffeine, EhCache 등)에 대해 구체적으로 제공해야 합니다. 따라서 사용할 캐시를 직접 등록해야 합니다.
CaffeineCacheManager
:Caffeine
라이브러리 기능(시간 기반 만료, 크기 기반 제거 정책, Weak/Soft reperences 등)을 사용할 수 있습니다. 따라서Ceffeine
라이브러리 의존성이 필요합니다.- 이름 기반으로 캐시를 자동 생성할 수 있습니다. (
yml
또는@Configuration
으로 생성) - 고성능과 정책을 통한 제어가 필요할 때 사용합니다. (
weak keys와 soft value
,lazy cleanup
관련 아래에서 설명)
Weak keys와 Soft value
Caffeine
의 대표적인 기능으로 Weak keys
와 Soft value
를 사용할 수 있는데요,
Weak keys
는 키가 더 이상 강한 참조로 참조되지 않으면 JVM GC에 의해 캐시 값이 제거됩니다.Soft value
는 메모리가 부족할 때 JVM GC에 의해 캐시 값이 제거됩니다.
이를 통해 고성능 캐싱 기능을 제공하는 것을 알 수 있습니다.
Lazy Cleanup (게으른 정리)
Caffeine
또 다른 대표적인 기능 중 하나가 시간 기반 만료
정책을 지원합니다.
이는 CaffeineCachaManager
에 Caffeine
을 등록할 때 expireAfterWrite()
, expireAfterAccess
등으로 설정할 수 있는데요, 사용할 때 주의할 점이 있습니다.
CaffeineCachaManager
은 만료 시간이 되었다고 해서 캐시를 바로 제거하지 않습니다. 만료된 항목을 바로 제거하려면 스레드가 주기적으로 스캔하고 제거해야 하는데, 이는 시스템의 자원을 많이 소모합니다.
따라서 CaffeineCacheManager
는 시스템 자원을 효율적으로 사용하기 위해 lazy cleanup
방식으로 캐시를 제거하는데, 캐시에 접근하지 않는 데이터는 굳이 삭제하는 작업을 수행하지 않는 것입니다.
만약 즉시 제거가 필요하다면 cleanUp()
메서드 등을 사용하거나 별도의 작업이 필요합니다.
캐시 무효화 정책
데이터를 캐시 할 땐 언제까지 유효해야 하는지 정해야 합니다.
너무 길게 설정하면 시스템 성능에 영향을 주고, 너무 짧게 설정해서 캐시 히트율이 떨어지면 데이터베이스 부하가 증가합니다.
얼마나 자주 조회되는지 등 데이터 접근 패턴도 고려해야 합니다.
매일 캐시가 무효화돼야 하는 경우
이번에 새로 개발한 기능 중 데일리 리포트 기능은 조회 일자
로부터 한 달까지가 유효한 데이터입니다.

그림에선 11월 20일 조회를 했다고 가정하면, 10월 20일까지가 유효한 데이터가 됩니다. 11월 21일 조회를 하면 10월 21일까지로 유효한 데이터 범위가 계속 이동합니다.
따라서 적어도 하루가 지날 때마다 최근 한 달
이라는 기준이 계속 변하기 때문에 매일 자정
이 되면 캐시를 무효화해야 합니다.
매주 캐시가 무효화돼야 하는 경우
위클리 리포트는 조회 일자
가 포함된 주차부터 4주 차 전까지가 유효한 데이터입니다.
그리고 위클리 리포트는 1주일에 한 번만 생성할 수 있습니다.

그림에선 11월 18일 월요일에 위클리 리포트를 응답받았다고 가정하면, 해당 응답은 그 주 일요일까지 같은 응답이기 때문에 1주간 캐시가 돼야 합니다.
따라서 다음 주차가 되면 새로운 주차에 대한 응답으로 변해야 하기 때문에 적어도 매주 월요일 자정
이 되면 캐시를 무효화해야 합니다.
무효화의 함정
캐시 무효화를 설정할 때 의도하지 않은 문제가 발생할 수 있습니다.
예를 들어 하루 단위로 캐시를 설정한다고 할 때 86,400초(=24 * 60 * 60)로 설정하면, 자정 직전(D일 23시 59분 59초)에 캐시한 데이터는 다음날 자정 직전(D+1일 23시 59분 59초)까지 캐시가 돼버리는 함정에 빠집니다.
따라서 이런 함정에 빠지지 않도록 별도의 스케쥴러를 통해 캐시를 무효화해 줄 필요가 있습니다.
캐시 무효화 적용
캐시 그룹에 기본 만료 시간 지정

데일리 리포트 관련 API
는 24시간 * 60분 * 60초 = 86,400초
, 위클리 리포트 관련 API
는 7일 * 24시간 * 60분 * 60초 = 604,800초
로 캐시 무효화 시간을 지정했습니다.
캐시 무효화 스케쥴러

캐시 무효화 스케쥴러는 @Schedule
애너테이션으로 cron
표현식을 통해 작성했습니다.
매일 자정
에데일리 리포트 관련 API
관련 캐시를 모두 무효화합니다.매일 월요일 자정
에는위클리 리포트 관련 API
관련 캐시를 모두 무효화합니다.
서비스에 캐시 적용하기
데일리 리포트 조회 API

메서드에 @Cacheable
애너테이션을 붙이는 것만으로 선언적인 캐시가 가능합니다.
cacheName
:- 저장할
Cache
객체를CacheManager
가 찾을 때 사용하는 이름입니다. 캐시 이름이 중복되면 덮어 쓰이기 때문에 고유한 이름을 사용해야 합니다.
- 저장할
cacheManager
:- 아무것도 지정하지 않을 시 스프링에서
Bean
으로 등록된CacheManager
중 가장 먼저 찾은 것을 주입합니다. - 만약 여러 캐시 매니저를 사용한다면, 명시적으로 사용할
CacheManager
를 지정하는 것이 좋습니다.
- 아무것도 지정하지 않을 시 스프링에서
key
:- 아무것도 지정하지 않으면 메서드의 모든 매개변수를 조합해서
SimpleKey
객체로 키를 생성합니다. 매개변수가 없으면SimpleKey.EMPTY
가 키로 사용됩니다. Cache
의key
로 사용되는 유일한 값을 생성할 때 사용할 메서드의 인자로 고유한key
를 생성하는데, SpEL 표현식으로 사용할 수 있습니다.
- 아무것도 지정하지 않으면 메서드의 모든 매개변수를 조합해서
unless
:- SpEL 표현식을 사용해서
조건이 참이면 캐싱하지 않는다
는 의미입니다. 반대로condition
은조건이 참이면 캐싱한다
라는 의미로 캐싱 작업을 시작하기 전에 조건을 평가합니다. #result
는 메서드가 반환하는 값이며, 예시에선 해당 값이null
이면 캐싱을 하지 않는다는 조건입니다.- 그 외
p0
,a0
는 메서드의 첫 번째 매개변수를 참조하고p1
,a1
은 두 번째 매개변수를 참조합니다.p
와a
는 동일하게 매개변수를 가리키지만 표준은a
를 사용합니다.
- SpEL 표현식을 사용해서
데일리 리포트 상태 조회 API

위클리 리포트 상태 조회 API

나머지 API도 선언적으로 캐시를 적용했습니다.
캐시 적용 테스트
이제 캐시를 적용한 테스트를 진행해 보겠습니다.
테스트 환경
- 스레드 풀 크기: 10 (기본값)
- 테스트 API에 대해 1회씩 요청한 후 수행 (캐시 적용)
- 그 외 기존 부하 테스트와 동일한 조건
부하 테스트 (커넥션 풀 10개)
(기존) 부하 테스트 결과
라벨 | 표본 수 | 평균 | 최소값 | 최대값 | 표준편차 | 오류 % | 처리량 |
생성 가능 일자 조회 (데일리 리포트) |
5000 | 351 | 17 | 500 | 48.13 | 96.340% | 423.51347 |
생성 가능 일자 조회 (위클리 리포트) |
5000 | 348 | 327 | 483 | 19.76 | 100.000% | 421.86973 |
데일리 리포트 조회 | 5000 | 359 | 327 | 579 | 40.92 | 100.000% | 427.31390 |
총계 |
15000 | 353 | 17 | 579 | 38.50 | 98.780% | 283.02955 |
(캐시 적용) 부하 테스트 결과
라벨 | 표본 수 | 평균 | 최소값 | 최대값 | 표준편차 | 오류 % | 처리량 |
생성 가능 일자 조회 (데일리 리포트) |
5000 | 59 | 8 | 217 | 32.81 | 0.000% | 489.09322 |
생성 가능 일자 조회 (위클리 리포트) |
5000 | 70 | 8 | 309 | 43.03 | 0.020% | 489.95590 |
데일리 리포트 조회 | 5000 | 41 | 7 | 246 | 29.51 | 0.000% | 492.22288 |
총계 | 15000 | 57 | 7 | 309 | 37.57 | 0.007% | 294.44095 |
JMeter 테스트 결과를 보면, 평균 응답 속도는 353ms => 57ms
로 83.8%
빨라진 것을 확인할 수 있습니다.
더불어 처리량 각 API마다 대략 70 TPS가 증가했는데, 오류율도 98.78% => 0.007%
로 목표치에 도달했습니다.
가장 느린 응답 속도는 579ms => 309ms
로 46.6%
빨라졌습니다.

핀포인트를 통해 확인한 결과, 가장 느린 응답 속도는 3200ms => 176ms
로 94.5%
빨라진 것을 확인했습니다.
트랜잭션 세부 내역에서 데이터베이스 커넥션이 없는 것을 확인할 수 있었으며, 실제 캐시가 적용되어 응답 속도가 25ms
로 굉장히 빠른 것을 볼 수 있습니다.
스파이크 테스트 (커넥션 풀 10개)
(기존) 부하 테스트 결과
라벨 | 표본 수 | 평균 | 최소값 | 최대값 | 표준편차 | 오류 % | 처리량 |
생성 가능 일자 조회 (데일리 리포트) |
1000 | 1605 | 51 | 2719 | 849.00 | 57.600% | 283.28612 |
생성 가능 일자 조회 (위클리 리포트) |
1000 | 1121 | 0 | 2148 | 971.76 | 94.000% | 180.31013 |
데일리 리포트 조회 | 1000 | 1467 | 0 | 2150 | 897.93 | 98.900% | 132.81976 |
총계 | 3000 | 1397 | 0 | 2719 | 930.17 | 83.500% | 393.28789 |
(캐시 적용) 부하 테스트 결과
라벨 | 표본 수 | 평균 | 최소값 | 최대값 | 표준편차 | 오류 % | 처리량 |
생성 가능 일자 조회 (데일리 리포트) |
1000 | 1253 | 46 | 2648 | 603.80 | 6.500% | 290.78220 |
생성 가능 일자 조회 (위클리 리포트) |
1000 | 724 | 0 | 1705 | 422.24 | 18.900% | 243.30900 |
데일리 리포트 조회 | 1000 | 649 | 0 | 1716 | 368.04 | 18.800% | 233.04591 |
총계 | 3000 | 875 | 0 | 2648 | 546.10 | 14.733% | 682.74920 |
JMeter 테스트에서 평균 응답 속도는 1397ms => 875ms
로 37.3%
빨라진 것을 확인할 수 있습니다. 가장 느린 응답 속도는 2719ms => 2648ms
로 유의미한 효과는 보기 힘들었습니다.
눈에 띄는 점은 2개의 API에서 스파이크 트래픽 상황에서 목표 응답 속도인 2000ms
보다 빠른 1700ms
를 달성했다는 점, 처리량이 각 API마다 많게는 100 TPS가 증가했다는 점입니다.

핀포인트를 통해 확인한 가장 느린 응답 속도로 집계된 트랜잭션의 세부 내역을 보면, 데이터베이스에 더 이상 접근하지 않기 때문에 커넥션 대기 시간이 사라졌지만 HTTP 요청 자체를 처리하는데 시간이 오래 걸린 것을 보아 톰캣 스레드 풀의 문제로 보입니다.
정리
- OS의 시분할 시스템에 의해 SSD를 사용할 땐 CPU 코어 개수가 적을 때 데이터베이스 커넥션 풀을 줄이면 처리 속도는 더 빨라집니다.
하지만 트래픽 부하가 있는 상황에선 커넥션 풀을 늘리는 것이 줄이는 것보다 나을 수 있습니다. - 캐시를 적용해 데이터베이스 접근을 줄이면 트래픽 부하 상황을 개선할 수 있습니다.
- 다만, 캐시 무효화 정책을 잘 설정해야 하고, 캐시 적용에 많은 자원이 사용되지 않도록 해야 합니다.
해결 과정에 초점을 맞추다 보니 글이 길어져버렸네요..
그래도 긴 글 읽어주셔서 감사합니다.
참고
- https://freedeveloper.tistory.com/250
- https://erjuer.tistory.com/127
- https://gngsn.tistory.com/158
- https://keun.me/java-caffeine/#%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%BA%90%EC%8B%9C-%EC%A2%85%EB%A5%98
- https://mangkyu.tistory.com/370
- https://stackoverflow.com/questions/57617581/caffeinecachemanager-vs-simplecachemanager
'Project' 카테고리의 다른 글
모놀리식 아키텍처에서 이벤트 기반으로 비즈니스 로직의 원자성 확보하기 (2) (1) | 2025.01.17 |
---|---|
모놀리식 아키텍처에서 이벤트 기반으로 비즈니스 로직의 원자성 확보하기 (1) (0) | 2025.01.16 |
[올려올려 라디오] 신규 분석 기능 성능 테스트 (2) (0) | 2024.12.06 |
[올려올려 라디오] 신규 분석 기능 성능 테스트 (1) (1) | 2024.12.05 |
[올려올려 라디오] 분석 서비스 개발 중 만난 데드락과 동시성 문제 해결기 (5) (0) | 2024.11.28 |