배경 (2026-09-26 부하 시험) 메모리 문제를 고친 뒤에도 부하 중 db 가 CPU 152% 를 쓰며 목록 API 가 300~600 ms 로 느려졌다. 기업 상세 · 수주 · 백테스트는 Redis 에 캐시돼 p95 30 ms 였지만, 부하의 대부분을 차지하는 기업 목록 · 경보 목록 · 대시보드 · 지역 목록 · 지역 시계열은 요청마다 DB 를 조회했다. 각각은 혼자 부르면 10~21 ms 지만 같은 쿼리가 초당 수백 번 반복됐다. 또 기업 목록 쿼리는 ‘기업별 최신 기간’ CTE 가 인라인되어 기업 43곳마다 다시 집계됐다(EXPLAIN ANALYZE: loops=43).
데이터가 바뀌는 때는 정해져 있다 — 배치 Job 이 끝날 때와 사람이 경보를 확인(ACK)할 때뿐이다.
결정
JsonCache, TTL 10분)에 넣는다. 대시보드는 실행 중인 Job 상태가 섞여 있어 15초.
제네릭 응답(Page<CompanyRow>)은 TypeReference 로 읽어 타입을 보존한다(Class 로 읽으면 항목이 Map 으로 풀려 숫자 표현이 달라질 수 있음).q) · 기간(since) · 대상 키가 있으면 바로 조회.br:gen)를 INCR 해 한 번에 전체를 버린다(기존 방식). 호출 위치를
Job 5개의 리스너 → 모든 Job 이 끝나는 한 곳(BatchLauncher.awaitFinished) 으로 옮겼다. worker 와 CLI 가 모두 이 메서드로 끝을 기다린다.
STOPPED · FAILED 여도 커밋된 청크는 반영돼 있으므로 상태와 관계없이 무효화한다.
예전에는 공시 · 미분양 · 가격지수 · 총가구 Job 뒤에 무효화가 없어, 이 목록들을 캐시했다면 옛 값을 보여 줄 뻔했다.
ACK 는 기존대로 API 에서 무효화한다.MATERIALIZED — 13.8 ms → 2.1 ms (EXPLAIN ANALYZE).결과 k6 50 VU · 90초: 약 3,400 req/s · API p95 30 ms · 오류 0 (전: 504 req/s · 307 ms).
RuleEvalJobIT.Job_이_끝나면_캐시된_경보_목록이_바로_새_경보를_보여_준다 가 캐시된 목록이 Job 직후 새 경보를 보여 주는지,
리스너가 없는 Job 도 세대를 올리는지 확인한다.
관측 buildrisk_cache_requests_total{result=hit·miss·error} 카운터와 Grafana ‘조회 캐시 적중률’ 패널로 캐시가 실제로 DB 부하를 덜어 주는지 본다
(error = Redis 를 쓸 수 없어 캐시 없이 조회한 요청 — 캐시는 장애 때 기능을 막지 않고 성능만 잃는다).
한계 make psql 로 DB 를 직접 고치면 캐시가 최대 10분 옛 값을 보여 준다(세대를 올리려면 아무 Job 이나 한 번 실행).
api 가 여러 대여도 세대 키가 Redis 하나에 있으므로 함께 무효화된다.
덧붙임 (2026-10-09, VERIFICATION 105 · 138)
덧붙임 (2026-10-11, VERIFICATION 175 · 186 · 194) — 대상 기업만 · 규칙 변경 무효화 · 같은 키 계산 하나로
ref.company 에는 DART 의 모든 회사(약 12만)가 있어 존재만 보고 저장하면 코드를 바꿔 부르는 것만으로
세션과 같은 Redis 에 키가 쌓였다(비대상 200곳 × 5조회: 키 +847 · 849 KB → +0). 판단은 계산한 뒤(캐시에 없을 때)만 해 적중 경로에 쿼리를 더하지 않는다.