분석 트리거(/api/analysis/run) 직후 kisApiExecutor(core 10 / max 50)에서 일봉 조회가 병렬로 동시에 터지면서 다음 현상이 재현됨.
LLMAnalysisResult 누락Spring Retry만 도입할 경우 재시도 간격 제어는 가능하지만 선제적 유량 제어가 불가능하다. 따라서 다음과 같이 역할을 분리.
| 컴포넌트 | 책임 |
|---|---|
| RateLimiter | 호출 시점 자체를 늦춰 KIS의 20 req/sec 한도를 넘기지 않도록 사전 차단 |
| Retry | 한도를 넘겨 받은 5xx/429에 대해 지수 백오프 재시도 |
KIS 한도(20 req/sec)에 헤드룸 25% 를 두고, 해외 분석 도입 후의 호출 패턴을 기준으로 산정.
application.yml
kis:
api:
resilience:
rate-limit:
limit-for-period: 15 # 1초당 15건 (한도 20 대비 75% 사용)
refresh-period-ms: 1000
timeout-ms: 5000 # permit 대기 최대 5초 (캐시 미스 워스트 케이스 커버)
retry:
max-attempts: 3 # 최초 호출 + 2회 재시도
initial-wait-ms: 300 # 1차 대기 300ms
multiplier: 2.0 # 300ms → 600ms → 1200ms (지수 백오프)
산정 근거:
kisApiExecutor의 CallerRunsPolicy와 결합해 백프레셔를 끝까지 전달하되, 호출 스레드가 무한 블로킹되지 않도록 상한 설정.LLMApiClient.analyzeStock의 60초 orTimeout 안에 충분히 들어옴.무분별한 재시도는 비즈니스 로직 오류까지 다시 호출해 비용을 키우므로, 재시도 가치가 있는 예외만 통과시킨다.
// KisResilienceConfig.java:68
private boolean isRetryable(Throwable throwable) {
if (throwable instanceof WebClientResponseException responseException) {
return responseException.getStatusCode().is5xxServerError()
|| responseException.getStatusCode().value() == 429;
}
return false;
}
KisApiClient.callApi의 실제 HTTP 호출 부분만 데코레이터로 감싸서, JSON 파싱·로깅 등 부가 작업은 재시도 범위에서 제외.
// KisApiClient.java:371
String responseBody = Retry.decorateSupplier(kisApiRetry,
RateLimiter.decorateSupplier(kisRateLimiter,
() -> executeApiCall(method, fullUrl, endpoint, trId, accessToken)
)
).get();
데코레이터 순서를 Retry(바깥) → RateLimiter(안) 로 잡은 이유:
@Value로 외부화 → 운영 중 한도 조정 시 코드 변경 없이 application.yml만 수정RateLimiter.onFailure, Retry.onRetry, Retry.onError 이벤트를 모두 @Slf4j로 로깅 → 언제, 몇 번째 시도에서, 어떤 에러로 재시도했는지 가시화| 지표 | Before | After |
|---|---|---|
| KIS 5xx/429 발생 비율 | 분석 1회당 평균 3~5건 | 0건 (피크 1건 미만) |
| 일봉 누락으로 인한 LLM 분석 스킵 종목 | 평균 2건/cycle | 0건 |
| 분석 1회 평균 소요시간 | 약 18초 (실패→무시) | 약 22초 (재시도 흡수 포함, 전 종목 성공) |
| 설정 변경 시 재배포 필요 여부 | 코드 수정 필요 | YAML 수정만 |
callApi 한 군데에만 적용되므로 국내/해외, 거래량/일봉 어떤 신규 KIS 엔드포인트가 추가돼도 자동 보호 받음AnalysisConsumer → DefaultErrorHandler → DLT 경로가 그대로 살아있어, 회복 가능한 에러는 인라인 재시도, 회복 불가능한 에러는 DLT 로 깔끔하게 분기되는 2단 방어선이 완성됨kisApiExecutor의 max pool size를 줄여 동시성 자체를 낮추는 안도 검토했으나, 이는 국내 단독 분석의 응답시간까지 함께 떨어뜨리는 부작용이 있어 기각. RateLimiter는 "호출이 몰릴 때만 직렬화"되므로 평시 성능은 그대로 유지된다.fetchNewAccessToken)이 6시간마다 끼어들 때 충돌을 피할 수 있고, 여유분이 있다.