Files
comrelay/docs/redis-cache-review.md
T
ttipo fa4bc40761 docs: 8단계 진행현황·API 계약 갱신 + 알림/Redis 검토 문서 추가
- 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>
2026-06-19 13:13:07 +09:00

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.ts CacheModule.registerAsyncREDIS_HOST 있으면 createKeyv(redis://…) 스토어, 없으면 인메모리 폴백(LRU). keyv error 흡수(장애→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. 버전 네임스페이스 무효화 — 변경 시 버전 +1 로 모든 페이지 캐시를 O(1) 폐기. 키 와일드카드 삭제 없이 정확.
  2. 다중 인스턴스 무효화 안전 — 버전 카운터가 공유 Redis 에 있어, 인스턴스 A 의 변경이 인스턴스 B 의 다음 읽기에서 즉시 캐시 미스를 유발. (알림 R6 와 달리 캐시는 추가 작업 없이 이미 수평 확장 정합)
  3. 그레이스풀 폴백REDIS_HOST 미설정 시 인메모리, Redis 장애 시 try/catch 로 DB 조회. 캐시가 가용성을 떨어뜨리지 않음.
  4. keyv 오류 흡수app.modulekeyv.on('error') 로 연결 오류가 프로세스를 죽이지 않음.
  5. meta 미캐싱 결정 — DB 미접근 getter 라 캐싱 제외. 올바른 판단.
  6. plain-DTO 캐싱RepoResponse[](날짜는 이미 ISO 문자열)만 저장 → JSON 직렬화 무손실, 엔티티 hydration 함정 없음.
  7. TTL 백스톱 — 업무 카운트처럼 무효화하지 않는 파생값도 60초 내 자연 보정.

5. ⚠️ 빠진 부분 (Gaps)

# 빠짐 현재 상태 영향 우선순위
C1 멤버 변경 시 목록 캐시 무효화 누락 보강 완료(2026-06-19)MemberServiceRepoCacheService 주입, 초대/제거invalidateList() 호출. (역할 변경은 목록 응답 불변이라 제외) 멤버 초대/제거 후 목록 카드의 멤버 수·아바타 즉시 반영 P2 완료
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 멤버 초대/제거 시 목록 캐시 무효화(MemberServiceRepoCacheService)
    • D4 enableShutdownHooks 로 종료 시 Redis 연결 graceful close(死코드였던 onModuleDestroy 활성화)
    • D5 캐시 모드 기동 로그로 오설정 가시화
  • 보류(선택) — 트래픽/확장 시 재검토
    • C2 캐싱 커버리지 확대(업무 목록/사용자 검색 등) — 무효화 설계 비용이 커 현재는 과설계
    • D3 Redis 클라이언트 단일화(keyv+ioredis) — 통합이 캐시를 더 복잡하게 만들어 현행 유지
  • 현행 유지(영향 없음, 평가 후 변경 불필요로 판단)
    • D1 버전 비원자 증가 — 정합성 증명상 안전(버전은 전진 + DB 커밋이 set 보다 선행). 원자 INCR 은 클라이언트 혼용을 유발해 오히려 손해 → 변경 안 함
    • D2 캐시 스탬피드 · D6 sub-ms 윈도 — 캐시-어사이드 표준 한계

요약: 실질 빠짐(C1)과 잠재 위험(D4 死코드·D5 오설정 가시성)은 보강 완료. 남은 항목은 모두 평가 후 의도적으로 현행 유지(과설계 회피)이며 동작에 영향이 없다.