Post

캐시는 어디에 두어야 하는가, 뇌과학에게 물어봤다

캐시를 도입할 때마다 자주 같은 고민이 든다 — 이걸 로컬에 둘까, 리모트에 둘까. 그러다 문득 이런 생각이 들었다. 사람의 기억도 이렇게 나뉘어 있지 않을까? 순간적으로 빠르게 쓰고 버리는 기억(작업기억, 흔히 말하는 단기기억)과, 오래 남기려고 따로 옮겨두는 기억(장기기억)이 따로 있는 것처럼. 그래서 이번엔 뇌과학 쪽을 같이 확인해봤다.

“얼마나 빠른가”가 다가 아니다

캐시를 어디에 둘지는 속도만의 문제가 아니다. 두 가지를 같이 봐야 한다. 하나는 혼자 기억할지, 검색 가능하게 공유해둘지(정합성/공유의 문제)고, 다른 하나는 그 공유된 곳에 접근이 끊겼을 때도 버틸 수 있는지(가용성의 문제)다.

로컬 캐시(Caffeine, Guava, 인메모리 Map)는 접근이 제일 빠르다. 대신 용량이 제한적이고, 프로세스가 재시작되면 날아가고, 인스턴스마다 따로 기억해서 서버 A와 서버 B가 서로 다른 값을 들고 있을 수 있다. 딱 개인의 머릿속 기억 같은 존재다.

리모트 캐시(Redis, Memcached)는 네트워크 홉이 있어 로컬보다 느리다. 대신 여러 인스턴스가 공유해서 항상 같은 값을 보고, 재시작해도 살아있고, 색인돼서 검색이 가능하다. 딱 도서관 서가 같은 존재다 — 개인이 다 외우고 있지 않아도, 색인을 따라가면 누구든 같은 자료를 찾아낼 수 있다.

예전에 읽은 제텔카스텐 책(쇤케 아렌스, 《스마트하게 메모하기》)에 이런 내용이 있었다. 사람은 잊어버리기 때문에, 오히려 원하는 정보를 다시 잘 찾아내는 능력이 중요하다는 것이다. 생각해보면 책이 뒤에 색인을 달아두는 이유도 같다 — 내용을 전부 외우게 만드는 대신, 필요할 때 바로 찾아갈 수 있게 색인을 만들어둔다. DB의 인덱스도, 리모트 캐시도 결국 같은 역할을 한다. 모든 인스턴스가 각자 다 기억(로컬 캐시에 다 우겨넣기)하려 하지 말고, 공유된 곳에 색인해서 검색 가능하게(리모트 캐시) 만드는 게 나을 때가 많다는 논리와 정확히 같다.

기억 공고화, 그리고 멀티레벨 캐시

뇌과학에는 기억 공고화(memory consolidation)라는 개념이 있다. 작업기억(단기기억)은 해마에서 빠르게 처리되지만 휘발성이 강하다. 시간이 지나면서, 특히 수면 중에, 이 정보가 안정적인 장기기억으로 “공고화”된다.

이건 멀티레벨 캐시(L1 로컬 → L2 리모트)의 승격(promotion) 구조와 거의 같다. 자주, 혹은 최근에 쓰인 값은 일단 빠른 로컬(작업기억)에 두고, 그게 지속적으로 유의미하다고 판단되면 리모트(장기기억)로 옮겨서 안정적으로 공유하고 보존한다.

리모트가 끊기면 어떻게 되는가

리모트 캐시는 네트워크 의존성을 하나 더 만든다. Redis가 죽거나 네트워크가 끊기면, 그 순간 캐시가 통째로 사라진 것과 같다. 이때 애플리케이션이 어떻게 반응하도록 설계했는지가 실제로는 속도보다 더 중요할 때가 많다.

  • Fail-open(캐시 없이 원본 직행): 데이터는 안전하지만, 캐시가 막아주던 트래픽이 그대로 원본(DB)에 실려서 순간적으로 두들겨 맞는다. 이걸 thundering herd 혹은 cache stampede라고 부른다.
  • Fail-closed(에러 반환): 데이터 부정확 위험은 없지만 서비스 자체가 멈춘다.
  • 로컬 캐시를 fallback으로 병행: 평소엔 리모트를 쓰다가, 리모트 접근이 실패하면 로컬에 남아있는 값(약간 오래됐더라도)으로 대체 응답한다. 완전한 실패보다 살짝 stale한 응답이 나을 때가 많다.

“기억하기보다 검색하기가 중요하다”는 원칙도, 사실은 검색할 수 있는 경로(책, 인터넷, 동료)가 항상 살아있다는 전제 위에서만 성립한다. 그 경로가 끊길 수 있다는 걸 감안하면, 완전히 다 외부화하지 말고 최소한의 로컬 기억, 즉 대략적인 감을 fallback으로 남겨두는 게 안전하다.

실제로는 이렇게 쓰고 있다

상품 메타 정보(약관, 상품 설명 등)를 관리할 때, 원본을 매 1분마다 폴링해서 로컬에 동기화해두는 방식을 쓰고 있다. 여기에 원본이 바뀌면 즉시 동기화되는 이벤트 기반 경로를 같이 둬서, 평소엔 이벤트로 거의 실시간에 가깝게 반영되고, 혹시 이벤트가 유실되더라도 최대 1분 안에는 폴링이 그 차이를 잡아준다.

정합성도 중요하고, 이벤트 전달이 100% 보장되진 않는다는 걸 알기 때문에 택한 구조다. 이벤트(빠른 주경로)와 주기적 폴링(느리지만 확실한 안전망)을 병행하고, 로컬(빠른 응답)과 리모트(원본, 정합성의 기준점)를 같이 쓴다 — 앞서 말한 병행 전략이 실제로 이렇게 돌아간다.

정리하면

상황추천
여러 인스턴스가 항상 같은 값을 봐야 함(정합성 중요)리모트
인스턴스마다 값이 살짝 달라도 무방로컬 가능
값이 크고 무거워서 로컬 메모리에 부담됨리모트
네트워크 홉조차 감당 안 되는 초저지연 필요로컬
정합성도 중요하고, 리모트 장애 시에도 서비스가 죽으면 안 됨병행(로컬을 리모트의 fallback으로, 혹은 L1/L2 멀티레벨)
리모트 장애 시 서비스가 잠깐 멈춰도 괜찮음(정확성이 가용성보다 우선)리모트 단독

실무에서 트래픽이 몰리는 서비스는 결국 병행으로 수렴하는 경우가 많다. 평소엔 리모트로 정합성을 지키고, 리모트가 흔들릴 때는 로컬로 최소한의 서비스를 유지하는 식이다.

결국 이건 세 가지 축의 트레이드오프다. 정합성(공유가 필요한가), 속도(초저지연이 필요한가), 가용성(연결이 끊겨도 버텨야 하는가). “빠르지만 고립된 개인 기억”과 “약간 느리지만 일관된 공유 색인 저장소” 중 하나를 고르는 문제가 아니다. 평소엔 후자를 쓰되 전자를 안전망으로 같이 들고 있는 게, 실무에서는 정답인 경우가 많았다.

참고


이 글은 업무든 일상이든, 거기서 얻은 통찰을 컴퓨터 과학 개념에 빗대어 풀어보는 '경험 디버깅(exp-debug)' 연재입니다. 실제 경험에서 얻은 인사이트를 바탕으로, AI와 함께 정리하고 다듬었습니다.

This post is licensed under CC BY 4.0 by the author.