Cloudturing blog

캐시는 저장보다 삭제가 어렵다: 무효화 경로를 통합한 과정

Cloudturing Team 발행: 2026. 08. 24 16:18 수정: 2026. 08. 25 14:37

설정 변경 뒤 Legacy와 Gateway 캐시 key를 공통 함수로 무효화하는 흐름

3줄 요약

  • DB의 챗봇 설정은 최신인데 한 위젯 경로가 이전 값을 계속 보여, 서로 다른 서비스가 별도 cache key를 사용하면서 한쪽만 무효화된 사실을 발견했다.
  • 긴급 대응에서는 stale key를 넓게 삭제했지만 정상 경로는 key builder와 공통 invalidation 함수를 도입해 변경된 chatbot ID만 삭제하도록 바꿨다.
  • cache-aside에서 저장 성공은 작업의 절반이며, 모든 reader가 어떤 key를 재생성하는지 writer의 코드 계약과 회귀 테스트에 포함해야 한다.

증상: DB는 맞는데 위젯이 틀렸다

관리 화면에서 설정을 바꿨고 Cloud SQL의 값도 최신이었다. 일부 API 응답도 새 값을 반환했다. 그런데 특정 위젯 로딩 경로는 이전 engine 선택이나 디자인 설정을 계속 사용했다.

이런 문제는 DB 저장 실패처럼 보이지 않는다. 저장 API는 성공했고 관리 화면을 다시 열어도 최신 값이 보인다. cache를 직접 지우거나 TTL이 끝난 뒤에야 위젯이 정상화되므로 환경이나 CDN 문제처럼 오해하기 쉽다.

실제 원인은 같은 chatbot 설정을 읽는 두 runtime이 서로 다른 Valkey key namespace를 사용한 데 있었다. 설정 변경 경로는 이전 서비스의 key만 삭제했고, 새 Gateway 위젯 key는 남아 있었다.

Cache-aside에서 invalidation의 위치

두 reader 모두 cache-aside 형태였다.

요청 → cache hit → cached config 반환
     → cache miss → DB 조회 → cache 저장 → 반환

DB를 갱신한 뒤 cache key를 삭제하면 다음 read가 miss를 만나 최신 DB 값으로 cache를 다시 만든다. cache 값을 직접 수정하는 것보다 reader의 한 가지 생성 경로를 유지할 수 있다.

문제는 “설정 cache”라는 개념이 하나여도 실제 key가 하나가 아니라는 점이었다.

Legacy reader  → legacy-config:{id}
Gateway reader → gateway-config:{id}

writer가 첫 번째 key만 알고 있으면 두 번째 reader의 stale window는 TTL 전체가 된다.

왜 key가 두 개 생겼나

시스템을 한 번에 교체하지 않고 기존 runtime과 새 Gateway를 병행하면 동일한 DB row를 서로 다른 형태로 cache할 수 있다. response schema, TTL과 consumer가 다르면 key namespace를 분리하는 것 자체는 잘못이 아니다.

문제는 소유권이 분산된 상태에서 새 설정 저장 API가 각 cache의 존재를 알아야 했다는 점이다. 디자인, 플러그인, 워터마크, 만족도 조사, 이름과 engine migration처럼 설정 변경 endpoint가 많아질수록 누락 가능성이 커졌다.

key 문자열이 여러 route에 직접 쓰여 있으면 code review에서도 새 reader cache를 빠뜨리기 쉽다.

긴급 복구와 구조 개선을 분리했다

운영 장애 중에는 영향 범위를 빠르게 줄여야 했다. 먼저 Gateway 설정 cache namespace의 stale entry를 넓게 삭제하고, 다음 요청에서 DB 최신 값으로 재생성되는지 확인했다.

이 방식은 즉시 복구에는 유효하지만 정상 운영 로직으로 쓰기에는 거칠다. wildcard 전체 삭제는 문제없는 chatbot까지 cold miss로 만들고 DB 부하를 순간적으로 늘린다. 원인 key와 영향 ID를 숨겨 다음 누락을 막지도 못한다.

그래서 긴급 전체 삭제는 사건 대응 기록으로 닫고, 재발 방지는 별도 코드 변경으로 진행했다.

공통 key builder

먼저 cache key 조립을 함수로 고정했다.

buildLegacyConfigKey(chatbotId)
buildGatewayConfigKey(chatbotId)

ID는 trim한 뒤 빈 값을 거부했다. 잘못된 ID가 prefix만 있는 넓은 key가 되는 것을 막는다. 서비스별 코드 복사본도 shared source에서 동기화하고 각 runtime의 테스트로 실제 배포 파일이 같은 계약을 유지하는지 확인했다.

공통화의 목적은 짧은 문자열 두 개를 재사용하는 데 있지 않다. key namespace의 이름과 입력 검증을 변경할 한 곳을 만들고, reader와 invalidator가 같은 builder를 사용하게 하는 것이다.

