- integration-progress/api-contract: 8-1 페이지네이션·8-2 알림·8-3 Redis 캐싱 반영. - notification-review.md: 알림 시스템 전수 검토(유형/빠짐/주의) + 보완 이력. - redis-cache-review.md: Redis 캐시 전수 검토 + 보완 이력. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
Redis 캐싱 시스템 전체 검토
검토일: 2026-06-19 · 대상: 8-3단계 Redis 캐싱 구현 전체 관련 계약:
api-contract.md§0(캐싱) · 진행 기록:integration-progress.md§8-3 코드 기준:backend/src/app.module.ts(CacheModule),modules/repo/repo-cache.service.ts,repo.service.ts(findAll·무효화),repo-sync.service.ts
알림 검토(notification-review.md)와 동일 형식으로 ① 무엇을 캐싱했는가, ② 빠진 부분은 없는가, ③ 잘못된 부분은 없는가에 답한다.
갱신(2026-06-19): 검토 후 보강 완료 — C1(멤버 초대/제거 시 목록 캐시 무효화) 추가, D5(캐시 모드 기동 로그로 가시화), D4(
enableShutdownHooks로 종료 시 Redis 연결 정상 정리 — 기존 死코드였던onModuleDestroy활성화). 나머지(C2·D1·D2·D3·D6)는 평가 후 현행 유지(과설계 회피) — §6·§7 참조.
1. 아키텍처 요약
이 프로젝트에서 Redis 는 두 용도로 쓰인다(같은 인스턴스, 별개 연결·클라이언트).
| 용도 | 클라이언트 | 무엇 | 키/채널 |
|---|---|---|---|
| 캐시(8-3) | @keyv/redis(node-redis 계열) |
전역 CacheModule 스토어 + 저장소 목록 캐시 | 키 repos:list:* |
| 알림 실시간(8-2/R6) | ioredis |
SSE fan-out pub/sub | 채널 relay:notifications |
- 캐시 전환:
app.module.tsCacheModule.registerAsync—REDIS_HOST있으면createKeyv(redis://…)스토어, 없으면 인메모리 폴백(LRU). keyverror흡수(장애→DB 폴백). 기본 TTL 60초. - 저장소 목록 캐싱:
RepoCacheService가 버전 네임스페이스 방식으로GET /repos(findAll)를 캐시. 변경 시 버전을 올려 일괄 무효화.
2. 구현된 캐싱
캐시 대상
| 대상 | 캐시? | 근거 |
|---|---|---|
GET /repos(저장소 목록, findAll) |
✅ 캐시(버전 네임스페이스, TTL 60s) | 랜딩·최다 트래픽 + DB 집계(멤버 조인 + 업무 카운트) 비용 |
GET /repos/meta(생성 폼 메타) |
❌ 미캐싱(의도) | DB 미접근 순수 인메모리 getter — Redis 왕복이 오히려 손해 |
| 그 외 모든 읽기 | ❌ 미캐싱 | §5 C2 참조 |
동작 방식 (RepoCacheService)
- 키:
repos:list:v{ver}:p{page}:s{size}— 페이지 단위로 별도 캐시. - 버전 카운터:
repos:list:ver(TTL 없음, 영속).version()이 없으면 1로 초기화. - 무효화:
invalidateList()가 버전을 +1 → 이전 버전 키 전체가 더 이상 참조되지 않음(keyv 와일드카드 삭제 미지원 회피). 남은 구버전 키는 60s TTL 로 자연 소멸. - 폴백: 모든 캐시 접근(
get/set)을 try/catch 로 감싸 실패 시 DB 조회로 폴백.
무효화 호출 지점 (현재 4곳)
| 위치 | 트리거 |
|---|---|
repo.service.ts |
저장소 생성 / 수정 / 삭제 |
repo-sync.service.ts |
Gitea 동기화(import/누락표시/admin 변경분 발생 시) |
member.service.ts |
멤버 초대 / 제거(목록 카드의 멤버 수·아바타 반영) — 2026-06-19 보강 |
멤버 역할 변경은 목록 응답(멤버 수/아바타)을 바꾸지 않으므로 의도적으로 무효화하지 않는다.
3. 검토 결과 한눈에
| 구분 | 건수 | 비고 |
|---|---|---|
| ✅ 정상 동작 확인 | 7 | 특히 다중 인스턴스 무효화 안전(공유 버전 키) |
| ⚠️ 빠진 부분(gap) | 2 → 1 | C1 보강 완료 / C2(캐싱 커버리지)는 선택 보류 |
| 🐛 주의/개선 | 6 | 기능 버그 없음. D4·D5 보강, 나머지 현행 유지 |
종합 판정: 저장소 목록 캐싱은 정상 동작하며, 버전 네임스페이스 + 공유 Redis 덕에 무효화가 다중 인스턴스에서도 안전(알림과 달리 별도 pub/sub 불필요). 치명적 버그 없음. 이번 검토의 핵심 빠짐이던 C1(멤버 변경 시 목록 캐시 무효화 누락)은 보강 완료 — 멤버 초대/제거 시 무효화 추가. 남은 것은 선택적 커버리지 확대(C2)와 동작에 영향 없는 견고성 항목뿐.
4. ✅ 정상 동작 확인 항목
- 버전 네임스페이스 무효화 — 변경 시 버전 +1 로 모든 페이지 캐시를 O(1) 폐기. 키 와일드카드 삭제 없이 정확.
- 다중 인스턴스 무효화 안전 — 버전 카운터가 공유 Redis 에 있어, 인스턴스 A 의 변경이 인스턴스 B 의 다음 읽기에서 즉시 캐시 미스를 유발. (알림 R6 와 달리 캐시는 추가 작업 없이 이미 수평 확장 정합)
- 그레이스풀 폴백 —
REDIS_HOST미설정 시 인메모리, Redis 장애 시 try/catch 로 DB 조회. 캐시가 가용성을 떨어뜨리지 않음. - keyv 오류 흡수 —
app.module의keyv.on('error')로 연결 오류가 프로세스를 죽이지 않음. - meta 미캐싱 결정 — DB 미접근 getter 라 캐싱 제외. 올바른 판단.
- plain-DTO 캐싱 —
RepoResponse[](날짜는 이미 ISO 문자열)만 저장 → JSON 직렬화 무손실, 엔티티 hydration 함정 없음. - TTL 백스톱 — 업무 카운트처럼 무효화하지 않는 파생값도 60초 내 자연 보정.
5. ⚠️ 빠진 부분 (Gaps)
| # | 빠짐 | 현재 상태 | 영향 | 우선순위 |
|---|---|---|---|---|
✅ 보강 완료(2026-06-19) — MemberService 에 RepoCacheService 주입, 초대/제거 시 invalidateList() 호출. (역할 변경은 목록 응답 불변이라 제외) |
멤버 초대/제거 후 목록 카드의 멤버 수·아바타 즉시 반영 | |||
| C2 | 캐싱 커버리지 | findOne(저장소 상세)·멤버 목록·업무 목록(/repos/:id/tasks)·내 업무·사용자 검색은 미캐싱 |
성능 최적화 여지(특히 업무 목록·사용자 검색은 빈도 높음). 단 무효화 복잡도(세그먼트/검색/담당자 조합)가 커 신중 필요 | P3(선택, 보류) |
C2 참고(보류)
업무 목록(/repos/:id/tasks)은 segment·q·page 조합 키가 많고 업무 상태 변경마다 무효화가 필요해, 캐싱하려면 저장소별 버전 네임스페이스 + 상태전이 시 무효화 설계가 추가로 든다. 현재 규모에선 과설계라 보류 — 트래픽/병목이 실제로 나타날 때 도입.
C2 참고
업무 목록(/repos/:id/tasks)은 segment·q·page 조합 키가 많고 업무 상태 변경마다 무효화가 필요해, 캐싱하려면 저장소별 버전 네임스페이스 + 상태전이 시 무효화 설계가 추가로 든다. 현재 규모에선 보류가 합리적.
6. 🐛 주의 / 개선 (견고성·일관성·구조)
기능 오류(버그)는 없다. 아래는 견고성·일관성·구조 관점.
| # | 항목 | 영향 | 권고 |
|---|---|---|---|
| D1 | 버전 카운터 비원자 증가(get+set) |
동시 무효화 시 증가분이 "유실"될 수 있음(둘 다 같은 값으로 set). 정합성은 깨지지 않음(버전은 어쨌든 전진 → 구버전 키 폐기, DB 커밋이 버전 set 보다 선행). 단 암묵적 순서 의존. | 엄밀히 하려면 Redis 원자 INCR(ioredis 이미 도입됨) 사용. 현재는 TTL 백스톱으로 충분 |
| D2 | 캐시 스탬피드(single-flight 없음) | 무효화/TTL 만료 직후 동시 요청이 모두 DB 재계산(한 요청이 채우기 전). 목록 쿼리라 영향 경미 | 트래픽 급증 시 잠금/단일 비행 도입 검토 |
| D3 | Redis 클라이언트 2종 공존 | 캐시=@keyv/redis(node-redis), 알림=ioredis. 같은 Redis 에 두 클라이언트 라이브러리 |
동작 무해. 정리하려면 한쪽으로 통일(번들/유지보수 단순화) |
| D4 | 종료 시 연결 graceful close | ✅ 보강 — main.ts enableShutdownHooks() 추가로 종료 시 lifecycle 훅 실행. 이전엔 훅 미설정이라 알림의 onModuleDestroy(ioredis 정리)가 호출조차 안 되던 死코드였음 → 이제 정상 종료 시 Redis pub/sub 연결 graceful close. (캐시 keyv 연결은 핸들 노출이 없어 프로세스 종료에 위임 — 잔여, 영향 없음) |
알림 연결 정리 활성화 완료 |
| D5 | 인메모리 폴백 = 다중 인스턴스 시 캐시 불일치 | ✅ 가시화 보강 — 기동 시 캐시 모드 로그(캐시: Redis 모드 / 캐시: 인메모리 모드(단일 인스턴스)) 추가로 오설정 조기 발견 가능. 설계상 단일 인스턴스 모드는 정상 |
로그로 가시화 |
| D6 | 쓰기-무효화 사이 sub-ms 스테일 윈도 | DB 커밋 후 invalidateList 까지의 찰나에 들어온 읽기가 캐시 히트로 직전 데이터 반환 가능 |
캐시-어사이드 표준 한계. 사실상 무시 가능(현행 유지) |
7. 권고 요약 (우선순위)
- ✅ 보강 완료(2026-06-19)
- C1 멤버 초대/제거 시 목록 캐시 무효화(
MemberService→RepoCacheService) - D4
enableShutdownHooks로 종료 시 Redis 연결 graceful close(死코드였던onModuleDestroy활성화) - D5 캐시 모드 기동 로그로 오설정 가시화
- C1 멤버 초대/제거 시 목록 캐시 무효화(
- 보류(선택) — 트래픽/확장 시 재검토
- C2 캐싱 커버리지 확대(업무 목록/사용자 검색 등) — 무효화 설계 비용이 커 현재는 과설계
- D3 Redis 클라이언트 단일화(keyv+ioredis) — 통합이 캐시를 더 복잡하게 만들어 현행 유지
- 현행 유지(영향 없음, 평가 후 변경 불필요로 판단)
- D1 버전 비원자 증가 — 정합성 증명상 안전(버전은 전진 + DB 커밋이 set 보다 선행). 원자 INCR 은 클라이언트 혼용을 유발해 오히려 손해 → 변경 안 함
- D2 캐시 스탬피드 · D6 sub-ms 윈도 — 캐시-어사이드 표준 한계
요약: 실질 빠짐(C1)과 잠재 위험(D4 死코드·D5 오설정 가시성)은 보강 완료. 남은 항목은 모두 평가 후 의도적으로 현행 유지(과설계 회피)이며 동작에 영향이 없다.