Reviewer 결과 17 PASS / 1 REJECT (#267 admin 권한 critical). - 17개 설계서를 Draft → Approved로 갱신 - #267(backend-user)은 critical 결함으로 06-Reviewer 유지 - 후속 17개 개선 이슈(#289~#305) 자동 등록 — 결함 124건 백로그 반영 (critical 3 / major 46 / minor 75) - docs/README.md에 18개 설계서 인덱스 추가 - CHANGELOG.md 2026-06-15 섹션 추가 Refs: #266 #268-#283 (현행화 완료) #267 (대기) #289-#305 (백로그)
설계서: 백엔드 - 캐시 관리 (#276)
상태: Approved 작성: [AI] Architect · 최종수정: 2026-06-15 추적성 — Redmine: #276 · 관련 ADR: 없음 · 구현 파일:
backend-java/src/main/java/com/tasteby/service/CacheService.java,backend-java/src/main/java/com/tasteby/controller/AdminCacheController.java· 테스트: TBD (현재 없음)
1. 목적 (Why)
LLM/YouTube 응답·식당 목록 등 비용이 큰 조회 결과를 Redis에 캐싱해 응답 속도와 외부 API 비용을 줄이고, Redis 미가용 시에도 서비스가 정상 동작하도록 graceful degradation 한다. 데이터 갱신 시 관리자가 즉시 캐시를 무효화할 수 있어야 한다.
2. 범위 (Scope)
- 포함:
- Redis 기반 문자열/JSON 키-값 캐시(
CacheService). - 공통 키 prefix(
tasteby:) 및 키 빌더(makeKey). - 객체 ↔ JSON 직렬화/역직렬화(Jackson
ObjectMapper). - TTL 설정(
app.cache.ttl-seconds, 기본 600초). - Redis 미가용 자동 감지 → 캐시 비활성(
disabled플래그). - 관리자 캐시 일괄 삭제:
POST /api/admin/cache-flush. - 데몬에서 신규 데이터 발생 시 자동 flush 트리거(호출처:
DaemonScheduler).
- Redis 기반 문자열/JSON 키-값 캐시(
- 제외 (out of scope):
- 캐시 이용 정책(어떤 조회를 캐싱할지)은 호출 서비스 책임.
- 키별 개별 삭제 API.
- 캐시 히트율/지표 수집.
- 캐시 워밍/사전 적재.
- 분산 캐시 클러스터 토폴로지.
3. 인수조건 (Acceptance Criteria)
- 애플리케이션 기동 시 Redis 에
PING을 보내 연결 가능 여부를 로그로 남긴다. - Redis 미가용이면
disabled=true로 전환되고 이후 모든 캐시 호출이 no-op 가 된다(null 반환 또는 무시). makeKey(parts...)는tasteby:prefix +:조인 키를 반환한다.get(key, type)은 값이 없거나 비활성이면null, 있으면 Jackson 으로 역직렬화한 객체를 반환한다.getRaw(key)는 원시 문자열을 반환한다.set(key, value)는 JSON 직렬화 후 TTL(app.cache.ttl-seconds) 로 저장한다.flush()는tasteby:*패턴의 모든 키를 삭제한다.POST /api/admin/cache-flush는requireAdmin()통과 시에만flush()를 호출하고{ok:true}를 반환한다.- Jackson 직렬화/역직렬화·Redis 통신 에러는 DEBUG 로그만 남기고 호출자에게 예외를 던지지 않는다.
4. 컨텍스트 & 제약
- 의존성:
- Spring Data Redis
StringRedisTemplate(Lettuce 클라이언트). - Jackson
ObjectMapper(전역 빈, SNAKE_CASE 네이밍). AuthUtil.requireAdmin()— 관리자 검증.- 환경: dev=로컬 Redis(brew), prod=OKE in-cluster Redis.
- Spring Data Redis
- 제약:
KEYS tasteby:*는 Redis O(N) 명령 — 키가 매우 많아지면 블로킹 위험. 본 서비스는 캐시 규모가 작아 허용.- TTL 기본 10분 — 응답 신선도와 비용의 균형.
disabled는 기동 시 한 번만 결정 — Redis 가 런타임 중 살아나도 자동 복구 안 됨.
- 가정:
- 캐시는 휘발성 — 손실되어도 DB로부터 재계산 가능.
- 모든 캐시 키는
tasteby:prefix 사용을 강제(공유 Redis 안전). - 캐시 값은 JSON 직렬화 가능한 POJO.
5. 아키텍처 개요
- 모듈:
CacheService— 캐시 게이트웨이(직렬화 + 키 관리 + 가용성 체크).AdminCacheController— 관리자 flush 엔드포인트.
- 경계:
- I/O: Redis(
StringRedisTemplate), 호출 서비스 ↔ DB. - 순수 로직: 키 빌더, JSON 직렬화(라이브러리 위임).
- I/O: Redis(
┌────────────┐ ┌─────────────┐
│ 호출 서비스 │───▶│ CacheService│──── get / set / flush ────▶ Redis (tasteby:*)
└────────────┘ │ (disabled?)│ TTL=app.cache.ttl-seconds
▲ ▲ │ fallback │
│ │ null └──────┬──────┘
│ │ │ 직렬화/역직렬화
│ │ ▼
│ │ ObjectMapper (Jackson)
│ │
│ └ DB 폴백 (호출 서비스 책임)
│
┌────┴────────────┐
│ DaemonScheduler │ (신규 데이터 시 자동 flush)
└─────────────────┘
관리자 ──POST /api/admin/cache-flush──▶ AdminCacheController ──▶ requireAdmin() ──▶ flush()
6. 데이터 모델
- 키 스키마:
tasteby:<part1>:<part2>:...—makeKey(parts...)로만 생성. 호출자가 자유롭게 정의. - 값 스키마: UTF-8 문자열.
set()은 Jackson 직렬화 결과(JSON),get(_, type)은 동일 타입으로 역직렬화.getRaw()는 원시 문자열. - TTL:
Duration ofSeconds(app.cache.ttl-seconds), 기본 600초. - 저장소 가용성 플래그:
boolean disabled—final아님, 기동 시 결정. - 응답 객체: flush 엔드포인트는
{"ok": true}(Map<String,Object>). - 경계 검증:
keynull/empty 체크 없음(호출자 책임).- 값 크기 상한 검증 없음(Redis 기본 한도에 의존).
7. 함수 명세 (Function Specs)
| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? |
|---|---|---|---|---|---|---|
CacheService(constructor) |
Redis ping 후 가용성 결정 | CacheService(StringRedisTemplate, ObjectMapper, int ttlSeconds) |
DI 빈 + TTL | 인스턴스 | ping 실패 시 WARN + disabled=true |
단순 |
makeKey |
표준 prefix 키 생성 | String makeKey(String... parts) |
varargs | tasteby:a:b:... |
- | 단순 |
get |
타입 지정 캐시 조회 | <T> T get(String key, Class<T> type) |
key, type | T 또는 null | 비활성/예외 시 null, DEBUG 로그 | 단순 |
getRaw |
원시 문자열 조회 | String getRaw(String key) |
key | String 또는 null | 비활성/예외 시 null | 단순 |
set |
JSON 직렬화 + TTL 저장 | void set(String key, Object value) |
key, value | void | 비활성/직렬화 실패 시 무시(DEBUG) | 단순 |
flush |
tasteby:* 일괄 삭제 |
void flush() |
- | void | 비활성/예외 시 무시(DEBUG) | 단순 |
AdminCacheController.flushCache |
POST /api/admin/cache-flush |
Map<String,Object> flushCache() |
- | {ok:true} |
requireAdmin() 실패 → 401/403 |
단순 |
8. 흐름 / 알고리즘
기동 시(Construct):
StringRedisTemplate.getConnectionFactory().getConnection().ping()호출.- 성공 →
disabled=false, "Redis connected" 로그. - 실패 →
disabled=true, WARN 로그 — 이후 모든 호출 no-op.
get(key, type):
disabled이면 즉시null.redis.opsForValue().get(key)→null이면null반환.mapper.readValue(val, type)으로 역직렬화 → 반환.- 어떤 예외든 잡아 DEBUG 로그 →
null반환.
set(key, value):
disabled이면 종료.mapper.writeValueAsString(value)→ JSON.redis.opsForValue().set(key, json, ttl)— TTL 적용.JsonProcessingExceptionDEBUG 로그.
flush():
disabled이면 종료.redis.keys("tasteby:*")→ 키 셋.- 비어있지 않으면
redis.delete(keys)일괄 삭제. - INFO 로그 "Cache flushed".
자동 flush 트리거: DaemonScheduler 가 스캔/처리 후 신규 건이 있을 때 호출.
관리자 flush: POST /api/admin/cache-flush → requireAdmin() → flush().
9. 엣지케이스 & 에러 처리
- Redis 다운(기동): ping 실패 →
disabled=true. 모든 호출 안전하게 no-op. 서비스는 DB 직조회로 동작 지속. - Redis 런타임 중 다운:
disabled가 false 인 상태로 예외 → DEBUG 로그 +null반환. 호출자는 DB 폴백. (자동 복구 미구현) KEYS명령 비용: 키 개수 폭증 시 블로킹 — 향후SCAN으로 교체 검토.- 직렬화 실패:
JsonProcessingExceptionDEBUG 로그만 — 값 저장 안 됨. 호출자는 일관성 가정 불가(다음get시 miss). - 역직렬화 실패: 타입 변경 후 잔존 키 → DEBUG 로그 + null → 호출자가 재계산. (배포 시 한 번 flush 권장)
- 부분 prefix 충돌:
tasteby:외 prefix 사용 시flush()가 삭제하지 않음 — 호출자가makeKey()만 사용하도록 컨벤션 준수. - 비관리자 flush 호출:
AuthUtil.requireAdmin()가 예외 → 글로벌 예외 핸들러가 401/403 반환. - 동시 flush + set: race condition 으로 직후 set 만 살아남을 수 있음. 캐시 정합성이 휘발성이라 허용.
10. 테스트 계획
현재 자동 테스트 없음(TBD). 권장 케이스:
- Unit (CacheService, embedded Redis 또는 Testcontainers):
makeKey("a","b")→tasteby:a:b.set→get라운드트립 동등성.- TTL 적용 확인(짧은 TTL 주입 후 expire 대기).
flush()후get→ null.- Redis 비활성 시 모든 호출 no-op 보장.
- 잘못된 JSON 값 →
getnull 반환, 예외 누출 없음.
- Integration (AdminCacheController):
- 비관리자 401/403.
- 관리자 200 + Redis 에 키 없어짐.
- 모킹:
StringRedisTemplateMockito 또는 Testcontainers Redis.AuthUtil정적 호출은 MockedStatic.
11. 리스크 & 대안 검토
- 선택: 단일 Redis + StringRedisTemplate + JSON 문자열 저장.
- 장점: 단순, 디버깅 쉬움(redis-cli
GET가능), 어떤 POJO 도 캐싱. - 단점: 직렬화/역직렬화 오버헤드, 타입 안전성 약함.
- 장점: 단순, 디버깅 쉬움(redis-cli
- 대안:
- Spring
@Cacheable/CacheManager— 선언적이지만 동적 키/조건이 어렵고 graceful degradation 처리가 까다로움. - Caffeine 로컬 캐시 — JVM 내라 빠르지만 멀티 파드 간 일관성 깨짐.
- Protobuf/MsgPack 바이너리 — 성능↑이나 운영 가시성↓.
- Spring
- 트레이드오프: 현재 규모(개인 운영, 트래픽 적음)에서 단순함이 우선.
- 되돌리기 어려운 결정 없음 — 키 스키마만 일관 유지하면 내부 구현은 교체 가능.
12. 미해결 질문 (Open Questions)
- 런타임 중 Redis 가 복구되면
disabled를 자동 해제할지(주기적 ping 헬스체크)? KEYS를SCAN으로 교체할 시점은 언제인가(키 개수 기준)?- 키별 TTL 차등 지정(짧은/긴 TTL)이 필요한가?
- 캐시 히트율 지표(Micrometer)를 도입할 것인가?
- 관리자용 키별 삭제/조회 API 가 필요한가?
- 배포 시 자동 flush 훅(애플리케이션 기동 또는 마이그레이션 후)이 필요한가?