Cache Systems
Cache Systems
Cache Systems
Client-side Cache
- 정의: 서버로 요청을 보내기도 전에, 사용자의 브라우저/모바일 기기 로컬에 저장해둔 응답을 그대로 재사용하는 캐시다. 네트워크 왕복 자체가 발생하지 않는다는 점에서 아래 CDN·Load Balancer 캐시(요청은 서버까지 가되 더 가깝거나 더 빠른 계층에서 응답)와 결정적으로 다르다.
- 캐싱하는 데이터:
- HTTP 캐시(
Cache-Control,ETag,Last-Modified) — JS/CSS 번들, 이미지, 폰트 같은 정적 리소스. localStorage/IndexedDB— 사용자 설정값, 로그인 세션 토큰, 자주 안 바뀌는 API 응답(사용자 프로필 등).- Service Worker 캐시 — 오프라인에서도 떠야 하는 앱 셸(app shell), API 응답.
- HTTP 캐시(
- 이런 곳에 적용하면 좋다:
- 빌드 시 해시가 붙는 정적 자산(
main.a1b2c3.js): 내용이 바뀌면 파일명(URL) 자체가 바뀌므로Cache-Control: max-age=31536000, immutable로 사실상 영구 캐싱해도 안전하다. 대부분의 SPA 배포(Webpack/Vite 빌드 산출물)가 이 패턴을 쓴다. - 반복 방문이 잦은 서비스: Gmail, Twitter(X) 같은 PWA는 Service Worker로 앱 셸을 캐싱해 재방문 시 로딩 화면 없이 바로 뜨게 한다. 오프라인 상태에서도 최소 UI를 보여줄 수 있다.
- 자주 조회되지만 실시간성이 필요 없는 개인 데이터: 모바일 앱이 사용자 프로필/설정을 로컬 DB에 캐싱해두고, 앱 실행 시 캐시를 먼저 보여준 뒤 백그라운드에서 최신화(stale-while-revalidate)하는 방식.
- 단,
index.html처럼 배포 시 즉시 바뀌어야 하는 진입점 파일은no-cache로 두어 매번 서버에 최신 여부를 확인하게 해야 한다 — 그렇지 않으면 배포 후에도 사용자가 예전 번들을 계속 캐싱해서 쓰는 문제가 생긴다.
- 빌드 시 해시가 붙는 정적 자산(
CDN(Static Data)
- 정의: 전 세계에 분산된 엣지(edge) 서버에 오리진(origin) 서버의 데이터를 미리 복제해두고, 사용자와 지리적으로 가까운 엣지가 대신 응답하게 만드는 캐시 계층이다. 요청은 서버(엣지)까지 가지만, 오리진까지 왕복하지 않아도 되므로 지연시간을 크게 줄인다.
- 캐싱하는 데이터: 이미지, 동영상, JS/CSS 번들, 폰트 같은 정적 파일이 기본이지만, 자주 바뀌지 않는 API 응답(뉴스 헤드라인, 상품 목록 첫 페이지, 이벤트 배너)도 짧은 TTL로 캐싱해 “정적에 가깝게” 취급하는 경우가 많다.
- 이런 곳에 적용하면 좋다:
- 전 세계에 사용자가 흩어져 있고 지연시간이 중요한 서비스: Netflix/YouTube는 각 ISP 내부에 자체 캐시 서버(Netflix Open Connect, Google Global Cache)를 두고 동영상을 캐싱해, 사용자가 대륙을 건너 오리진까지 스트리밍받지 않아도 되게 한다.
- 트래픽이 몰리는 정적 자산으로 오리진 부하를 줄여야 할 때: 이커머스 상품 이미지, 프론트엔드 빌드 파일을 CloudFront/Cloudflare/Akamai 같은 CDN에 올려두면, 트래픽이 급증해도 오리진 서버는 캐시 미스일 때만 호출된다.
- read 비중이 압도적으로 높고 변경 빈도가 낮은 콘텐츠: 뉴스 사이트의 기사 본문, 블로그 정적 페이지처럼 한 번 쓰이고 수많은 사용자가 읽기만 하는 데이터.
- 반대로 장바구니, 결제 화면, 로그인 후 대시보드처럼 사용자마다 응답이 달라지는(개인화된) 동적 콘텐츠는 CDN 캐싱에 부적합하다 — 이런 요청은 CDN을 그냥 패스스루로 통과해 오리진까지 가야 한다.
Load Balancer(Cache)
- 정의: 로드밸런서/리버스 프록시 계층(Nginx, Varnish, Envoy 등)에서 요청을 백엔드 애플리케이션 서버로 넘기기 전에, 동일한 요청에 대한 응답을 자체적으로 짧게 캐싱해두는 것이다. CDN이 지리적으로 먼 사용자를 위한 캐시라면, 이쪽은 같은 리전 안에서 애플리케이션 서버 앞단 한 곳에 걸어두는 캐시에 가깝다.
- 캐싱하는 데이터: 동일한 URL/쿼리스트링 조합으로 반복해서 들어오는 GET 요청의 응답 전체 — 상품 상세 API, 랭킹/인기글 목록처럼 여러 사용자가 공통으로 조회하는(개인화되지 않은) 응답.
- 이런 곳에 적용하면 좋다:
- 마이크로캐싱(microcaching): Nginx
proxy_cache로 TTL을 1~5초처럼 아주 짧게 잡아, 완전히 실시간은 아니어도 되지만 초당 요청이 몰리는 엔드포인트(상품 상세, 실시간 랭킹)를 캐싱한다. 짧은 staleness는 허용하면서 백엔드 부하는 크게 줄일 수 있다. - 플래시 세일/타임딜처럼 순간 트래픽이 폭증하는 상황: 같은 상품 페이지에 수만 명이 동시에 몰릴 때, 백엔드가 매 요청마다 DB를 조회하는 대신 로드밸런서 단에서 1초짜리 캐시로 응답해버리면 DB 커넥션 폭주를 막을 수 있다. Varnish가 이런 HTTP 가속기 용도로 전통적으로 많이 쓰인다.
- 응답이 사용자에 따라 달라지지 않는 공개 API: 인증 토큰이나 세션에 따라 응답이 바뀌지 않는 요청일수록(즉
Vary헤더로 인해 캐시 키가 사용자마다 갈라지지 않을수록) 캐시 적중률이 높아져 효과가 커진다. - 반대로 사용자별 개인화 응답이나 매 요청 최신성이 필수인 결제/재고 확인 같은 엔드포인트는 로드밸런서 캐싱 대상에서 제외해야 한다 — 짧은 TTL이라도 재고 초과 판매 같은 사고로 이어질 수 있다.
- 마이크로캐싱(microcaching): Nginx
Nginx는 캐시를 어디에 저장하는가
- Nginx의
proxy_cache는 Redis/Memcached 같은 별도 외부 저장소 없이, Nginx가 떠 있는 서버의 로컬 디스크에 응답을 파일로 저장하는 방식이다.
1
2
3
4
5
6
7
8
9
10
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;
server {
location /api/ {
proxy_pass http://backend;
proxy_cache my_cache;
proxy_cache_valid 200 5s;
proxy_cache_key "$scheme$request_method$host$request_uri";
}
}
keys_zone=my_cache:10m: 캐시 키와 메타데이터(만료 시각, 상태 등)를 저장하는 공유 메모리 영역. 응답 본문이 아니라 “이 키가 어느 디스크 파일에 있는지”를 가리키는 인덱스만 담는다. 10m이면 대략 8만 개 안팎의 키를 관리할 수 있다(키 1개당 약 128바이트).- 실제 응답 본문(body):
/var/cache/nginx아래 디스크 파일로 저장된다.levels=1:2는 캐시 키를 해시해 하위 디렉토리 2단계로 분산 저장함으로써 한 디렉토리에 파일이 몰리는 것을 막는 옵션이다. proxy_cache_key: 어떤 기준으로 같은 요청인지 판단할지 정하는 키. 기본값은 URL 전체(스킴+메서드+호스트+URI)라서 쿼리스트링이 다르면 다른 키로 취급된다.proxy_cache_valid: 백엔드 응답 상태 코드별 캐시 유지 시간(TTL). 오리진이Cache-Control/Expires헤더를 내려주면 그걸 우선 따르고, 없을 때 이 설정값을 쓴다.inactive=60m: 60분 동안 한 번도 조회되지 않은 캐시 항목은 만료 전이라도 삭제하는 청소 정책.max_size를 넘으면 오래된 항목부터 제거한다(LRU와 유사).
1
2
3
4
5
6
7
8
9
Client -> Nginx: GET /api/products/1
캐시 MISS: 디스크에 해당 키 파일 없음
-> Nginx가 백엔드에 요청 -> 응답을 받아 디스크에 저장 + 클라이언트에 반환
-> 응답 헤더 X-Cache-Status: MISS
같은 요청 재요청 (TTL 이내):
캐시 HIT: 디스크 파일에서 바로 읽어 응답, 백엔드 호출 없음
-> 응답 헤더 X-Cache-Status: HIT
- 즉 로컬 파일시스템이 곧 캐시 저장소이고, 공유 메모리(
keys_zone)는 그 파일을 빠르게 찾기 위한 인덱스 역할만 한다. 이 캐시는 Nginx 인스턴스 하나에 종속된 로컬 캐시라서, 여러 대의 Nginx가 로드밸런싱되어 있으면 각 인스턴스가 자기 디스크에 따로 캐싱한다 — 인스턴스 간에 캐시가 공유되지 않는다. 인스턴스 간 캐시를 공유하려면 앞단에 별도 캐시 계층(예: 공용 Varnish/Redis)을 두거나 동일 사용자를 항상 같은 인스턴스로 보내는 sticky routing이 필요하다.
Message Broker(Messages)
- 정의: Producer와 Consumer 사이에서 메시지(이벤트)를 일시적으로, 혹은 정해진 보관 기간(retention) 동안 저장해두는 완충 계층이다. 엄밀히는 “캐시”보다 “큐/로그”에 가깝지만, Producer가 만든 데이터를 Consumer가 실제로 소비할 때까지 대신 들고 있어준다는 점에서 캐시와 같은 역할(느린 쪽을 기다리지 않고 빠른 쪽이 계속 동작하게 함, 원본을 다시 조회하지 않고 저장된 사본을 읽음)을 한다.
- 캐싱하는 데이터: 아직 소비되지 않았거나 retention 기간 내에 있는 이벤트/메시지. Kafka는 이를 디스크에 append-only 로그 세그먼트로 순차 기록하는데, 최근에 쓰였거나 읽힌 세그먼트는 자연스럽게 OS의 page cache에 올라가 있어 대부분의 읽기가 실제 디스크 I/O 없이 메모리에서 처리된다.
- 이런 곳에 적용하면 좋다:
- Producer와 Consumer의 처리 속도가 다른 시스템: 주문 이벤트가 순간적으로 몰려도, 재고 차감/알림 발송 같은 Consumer가 그보다 느리게 처리해도 메시지가 유실되지 않고 브로커에 쌓여 기다린다. Kafka, RabbitMQ 모두 이 디커플링(decoupling) 역할을 한다.
- 이벤트 재처리/재생(replay)이 필요한 도메인: Kafka는 컨슈머가 읽어갔다고 해서 메시지를 바로 지우지 않고, retention 기간(예: 7일) 동안 그대로 유지한다. 그래서 Consumer가 장애 후 복구되거나, 새로운 Consumer Group이 추가되어도 로그의 처음부터 다시 읽을 수 있다. 로그 파이프라인, CDC(Change Data Capture), 이벤트 소싱 아키텍처에서 핵심적으로 쓰인다.
초고처리량 로그/메트릭 수집: Kafka가 초당 수백만 건의 메시지를 처리할 수 있는 핵심 이유 중 하나가 JVM 힙에 별도의 캐시를 두지 않고 OS page cache를 그대로 재사용하기 때문이다. 여기에 순차 디스크 쓰기(sequential write)와 zero-copy(
sendfile시스템 콜로 page cache의 데이터를 애플리케이션 메모리를 거치지 않고 커널 레벨에서 바로 소켓 버퍼로 전달)를 결합해, 디스크 기반임에도 인메모리 수준의 처리량을 낸다.1 2 3 4 5 6 7 8 9 10
Producer -> Kafka Broker | v [OS Page Cache] <-- 최근 쓰인/읽힌 로그 세그먼트가 상주 | (write: 비동기 flush, read: 대부분 여기서 처리) v [Disk: log segment files] <-- retention 기간 동안 보관 Consumer <- sendfile(zero-copy) <- Page Cache (애플리케이션 힙을 거치지 않고 커널이 직접 소켓으로 전송)- 여러 소비자가 같은 이벤트를 각자 다른 목적으로 소비해야 할 때: 주문 이벤트 하나를 결제, 배송, 추천 시스템이 독립적으로 각자 구독. 브로커는 메시지를 한 번만 저장하고, 여러 Consumer Group에게 동일한 로그를 각자의 오프셋(offset)으로 독립적으로 읽어가게 한다.
- 대표 시스템: Kafka(고처리량 로그 기반, 재생 가능한 이벤트 스트림), RabbitMQ(라우팅/우선순위/재시도 정책이 중요한 태스크 큐), Amazon SQS(완전관리형 단순 큐), Redis Streams(가볍고 지연시간이 짧은 이벤트 스트림).
CPU/RAM/Disk Cache
- 정의: 컴퓨터의 메모리 계층(memory hierarchy)은 “가까울수록 빠르지만 용량이 작고 비싸다”는 원칙으로 층층이 쌓여 있고, 상위 계층이 바로 아래 계층의 데이터를 캐싱하는 구조다 — CPU 캐시는 RAM을, RAM(OS 페이지 캐시)은 디스크를, 필요하면 로컬 디스크가 더 느린 네트워크 스토리지를 캐싱한다. 앞서 다룬 CDN·Load Balancer·Kafka의 캐시도 결국 이 하드웨어 계층 위에서 동작하는 것이다.
1
2
3
4
5
6
7
레지스터 ~0.5ns
L1 캐시 ~1ns (코어별, 32~64KB)
L2 캐시 ~4ns (코어별/코어쌍 공유, 수백KB~수MB)
L3 캐시 ~15ns (전체 코어 공유, 수~수십MB)
RAM ~100ns (OS Page Cache 포함, 수~수백GB)
SSD ~10~100us
HDD / 네트워크 ~1~10ms
CPU Cache (L1/L2/L3)
- 캐싱하는 데이터: 최근에 접근한 메모리 주소의 데이터/명령어를 캐시라인(보통 64바이트) 단위로 저장한다. L1은 명령어 캐시(I-cache)와 데이터 캐시(D-cache)로 나뉘어 코어마다 독립적으로 존재하고, L2는 코어(또는 코어 쌍)별, L3는 전체 코어가 공유한다.
- 이런 곳에 적용하면 좋다: 데이터를 메모리 상에서 연속적으로 접근(sequential access)하도록 구조를 짜면 캐시 적중률이 크게 오른다. 예를 들어 2차원 배열을 행 우선(row-major)으로 순회하면 열 우선으로 순회할 때보다 캐시 미스가 훨씬 적다.
- 게임 엔진의 ECS(Entity Component System): 객체 하나에 위치/속도/체력을 다 몰아넣는 OOP 방식 대신, “모든 엔티티의 위치”를 연속된 배열 하나에 몰아 저장해(data-oriented design) 수만 개 엔티티를 순회하며 갱신할 때 캐시 히트율을 극대화한다.
- 고성능 수치 연산 라이브러리(BLAS, NumPy 내부 구현): 행렬 곱셈에서 블록(tile) 단위로 쪼개 연산해 L1/L2 캐시 안에서 최대한 재사용하도록 최적화한다.
RAM (OS Page Cache)
- 캐싱하는 데이터: 디스크에서 한 번 읽거나 쓴 파일 블록을 커널이 자동으로 RAM에 보관한다. 애플리케이션이 따로 요청하지 않아도 OS가 알아서 관리하며,
free -h로 봤을 때buff/cache로 잡히는 메모리가 이 영역이다. - 이런 곳에 적용하면 좋다: 같은 파일에 반복 접근하는 워크로드일수록 효과가 크다.
- 데이터베이스: PostgreSQL/MySQL은 자체 버퍼풀(아래 Relational Database 섹션 참고)뿐 아니라, 그 아래 OS page cache에도 같은 데이터 파일이 한 번 더 캐싱돼 있어 버퍼풀 미스가 나도 디스크까지 안 가고 page cache에서 채워지는 경우가 많다.
- Kafka: 위 Message Broker 섹션에서 다뤘듯, 자체 애플리케이션 캐시를 두지 않고 OS page cache에 전적으로 의존해 순차 읽기/쓰기 성능을 극대화하는 대표 사례다.
- 반대로 랜덤하게 흩어진 대용량 파일을 한 번씩만 스캔하는 배치 작업(예: 전체 로그 아카이브 백업)은 page cache를 채워도 재사용될 일이 없어 오히려 다른 프로세스가 쓰던 캐시를 밀어내는 부작용(cache pollution)이 날 수 있다.
Disk Cache
- 캐싱하는 데이터: SSD/HDD 컨트롤러 자체에 내장된 작은 DRAM 버퍼로, 쓰기 요청을 실제 낸드/플래터에 기록하기 전에 잠깐 담아두는(write-back) 캐시다.
- 이런 곳에 적용하면 좋다: 쓰기가 잦은 워크로드에서 지연시간을 줄여준다. 다만 이 캐시는 휘발성이라 정전 시 아직 디스크에 반영되지 않은 데이터가 유실될 수 있다 — 그래서 서버/DB용 SSD는 PLP(Power Loss Protection)용 커패시터를 달아, 정전이 나도 캐시에 남은 데이터를 낸드에 마저 기록할 시간을 확보한다. 일반 소비자용 SSD를 DB 서버에 쓰면 안 되는 이유 중 하나다.
- 더 넓게 보면 로컬 SSD가 더 느린 스토리지의 캐시로 쓰이는 계층형 스토리지(tiered storage)도 같은 개념이다. AWS Storage Gateway가 로컬 캐시(SSD)에 자주 쓰는 데이터를 두고, 나머지는 S3 같은 저렴하고 느린 스토리지에 두는 방식이 대표적이다.
Distributed Cache(In-memory Cache)
- 정의: 여러 애플리케이션 서버가 네트워크를 통해 공유하는 별도의 인메모리 저장소(Redis, Memcached 등)에 자주 조회되는 데이터를 저장해두는 캐시다. 각 서버 프로세스 안에만 저장되는 로컬 캐시와 달리 모든 인스턴스가 동일한 캐시를 바라보므로, 서버가 여러 대로 스케일아웃되어도 “어느 서버로 요청이 갔느냐”에 따라 캐시된 값이 달라지는 문제가 없다. Load Balancer 섹션에서 다룬 Nginx
proxy_cache가 인스턴스마다 로컬 디스크에 따로 캐싱하는 것과 정반대 지점에 있다. - 캐싱하는 데이터: 반복적으로 조회되는 DB 쿼리 결과(상품 상세, 사용자 프로필), 세션 데이터(로그인 세션, JWT 블랙리스트), 실시간 카운터/랭킹(좋아요 수, 조회수, 인기글 순위), 레이트 리미팅 카운터, 분산 락(distributed lock).
대표 캐싱 패턴:
1 2 3 4 5 6 7
Cache-Aside (가장 흔한 패턴) App -> Redis: GET key HIT -> 바로 반환 MISS -> DB 조회 -> App이 Redis에 SET -> 반환 Write-Through: 쓰기 시 캐시+DB 동시 반영 (캐시가 항상 최신, 쓰기 지연 증가) Write-Back: 쓰기를 캐시에만 반영, 비동기로 DB에 반영 (쓰기 빠름, 캐시 장애 시 유실 위험)
- 이런 곳에 적용하면 좋다:
- 반복 조회가 많아 DB 부하를 줄여야 할 때: 상품 상세, 사용자 프로필처럼 동일 키로 요청이 몰리는 API에 Cache-Aside를 적용하면 같은 데이터를 매번 DB까지 가지 않고 Redis에서 응답한다.
- 여러 서버 인스턴스 간 상태를 공유해야 할 때: 로드밸런서 뒤에 WAS 인스턴스가 여러 대 떠 있으면, 세션을 각 서버 로컬 메모리에 두는 순간 sticky session 없이는 로그인 상태가 인스턴스마다 달라진다. Redis에 세션을 두면 요청이 어느 서버로 가든 동일한 세션을 조회할 수 있다.
- 실시간 카운터/랭킹: 좋아요 수, 실시간 인기글 랭킹처럼 쓰기가 매우 잦은 값을 DB에 매번
UPDATE로 반영하면 락 경합이 심해진다. Redis의INCR, Sorted Set(ZINCRBY) 같은 원자적 연산으로 처리하고, 주기적으로만 DB에 동기화한다. - 분산 락 / 레이트 리미팅: 여러 서버 인스턴스가 동시에 같은 리소스(재고 차감, 좌석 예약)에 접근하는 것을 막아야 할 때
SETNX기반 분산 락을 쓰거나, API 요청 제한을 sliding-window 알고리즘으로 Redis에 구현한다. - 대표 시스템: Redis(String/Hash/List/Set/Sorted Set 등 다양한 자료구조, Pub/Sub, AOF/RDB로 영속성 옵션 제공), Memcached(순수 key-value, 멀티스레드 구조라 단순 캐시 용도로는 더 가벼움), Amazon ElastiCache/MemoryDB(관리형 Redis/Memcached).
- 고려할 점: 캐시 서버 자체가 단일 장애점(SPOF)이 될 수 있어 Redis Cluster/Sentinel 같은 이중화가 필요하고, 캐시와 DB 사이의 정합성(데이터가 바뀌었을 때 캐시를 언제 무효화할지)을 어떻게 관리할지 — TTL 기반, write-through, 이벤트 기반 invalidation 중 도메인에 맞는 전략을 골라야 한다.
Full-Text Search(Indexed Data)
- 정의: RDB의
LIKE '%keyword%'는 B-Tree 인덱스를 타지 못해 결국 풀스캔에 가까워진다. Elasticsearch/OpenSearch 같은 검색엔진은 원본 텍스트를 형태소 분석(analyze/tokenize)해 역색인(Inverted Index) 이라는 검색 전용 구조로 미리 변환해 저장해둔다. 소스 오브 트루스는 여전히 RDB고, 검색엔진은 그로부터 파생된 비정규화 사본을 들고 있다는 점에서 다른 캐시 계층들과 성격이 같다 — 다만 TTL로 만료시키는 캐시가 아니라, DB 변경사항이 반영될 때까지 계속 동기화해야 하는 파생 인덱스에 가깝다. 캐싱(색인)하는 데이터: 검색 대상 텍스트 필드(상품명, 설명, 게시글 본문, 로그 메시지)를 토큰 단위로 쪼개 “단어 → 그 단어가 등장하는 문서 ID 목록”으로 뒤집어 저장한다. 여기에 필터링/정렬용 구조화 필드(가격, 카테고리, 생성일)도 함께 색인한다.
1 2 3 4 5 6 7 8 9 10 11
원본 문서 doc1: "빨간 운동화 나이키" doc2: "파란 운동화 아디다스" 역색인(Inverted Index) "운동화" -> [doc1, doc2] "나이키" -> [doc1] "아디다스" -> [doc2] "빨간" -> [doc1] 검색: "운동화" 조회 -> 역색인에서 [doc1, doc2] 즉시 반환 (풀스캔 없음)
- 이런 곳에 적용하면 좋다:
- 자유 텍스트 검색이 핵심 기능인 서비스: 이커머스 상품 검색, 문서/이슈 검색(Confluence, GitHub 코드 검색)처럼 사용자가 임의의 단어로 콘텐츠를 찾아야 하는 경우.
LIKE '%keyword%'가 느려지기 시작하는 시점: RDB의 B-Tree 인덱스는'abc%'같은 앞부분 일치(prefix)에는 효과적이지만, 중간 일치('%abc%')에는 인덱스를 타지 못해 테이블이 커질수록 검색이 선형으로 느려진다. 검색 트래픽과 텍스트 컬럼 크기가 늘어날수록 별도 검색엔진으로 옮기는 것이 정석이다.- 관련도(relevance) 기반 정렬이 필요할 때: 단순히 “포함되는가/아닌가”가 아니라 “얼마나 관련 있는가”(TF-IDF, BM25 스코어링)로 순위를 매겨야 하는 검색 결과 — RDB에는 이런 스코어링 기능 자체가 없다.
- 오타 허용, 유사어, 자동완성: n-gram 토크나이저, fuzzy matching, synonym 사전을 설정해 “나이키”를 “나이스”로 잘못 쳐도 결과가 나오게 하거나, 입력 중 자동완성을 제공할 수 있다.
- 대용량 로그/메트릭 검색: ELK 스택(Elasticsearch + Logstash/Fluentd + Kibana)처럼 수집된 로그를 색인해두고, 특정 에러 메시지나 요청 패턴을 초 단위로 검색·집계한다.
- 대표 시스템: Elasticsearch/OpenSearch(범용, 분산, RESTful — Lucene 기반), Apache Solr(Lucene 기반의 오래된 강자), Meilisearch/Typesense(가볍고 빠른 개발자 친화적 검색), Algolia(완전관리형 SaaS).
- 고려할 점: 원본 DB와 검색 인덱스는 물리적으로 분리된 저장소이므로 동기화 파이프라인이 필요하다 — 위 Message Broker 섹션에서 다룬 것처럼 Kafka/CDC를 통해 DB 변경분을 검색엔진에 반영하는 구조가 흔하다. 이 동기화에는 지연이 있을 수밖에 없어(near real-time), 방금 쓴 데이터가 검색 결과에 즉시 반영되어야 하는 강한 일관성이 필요한 곳에는 적합하지 않다.
Relational Database(Bufferpool, Materialized View)
- 정의: RDB 자신도 내부적으로 캐싱 계층을 여러 겹 갖고 있다. Buffer Pool은 DB 엔진이 디스크 페이지를 메모리에 캐싱해두는 저수준 캐시로 엔진이 자동으로 관리하고, Materialized View는 비용이 큰 쿼리의 결과 자체를 개발자가 명시적으로 미리 계산해 저장해두는 고수준 캐시다.
Buffer Pool
- 정의: 디스크에서 읽은 데이터 페이지(보통 4KB~16KB)를 메모리에 캐싱해두는 DB 엔진 내부의 캐시다. OS Page Cache와 비슷해 보이지만, DB 엔진은 트랜잭션과 락 상태를 알고 있어 아직 디스크에 반영되지 않은 변경 페이지(dirty page) 관리, LRU 기반 페이지 교체, 체크포인트(checkpoint) 시점 제어까지 더 정교하게 다룰 수 있다는 점이 다르다. MySQL InnoDB의
innodb_buffer_pool_size, PostgreSQL의shared_buffers가 이 설정에 해당한다. - 캐싱하는 데이터: 테이블/인덱스의 디스크 페이지. 자주 읽히는 hot 데이터일수록 buffer pool에 상주할 확률이 높다.
- 이런 곳에 적용하면 좋다:
- 워킹셋(자주 접근되는 데이터의 크기)이 buffer pool보다 작을 때: 대부분의 읽기가 디스크까지 가지 않고 메모리에서 처리된다. 그래서 “buffer pool 크기를 물리 메모리의 70~80%로 잡아라”는 튜닝 가이드가 흔한 것 — 워킹셋을 최대한 메모리에 다 태우기 위함이다.
- 반대로 워킹셋이 buffer pool보다 크면 페이지가 들어오자마자 밀려나는 스래싱(eviction thrashing)이 발생해 성능이 급격히 나빠진다. 이 경우 buffer pool 증설, 인덱스 최적화, 혹은 위 Distributed Cache 계층을 앞단에 두는 식으로 접근해야 한다.
- 이중 캐싱(double caching) 주의:
O_DIRECT를 쓰지 않으면 buffer pool과 OS page cache가 같은 데이터를 중복으로 들고 있게 되어 메모리를 낭비한다. InnoDB는O_DIRECT로 OS page cache를 우회하도록 설정해 buffer pool만 신뢰하는 구성을 권장한다.
Materialized View
정의: 조인/집계처럼 계산 비용이 큰 쿼리의 결과를 실제 테이블처럼 디스크에 물리적으로 저장해두는 것이다. 일반 View는 저장된 쿼리문일 뿐이라 조회할 때마다 매번 다시 계산하지만, Materialized View는 계산된 결과를 그대로 저장해뒀다가 조회 시 그냥 읽기만 한다는 점에서 명시적인 쿼리 결과 캐시다.
1 2 3 4 5 6 7 8
일반 View 조회 SELECT * FROM daily_sales_view; -> 내부적으로 원본 쿼리(JOIN + GROUP BY)를 매번 재실행 Materialized View 조회 SELECT * FROM daily_sales_mv; -> 미리 계산되어 저장된 결과를 그대로 읽음 (재계산 없음) -> REFRESH MATERIALIZED VIEW daily_sales_mv; 를 실행해야 최신화됨
- 캐싱하는 데이터: 복잡한
JOIN,GROUP BY, 집계 함수(SUM,COUNT,AVG)의 결과. - 이런 곳에 적용하면 좋다:
- 대시보드/리포트성 쿼리: 일별/월별 매출 집계, 사용자 활동 통계처럼 원본 테이블 전체를 스캔하고 조인해야 나오는 결과를, 미리 계산해두고 대시보드는 그 결과만 가볍게 조회하게 한다.
- 실시간성이 크리티컬하지 않은 데이터: 어제까지의 매출 통계는 몇 분~몇 시간 지연되어도 무방한 경우가 많다 — 이 허용 범위 안에서
REFRESH주기(수동, 스케줄, PostgreSQL의CONCURRENTLY옵션으로 조회를 막지 않는 갱신 등)를 정하면 된다. - 데이터 웨어하우스/BI 연동: Redshift, BigQuery 같은 분석용 DB에서도 Materialized View를 지원하며, ETL 파이프라인 중간 산출물을 저장해두는 용도로도 널리 쓰인다.
- 고려할 점: 원본 테이블이 바뀌어도
REFRESH를 실행하기 전까지는 예전 값을 계속 보여준다는 점에서, 위 CDN·Distributed Cache처럼 staleness를 어디까지 허용할 수 있는가가 설계의 핵심이다. 일부 DB는 원본 변경분만 반영하는 incremental refresh를 지원해 전체 재계산 비용을 줄인다.
This post is licensed under CC BY 4.0 by the author.