TTL과 캐시 무효화
무효화 방식
- TTL 만료: 가장 단순. 불일치를 허용할 시간을 초 단위로 명시한다
- 키 삭제: 쓰기 트랜잭션 커밋 후 관련 키를
DEL
- 버전 키: 키에 버전을 넣어
user:42:v7처럼 쓰고 구버전은 자연 만료시킨다
- 조건부 재검증: HTTP
ETag / If-None-Match로 변경 없으면 304 응답
TTL 선택 기준
| 데이터 | TTL 감각 | 이유 |
|---|
| 코드와 설정 목록 | 수십 분 이상 | 거의 변하지 않음 |
| 상품과 프로필 | 수 분 | 변경되지만 즉시성 요구 낮음 |
| 재고와 잔액 | 수 초 또는 캐시 안 함 | 불일치 비용이 큼 |
수명을 정하는 것은 제품 결정이다
기술로 정할 수 없습니다. 얼마나 낡아도 되는지를 정해야 나오는 값입니다.
| 데이터 | 허용 | 수명 |
|---|
| 상품 설명 | 몇 분 늦어도 된다 | 길게 |
| 재고 수량 | 거의 실시간 | 아주 짧게 또는 캐시 안 함 |
| 남의 프로필 | 몇 시간 늦어도 된다 | 길게 |
| 내 설정 | 즉시 보여야 한다 | 지우기로 다룬다 |
마지막 줄이 요점입니다. 즉시 반영이 필요하면 수명이 아니라 지우기입니다. 수명을 아주
짧게 잡아 흉내 내면 적중률만 잃습니다.
수명이 한꺼번에 끝나면
같은 시각에 채운 키들은 같은 시각에 만료됩니다. 배포 직후나 캐시를 비운 뒤에 특히 그렇습니다.
그 순간 모두가 원본으로 몰린다
수명에 약간의 무작위를 섞어 만료를 흩는다
이것은 키가 많을수록 심해집니다. 하나가 만료되는 것은 문제가 아니지만 만 개가 동시에
만료되는 것은 문제입니다.
실무 포인트
- 키에 정렬, 필터, 페이지 같은 파라미터를 모두 담는다. 조건이 다른 목록이 같은 키를 쓰면 잘못된 응답이 나간다
- 삭제는 반드시 커밋 이후에. 커밋 전에 지우면 롤백 시 캐시가 잘못 채워진다
- 대량 키에 동일 TTL을 주면 만료가 한꺼번에 몰린다. TTL에 지터를 더해 분산한다
- 무효화 누락은 조용히 오래 살아남는다. 캐시 키 생성과 삭제 지점을 코드에서 한곳으로 모은다
Q.캐시 무효화 전략에는 어떤 것이 있고 각각 언제 쓰나요?
| 전략 | 내용 | 언제 |
|---|
| 만료 시간 | 일정 시간 뒤 자동으로 사라진다 | 약간의 지연을 허용할 때. 가장 단순하다 |
| 쓰기 시 삭제 | 데이터를 바꿀 때 함께 지운다 | 변경 지점이 명확할 때 |
| 변경 로그 구독 | DB 변경 이벤트를 받아 지운다 | 변경 경로가 여러 개일 때 |
| 버전 키 | 키에 버전을 넣고 버전을 올려 통째로 무효화 | 묶음 단위로 갱신할 때 |
| 태그 기반 | 관련 키에 태그를 붙여 함께 지운다 | 한 변경이 여러 키에 영향을 줄 때 |
실무에서는 만료 시간과 쓰기 시 삭제를 함께 씁니다. 삭제가 실패하거나 놓친 경로가 있어도 만료로 수렴합니다.
변경 로그 구독이 강한 이유는 애플리케이션 경로와 분리돼 있어서입니다. 관리자 화면이나 배치가 직접 DB 를 고쳐도 잡힙니다.
흔한 실수: 쓰기 시 삭제만 두는 것. 코드에서 놓친 경로가 반드시 생기고, 그때 옛 값이 영구히 남습니다.
Q.TTL은 어떤 기준으로 정하나요?
허용 가능한 어긋남 시간에서 거꾸로 잡습니다. 성능이 아니라 정합성 요구가 기준입니다.
| 데이터 | 허용 지연 | TTL |
|---|
| 상품 상세 정보 | 몇 분 | 5분에서 10분 |
| 목록과 검색 결과 | 수십 초 | 30초에서 1분 |
| 인기 순위 | 몇 분 | 1분에서 5분 |
| 설정값, 코드 테이블 | 몇 시간 | 1시간 이상 |
| 잔액, 재고 | 없다 | 캐시하지 않거나 아주 짧게 |
그 다음에 조회 빈도를 봅니다. 초당 1,000회 조회되는 데이터는 TTL 10초만 두어도 DB 부하가 1만분의 1로 줄어듭니다. 짧은 TTL 로도 대부분의 효과가 나옵니다.
만료 시각이 몰리지 않게 편차를 더하는 것도 함께 갑니다.
흔한 실수: 길게 잡을수록 좋다고 보는 것. 히트율은 오르지만 어긋난 값이 그만큼 오래 남습니다. 그리고 길게 잡으면 무효화 실패를 만료가 덮어주지 못합니다.
Q.트랜잭션 커밋 전에 캐시를 삭제하면 어떤 문제가 생기나요?
삭제와 커밋 사이에 조회가 끼어들면 옛 값을 다시 캐시에 채웁니다.
| 순서 | 동작 | 결과 |
|---|
| 1 | 캐시 삭제 | 캐시가 비었다 |
| 2 | 다른 요청이 조회 | DB 에서 아직 커밋 안 된 옛 값을 읽는다 |
| 3 | 그 값을 캐시에 채운다 | 옛 값이 박힌다 |
| 4 | 트랜잭션 커밋 | DB 는 새 값, 캐시는 옛 값 |
만료까지 어긋난 채로 남습니다. 롤백되는 경우도 문제입니다. 캐시를 이미 지웠으니 불필요한 미스가 발생합니다.
그래서 커밋 후에 삭제합니다. 프레임워크의 커밋 후 훅을 쓰면 안전하게 붙일 수 있습니다.
커밋 후 삭제가 실패할 수 있으므로 만료 시간을 함께 두거나, 짧은 지연 뒤 한 번 더 지우는 방법을 씁니다.
흔한 실수: 트랜잭션 안에서 캐시를 조작하는 것. 캐시는 트랜잭션에 참여하지 않으므로 롤백해도 되돌아가지 않습니다.
Q.같은 API인데 정렬 조건만 다를 때 캐시 키를 어떻게 설계하나요?
키에 응답을 결정하는 모든 요소를 넣되, 조합이 폭발하지 않게 제한합니다.
캐시 키 = 자원 종류 + 필터 + 정렬 + 페이지 + 버전
예: products:cat=10:sort=price_asc:page=1:v3
| 원칙 | 내용 |
|---|
| 허용 목록으로 정규화 | 정렬은 미리 정한 몇 가지만. 임의 문자열을 키에 넣지 않는다 |
| 순서 고정 | 파라미터 순서가 달라도 같은 키가 되게 정렬한다 |
| 기본값 명시 | 생략된 파라미터를 기본값으로 채워 키를 통일한다 |
| 무의미한 파라미터 제외 | 추적용 파라미터는 키에서 뺀다 |
| 버전 포함 | 스키마가 바뀌면 버전을 올려 통째로 무효화한다 |
첫 번째가 히트율과 보안을 함께 지킵니다. 사용자 입력을 그대로 키에 넣으면 조합이 무한해지고, 메모리를 채우는 공격 표면이 됩니다.
흔한 실수: 개인화 요소를 키에서 빠뜨리는 것. 로그인 사용자별로 결과가 다른데 키가 같으면 남의 응답이 나갑니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
캐시/Redis 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.