무효화 함수의 세 가지 범위

모든 호출자가 항상 모든 cache를 삭제하는 하나의 함수만 필요한 것은 아니었다. 현재 구조에서는 이전 Console 경로가 legacy cache를, 새 API 경로가 Gateway cache를 주로 소유한다. 관리·migration처럼 양쪽에 영향을 주는 변경은 둘 다 지워야 한다.

그래서 의도를 이름으로 드러냈다.

  • legacy 설정 cache만 무효화
  • Gateway 설정 cache만 무효화
  • 두 설정 cache를 함께 무효화

함수는 삭제 대상 key, key별 삭제 결과와 총 삭제 수를 반환한다. DEL 결과가 0이어도 오류는 아니다. cache entry가 없었다는 뜻일 수 있다. Valkey 연결 오류와 “지울 key가 없음”을 구분해야 한다.

저장과 cache 삭제의 순서

정본 DB 저장이 성공한 뒤 cache를 삭제한다. 먼저 cache를 지운 다음 DB write가 실패하면 concurrent read가 이전 DB 값을 다시 cache할 수 있다. 반대로 DB 저장 후 invalidation이 실패하면 stale cache가 남는다.

두 저장소를 하나의 원자 transaction으로 묶을 수 없으므로 실패 정책을 endpoint별로 정했다. 사용자에게 재시도 가능한 오류로 돌려야 하는 설정과, DB 저장은 유지하되 cache 삭제 실패를 구조화 로그로 남기는 경로를 구분했다. 중요한 것은 실패를 catch로 삼켜 성공처럼 보이게 하지 않는 것이다.

더 강한 일관성이 필요하면 outbox나 versioned cache key를 고려할 수 있지만, 당시 설정 변경 빈도와 구조에서는 targeted invalidation과 명시적 오류가 가장 단순했다.

회귀 테스트를 호출 구조에 붙였다

공통 함수가 두 key를 잘 삭제하는 테스트만으로는 부족하다. 새 endpoint가 공통 함수를 호출하지 않으면 같은 문제가 반복된다.

주요 설정 저장 경로에서 적절한 invalidator가 호출되는지, 전달된 chatbot ID가 맞는지, cache 실패 처리 계약이 유지되는지 테스트했다. migration과 관리자 변경처럼 양쪽 runtime에 영향을 주는 경로는 두 cache 삭제를 고정했다.

새 설정 API를 만들 때 key 문자열을 직접 쓰는 대신 공통 helper를 import하는 기존 패턴이 보이도록 코드 구조와 README도 함께 정리했다.

TTL은 무효화의 대체가 아니다

모든 cache에 TTL이 있으면 언젠가 최신화된다. 하지만 TTL이 하루라면 설정 변경 직후 사용자는 하루 동안 잘못된 동작을 볼 수 있다. TTL을 매우 짧게 하면 stale window는 줄지만 hit rate가 낮아지고 DB 부하가 커진다.

TTL은 invalidation message가 유실됐을 때 stale 상태가 영구히 남지 않게 하는 상한이다. 사용자가 저장 직후 변경을 기대하는 설정에는 write-triggered invalidation이 여전히 필요하다.

Redis의 cache-aside 가이드도 write 시 cache entry를 삭제하고 다음 read가 정본에서 다시 채우는 방식을 기본 패턴으로 설명한다. 이번 문제는 그 DEL 대상이 reader마다 달랐다는 데 있었다.

지금 다시 한다면

새 cache를 추가할 때 read path와 함께 invalidation owner를 반드시 적는다. cache 설계 문서에는 key pattern만이 아니라 다음을 포함할 것이다.

  • 정본 저장소와 cache value schema
  • key builder의 source of truth
  • TTL과 허용 stale window
  • 어떤 write event가 삭제하는가
  • invalidation 실패가 caller에 어떻게 보이는가
  • 전체 삭제가 허용되는 긴급 절차

장기적으로 legacy reader가 사라지면 두 key를 영구히 유지하지 않고 사용처를 측정한 뒤 legacy key와 helper를 함께 제거한다. 공통화는 오래된 구조를 영원히 보존하기 위한 것이 아니다.

핵심은 cache key를 한 파일로 옮기는 작업이 아니다. reader가 늘어날 때 writer가 모든 cache 일관성 책임을 기억에 의존하지 않도록, key와 invalidation을 코드 계약으로 만드는 것이다.

Cloudturing에서는

Cloudturing은 관리자가 변경한 챗봇 설정을 실제 대화에 빠르게 반영하기 위해 설정 저장 경로에 공통 cache invalidation 계약을 적용하고 있다. 설정을 읽는 runtime이 여러 개여도 관련 cache key를 함께 삭제해 오래된 응답 설정이 남는 시간을 줄인다.

참고 자료