포스트

캐시 톺아보기 (3) — 토폴로지와 실전 고려사항


Application Cache 토폴로지


Local Cache

다이어그램은 Server1, Server2, Server…n 각각이 내부에 독립적인 Cache와 App을 가진 구조를 보여준다.

┌─Server1─┐ ┌─Server2─┐ ┌─Server..n┐ │ Cache │ │ Cache │ │ Cache │ │ App │ │ App │ │ App │ └──────────┘ └──────────┘ └───────────┘ (서로 독립, 각자의 캐시)

특징:

  • 각 서버가 자기 프로세스 메모리(Heap) 안에 캐시를 유지한다. (Part 1에서 다룬 CPU Cache(L1/L2/L3)는 CPU와 RAM 사이에 위치하는 하드웨어 캐시지만, 여기서 말하는 Local Cache는 Caffeine/Guava 같은 소프트웨어 캐시로, Java 객체로서 JVM Heap 메모리 에 저장된다. 계층이 다르다.)
  • 장점: 네트워크 I/O 없이 메모리에서 직접 접근하므로 속도가 가장 빠르다. Redis 왕복 ~0.5ms vs Local 접근 ~수 ns.
  • 단점:
    • 서버 간 캐시 데이터가 불일치할 수 있다 (Server1은 갱신됐지만 Server2는 옛날 데이터).
    • 서버 자원(메모리)을 소비한다 → 서버당 캐시 크기 제한.
    • 캐시 데이터 변경 시 다른 서버에 전파하려면 clustering/replication 필요 → scale-out 할수록 동기화 비용 증가.
  • 대표 구현: Caffeine (Java), Guava Cache, EhCache(local mode)
  • 적합한 사례: 변경이 거의 없고 모든 서버에 동일한 데이터가 필요한 경우 (설정값, 코드 테이블 등)

Global Cache

다이어그램은 Server1, Server2, Server..n이 모두 하나의 외부 캐시 서버에 연결된 구조를 보여준다.

┌─Server1─┐ │ App │──┐ └──────────┘ │ ┌─Server2─┐ │ ┌──────────────┐ │ App │──┼───▶│ Cache Server │ └──────────┘ │ │ (Redis) │ ┌─Server..n┐ │ └──────────────┘ │ App │──┘ └───────────┘ (모든 서버가 하나의 캐시를 공유)

특징:

  • 별도의 캐시 서버(Redis, Memcached 등)를 두고 모든 애플리케이션 서버가 접근한다.
  • 장점:
    • 서버 간 데이터 공유가 쉽다. 한 서버가 갱신하면 다른 서버도 동일한 최신 데이터를 읽는다.
    • 캐시 데이터 변경 시 추가 동기화 작업이 불필요하다.
    • Scale-out 할수록 효율이 좋다 (서버가 늘어나도 캐시는 1곳).
  • 단점:
    • 네트워크 I/O가 필요하므로 Local Cache보다 느리다.
    • 캐시 서버 장애 시 모든 서버에 영향.
  • 데이터 분산:
    • Replication: Master/Slave로 동일 데이터를 복제. 읽기 분산, 고가용성 확보.
    • Sharding: 같은 스키마의 데이터를 여러 노드에 분산 저장. 단일 노드 용량 한계 극복.

Distributed Cache

다이어그램은 로드밸런서가 여러 서버로 트래픽을 분배하고, 각 서버가 분산된 캐시 클러스터(shard 0, 1, 2)에 접근하는 구조를 보여준다.

┌─────────────────────────────┐ LB ──▶ Server ──┐ │ Distributed Cache │ LB ──▶ Server ──┼─────▶│ [Shard0] [Shard1] [Shard2] │ LB ──▶ Server ──┘ └─────────────────────────────┘

Global Cache와의 차이:

  • Global Cache는 단일(또는 Master-Slave 구조) 캐시 서버.
  • Distributed Cache는 데이터를 여러 노드에 샤딩하여 분산 저장. 캐시 사이즈나 네트워크 용량이 단일 Global Cache의 한계를 넘을 때 사용한다.

대표 구현: Redis Cluster

Redis Cluster는 Hash Slot 방식으로 데이터를 분산한다. 전체 키 공간을 16384개의 슬롯으로 나누고, 각 노드가 슬롯 범위를 나누어 담당한다.

슬롯 할당 공식: HASH_SLOT = CRC16(key) mod 16384

예: 3개 노드 클러스터 key: "user:1001" CRC16("user:1001") mod 16384 = 5765 → Node B가 담당 ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Node A │ │ Node B │ │ Node C │ │ slot 0-5460 │ │ slot 5461- │ │ slot 10923- │ │ │ │ 10922 │ │ 16383 │ └─────────────┘ └──────┬──────┘ └─────────────┘ │ "user:1001" → slot 5765 → 여기에 저장
  • 왜 16384개?: 각 노드는 heartbeat 메시지로 자신이 담당하는 슬롯 비트맵을 전송한다. 16384bit = 2KB로 네트워크 부담이 작다. 65536개면 8KB가 되어 부담이 커진다.
  • Hash Tag: 키에 {...} 패턴이 있으면 중괄호 안의 문자열만 해시한다. 예: {user:1001}:name{user:1001}:age는 같은 슬롯에 배치 → 멀티키 연산 가능.
  • 노드 추가/삭제: 슬롯 범위를 재분배하고 해당 슬롯의 데이터만 이동한다. 전체 데이터 재배치가 아닌 부분 마이그레이션이므로 영향이 제한적이다.

Consistent Hashing (일관된 해싱)

분산 캐시에서 노드가 추가되거나 삭제될 때 키 재매핑을 최소화하는 기법이다.

일반 해싱의 문제:

  • hash(key) % N으로 노드를 결정. N이 변하면(노드 추가/삭제) 거의 모든 키가 다른 노드로 재매핑되어 캐시가 대량으로 무효화(cache stampede)된다.

Consistent Hashing의 해결:

  • 노드와 키를 모두 동일한 해시 링(hash ring) 위에 배치한다.
  • 키는 링 위에서 시계 방향으로 가장 가까운 노드에 매핑된다.
  • 노드가 추가/삭제되면, 평균적으로 K/n개의 키만 재매핑된다 (K: 전체 키 수, n: 노드 수).
  • 예: 100만 개의 키, 10개 노드 → 노드 1개 추가 시 약 10만 개(10%)만 이동. 일반 해싱이면 거의 100만 개가 이동.
  • 실제 사용: Twitter의 Twemproxy가 Redis/Memcached 앞단에서 Consistent Hashing으로 키를 분배한다.

Reverse Proxy 캐시

다이어그램은 AWS의 DynamoDB Accelerator(DAX)를 예시로 보여주며, 이는 Read+Write Through 방식의 Reverse Proxy 캐시다.

Client ──▶ DAX Cluster(Cache) ──▶ DynamoDB (Read Through + Write Through)

Reverse Proxy 캐시의 동작:

  • 클라이언트와 Origin 서버 사이에 위치하여, 클라이언트의 모든 요청을 가로챈다.
  • 읽기(Read Through): 캐시에 있으면 반환, 없으면 Origin에서 가져와 캐시 후 반환.
  • 쓰기(Write Through): 캐시에 먼저 기록하고, 캐시가 즉시 Origin에도 동기 반영한다.
  • 클라이언트는 캐시의 존재를 알 필요가 없다 (transparent).

대표 구현:

  • Nginx microcaching: Nginx가 reverse proxy로서 응답을 짧은 시간(1초 등) 캐싱. 트래픽 급증 시 Origin 부하를 극적으로 줄인다.
  • CDN (CloudFront, Akamai 등): 전 세계에 분산된 엣지 서버가 콘텐츠를 캐싱.
  • DAX: DynamoDB 전용 인메모리 캐시. API 호환이므로 애플리케이션 코드 변경 없이 적용 가능.

Buffer vs Cache

구분 Cache Buffer
목적 재사용 — 동일 데이터를 반복 접근할 때 속도 향상 완충 — 속도 차이가 큰 장치 사이에서 고속 장치의 대기를 줄임
데이터 보존 조회 후에도 삭제하지 않음 (TTL/Eviction 전까지 유지) 한 번 소비(전달)하면 삭제
용량 상대적으로 작음 캐시보다 일반적으로 큼
대표 예시 CPU L1~L3 캐시, Redis, CDN 프린터 버퍼, I/O 버퍼, TCP 송수신 버퍼

프린터 버퍼 예시: PC가 인쇄 데이터를 프린터 버퍼에 넣으면 PC는 다른 작업을 계속할 수 있다. 프린터는 버퍼에서 데이터를 꺼내 인쇄한다. 버퍼가 없으면 프린터(저속 장치)가 인쇄를 마칠 때까지 PC(고속 장치)가 대기해야 한다.


캐시 적용 시 고려사항 정리

고려사항 설명
Capacity 캐시에 얼마나 많은 데이터를 저장할 것인가? 파레토 법칙(20%)을 기준으로 산정
Replacement Policy LRU, LFU, FIFO 중 워크로드 특성에 맞는 정책 선택
Hit Rate 목표 적중률 설정 및 모니터링. 적중률이 낮으면 캐시 의미 없음
Read-Write Strategies 읽기/쓰기 비율, 일관성 요구 수준에 따라 전략 조합
Coherence 분산 환경에서 캐시 간 일관성 유지 방법
Expiration TTL 설정 — 너무 짧으면 Miss 증가, 너무 길면 stale 데이터
Eviction 용량 초과 시 어떤 항목을 제거할지 정책 설정

캐시 적용이 적합한/부적합한 케이스

적합한 케이스:

  • 원본 데이터 접근 시간이 오래 걸리는 경우
  • 반복적으로 동일한 결과를 반환하는 경우
  • 업데이트가 자주 발생하지 않는 데이터
  • 자주 조회되는 데이터 (Hot Data)
  • 입력값과 출력값이 일정한 데이터

부적합한 케이스:

  • 잦은 데이터 변경: 캐시를 갱신하는 비용이 캐시의 이점을 상쇄
  • 데이터 일관성이 실시간으로 보장되어야 하는 경우 (재고, 결제 잔액)
  • 접근 패턴이 균등하여 Hot Data가 없는 경우 (hit ratio가 낮아짐)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.