Skip to content

필사 모드: 데이터베이스 캐싱 전략 완전 가이드: 결국 무효화가 전부다

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며

이 블로그에는 Redis 캐싱 전략 완벽 가이드가 이미 있습니다. Cache-Aside, Read-Through, Write-Through, Write-Behind 같은 패턴 카탈로그를 다루는 글입니다.

이 글은 캐시를 데이터베이스 쪽에서부터 올라가며 봅니다. 순서가 다릅니다. 외부 캐시를 붙이자는 결정이 나오기 전에, 이미 존재하는 캐시 계층이 제대로 쓰이고 있는지 먼저 확인해야 하기 때문입니다. PostgreSQL에는 이미 shared_buffers가 있고, 운영체제 페이지 캐시가 있고, 머티리얼라이즈드 뷰라는 계산 캐시가 있습니다. 이것들이 놀고 있는 상태에서 Redis를 얹으면, 문제는 해결되지 않고 정합성 문제만 새로 생깁니다.

그리고 이 글의 절반은 무효화에 씁니다. 캐시 도입의 어려운 부분은 캐시를 채우는 것이 아니라 정확한 순간에 비우는 것이기 때문입니다. 패턴 이름을 외워도 이 부분을 틀리면 사용자에게 옛 데이터가 보입니다.

기준 엔진은 PostgreSQL 18 이며, 파라미터 기본값과 잠금 동작은 PostgreSQL 18 문서에서 확인했습니다.

1. 캐시를 붙이기 전에 확인할 것

"조회가 느리니 캐시를 붙이자"는 제안이 나왔을 때 먼저 던져야 할 질문이 세 개 있습니다.

질문 1 — 느린 이유가 계산인가 I/O인가. 같은 결과를 반복해서 만드느라 느린 것이라면 캐시가 답입니다. 인덱스가 없어서 매번 전체 테이블을 훑느라 느린 것이라면 캐시는 문제를 미룰 뿐입니다. 캐시가 만료될 때마다 같은 느린 쿼리가 다시 돕니다.

질문 2 — 읽기 대 쓰기 비율이 어떤가. 읽기가 압도적이면 캐시 적중률이 높고 무효화 빈도가 낮습니다. 쓰기가 잦으면 캐시가 계속 무효화되어 이득이 적고, 정합성 위험만 커집니다.

질문 3 — 옛 데이터를 얼마나 오래 보여 줘도 되는가. 이 질문에 대한 답이 초 단위인지 분 단위인지에 따라 설계가 완전히 달라집니다. "절대 안 된다"는 답이 나오면 캐시가 아니라 다른 해법을 찾아야 합니다.

세 질문에 답하기 전에 캐시를 붙이면, 나중에 "왜 가끔 옛날 값이 보이죠?"라는 버그 리포트를 받고 근본 원인을 찾지 못하게 됩니다.

2. 이미 있는 캐시 — shared_buffers와 운영체제 캐시

PostgreSQL은 자체 버퍼 풀과 운영체제 페이지 캐시를 함께 씁니다. 이 이중 구조를 이해하면 첫 번째 캐시 튜닝을 할 수 있습니다.

shared_buffers기본값은 128MB 입니다. 문서의 조정 지침은 명확합니다. "전용 데이터베이스 서버에 1GB 이상의 RAM이 있다면 shared_buffers의 합리적인 시작값은 시스템 메모리의 25%다. 더 큰 설정이 효과적인 워크로드도 있지만, PostgreSQL이 운영체제 캐시에도 의존하므로 RAM의 40%를 넘게 할당하는 것이 더 적은 양보다 나을 가능성은 낮다."

여기서 자주 나오는 오해가 있습니다. effective_cache_size는 메모리를 할당하지 않습니다. 플래너의 가정만 바꿉니다. 운영체제 캐시를 포함해 얼마나 많은 데이터가 캐시되어 있을지에 대한 추정치이며, 기본값은 4GB입니다. 메모리가 256GB인 서버에서 이 값이 기본값 그대로면 플래너는 인덱스 스캔이 디스크를 많이 때릴 것이라 보고 순차 스캔으로 기웁니다.

적중률은 이렇게 측정합니다.

-- 테이블별 버퍼 적중률: 낮은 순서로
SELECT schemaname, relname,
       heap_blks_read, heap_blks_hit,
       CASE WHEN heap_blks_hit + heap_blks_read = 0 THEN NULL
            ELSE round(heap_blks_hit * 100.0 / (heap_blks_hit + heap_blks_read), 2)
       END AS heap_hit_pct,
       CASE WHEN idx_blks_hit + idx_blks_read = 0 THEN NULL
            ELSE round(idx_blks_hit * 100.0 / (idx_blks_hit + idx_blks_read), 2)
       END AS idx_hit_pct
FROM pg_statio_user_tables
ORDER BY heap_blks_read DESC
LIMIT 20;

주의할 점 하나. 여기서 말하는 "read"는 PostgreSQL 버퍼 풀에 없어서 운영체제에 요청한 블록이지 반드시 디스크를 때린 것은 아닙니다. 운영체제 페이지 캐시에 있었다면 실제 디스크 I/O는 없었습니다. 그래서 이 숫자만으로 디스크 부하를 판단하면 과대평가하게 됩니다.

PostgreSQL 16 이후에는 pg_stat_io 뷰가 있어 더 정확한 그림을 볼 수 있습니다. backend_type, object, context별로 reads, hits, evictions, fsyncs를 분리해 보여 줍니다. contextbulkreadvacuum인 항목이 많다면 그것은 캐시 부족이 아니라 정상적인 대량 작업입니다.

3. 머티리얼라이즈드 뷰 — 데이터베이스 안의 계산 캐시

집계 결과처럼 "만드는 데 오래 걸리지만 자주 바뀌지 않는" 데이터에는 외부 캐시보다 머티리얼라이즈드 뷰가 나은 경우가 많습니다. 이유는 세 가지입니다. SQL로 조인할 수 있고, 인덱스를 걸 수 있고, 무효화 로직을 애플리케이션이 아니라 갱신 스케줄로 표현할 수 있습니다.

CREATE MATERIALIZED VIEW mv_daily_sales AS
SELECT tenant_id,
       date_trunc('day', created_at) AS sales_day,
       count(*)         AS order_count,
       sum(total_amount) AS total_amount
FROM orders
WHERE status = 'PAID'
GROUP BY 1, 2;

-- CONCURRENTLY 갱신에는 유일 인덱스가 필수다
CREATE UNIQUE INDEX uq_mv_daily_sales
  ON mv_daily_sales (tenant_id, sales_day);

경고: CONCURRENTLY 없는 REFRESH MATERIALIZED VIEWACCESS EXCLUSIVE 잠금을 잡습니다. 문서 표현으로 "많은 행에 영향을 주는 갱신은 자원을 덜 쓰고 더 빨리 끝나지만, 머티리얼라이즈드 뷰를 읽으려는 다른 연결을 막을 수 있습니다." 즉 갱신하는 몇 분 동안 그 뷰를 조회하는 모든 요청이 멈춥니다. 서비스가 읽는 뷰라면 반드시 CONCURRENTLY를 쓰세요.

CONCURRENTLY의 조건은 문서에 명시되어 있습니다. "이 옵션은 컬럼 이름만 사용하고 모든 행을 포함하는 UNIQUE 인덱스가 최소 하나 있을 때만 허용된다. 즉 표현식 인덱스이거나 WHERE 절을 포함해서는 안 된다." 그리고 "이미 채워져 있는 머티리얼라이즈드 뷰에만 사용할 수 있다"와 "이 옵션을 써도 하나의 머티리얼라이즈드 뷰에 대해 한 번에 하나의 REFRESH만 실행될 수 있다"도 함께 기억해야 합니다.

-- 읽기를 막지 않는 갱신
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales;

문서는 성능 상충도 짚습니다. CONCURRENTLY는 "영향받는 행이 적을 때 더 빠를 수 있다"고 합니다. 즉 전체를 다시 계산하는 상황이라면 오히려 느립니다. 매일 전량 재계산하는 야간 배치라면 서비스가 조용한 시간에 CONCURRENTLY 없이 돌리는 편이 나을 수 있습니다. 판단 기준은 그 시간대에 그 뷰를 읽는 트래픽이 있는가입니다.

머티리얼라이즈드 뷰가 맞지 않는 경우도 분명합니다. 갱신 주기가 초 단위여야 하거나, 사용자별로 결과가 전부 다르거나, 결과가 SQL로 표현되지 않는 계산이라면 외부 캐시나 다른 구조가 필요합니다.

4. 외부 캐시를 도입하는 판단 기준

2절과 3절을 다 해 보고도 부족할 때 외부 캐시가 등장합니다. 도입 판단에 쓸 수 있는 기준입니다.

도입이 타당한 신호

  • 같은 키에 대한 조회가 압도적으로 반복된다(적중률이 높을 것이 예상된다)
  • 원본 계산이 비싸고 결과가 작다
  • 초 단위의 지연된 정합성이 도메인상 허용된다
  • 데이터베이스가 CPU나 I/O 한계에 실제로 닿아 있다

도입을 미뤄야 하는 신호

  • 조회 키가 매번 달라 적중률이 낮을 것으로 보인다
  • 쓰기가 잦아 무효화가 계속 발생한다
  • 옛 데이터 노출이 금전이나 안전과 연결된다
  • 아직 인덱스 튜닝이나 쿼리 재작성을 해 보지 않았다

특히 마지막 항목을 강조합니다. 캐시는 성능 문제를 해결하지 않고 이동시킵니다. 캐시가 비어 있는 순간(배포 직후, 캐시 노드 재시작, 대규모 무효화 직후)에는 모든 부하가 그대로 데이터베이스로 갑니다. 원본 쿼리가 감당 가능한 수준이 아니면 그 순간 서비스가 무너집니다. 캐시는 정상 상태의 최적화이지 장애 대비책이 아닙니다.

5. 무효화의 네 가지 실패 모드

여기가 이 글의 핵심입니다. 캐시 정합성이 깨지는 방식은 유형화할 수 있습니다.

실패 모드 1 — 갱신 후 삭제 사이의 창. 데이터베이스를 갱신하고 캐시를 삭제하는 사이에 다른 요청이 캐시를 읽으면 옛 값을 봅니다. 창은 짧지만 0은 아닙니다. 트래픽이 많으면 반드시 발생합니다.

실패 모드 2 — 읽기와 쓰기의 경쟁. 더 고약한 경우입니다. 요청 A가 캐시 미스로 데이터베이스에서 값 V1을 읽습니다. 그 직후 요청 B가 값을 V2로 갱신하고 캐시를 삭제합니다. 그다음 A가 뒤늦게 V1을 캐시에 씁니다. 결과적으로 캐시에는 옛 값 V1이 만료될 때까지 남습니다. 삭제가 쓰기보다 먼저 일어났기 때문에 삭제로도 지워지지 않습니다.

실패 모드 3 — 트랜잭션 롤백. 캐시를 먼저 지우고 데이터베이스를 갱신했는데 트랜잭션이 롤백되면, 캐시는 비었고 데이터는 옛 상태입니다. 다음 조회가 옛 값을 다시 캐시에 채우므로 결과적으로는 정합하지만, 그사이에 새 값을 본 사용자가 있으면 혼란이 생깁니다.

실패 모드 4 — 부분 실패. 데이터베이스 갱신은 성공했는데 캐시 삭제가 네트워크 오류로 실패합니다. 재시도하지 않으면 만료 시각까지 옛 값이 남습니다.

이 네 가지에 대한 실무적 대응은 다음과 같습니다.

  • 삭제하되 채우지 않는다. 쓰기 경로에서 캐시에 새 값을 넣지 말고 삭제만 합니다. 새 값을 넣는 순간 실패 모드 2의 경쟁에 참여하게 됩니다.
  • 삭제를 커밋 이후로 미룬다. 트랜잭션 안에서 캐시를 지우면 롤백 시 불일치가 생깁니다. 커밋 성공을 확인한 뒤 지웁니다. 아웃박스 테이블에 무효화 이벤트를 트랜잭션과 함께 기록하고 별도 워커가 처리하면 실패 모드 3과 4를 함께 막을 수 있습니다.
  • 만료 시간을 반드시 건다. 무효화가 실패해도 만료가 최종 방어선입니다. 만료 없는 캐시 엔트리는 언젠가 반드시 사고를 냅니다.
  • 지연 이중 삭제를 고려한다. 갱신 직후 한 번 지우고, 짧은 지연 후 한 번 더 지우는 방식입니다. 실패 모드 2의 늦게 도착한 쓰기를 걷어냅니다. 완벽하지는 않지만 비용 대비 효과가 좋습니다.

6. 쓰기 경로의 순서 문제

5절을 순서 문제로 다시 정리하면 선택지는 네 가지이고, 각각의 위험이 다릅니다.

순서위험
캐시 갱신 → DB 갱신DB 갱신 실패 시 캐시에 존재하지 않는 값이 남는다. 쓰면 안 된다
DB 갱신 → 캐시 갱신두 쓰기의 순서가 뒤바뀌면 옛 값이 캐시에 남는다
캐시 삭제 → DB 갱신삭제와 갱신 사이의 조회가 옛 값을 다시 채운다
DB 갱신(커밋) → 캐시 삭제가장 안전하다. 남는 위험은 삭제 실패와 짧은 경쟁 창

네 번째가 표준입니다. 나머지는 특별한 이유가 없으면 쓰지 않습니다.

그리고 반드시 지켜야 할 것 하나. 캐시 조작을 데이터베이스 트랜잭션 안에 넣지 마세요. 두 가지 이유가 있습니다. 첫째, 트랜잭션이 롤백되어도 캐시 조작은 되돌아가지 않습니다. 둘째, 캐시 서버의 응답을 기다리는 동안 데이터베이스 트랜잭션이 열려 있게 되고, 그 트랜잭션은 VACUUM이 죽은 행을 회수하지 못하게 막습니다. 네트워크 지연이 몇 초만 되어도 데이터베이스 전체에 영향을 줍니다.

-- 무효화 이벤트를 트랜잭션과 원자적으로 기록하는 아웃박스
CREATE TABLE cache_invalidation_outbox (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  cache_key   text        NOT NULL,
  created_at  timestamptz NOT NULL DEFAULT now(),
  processed_at timestamptz
);

CREATE INDEX idx_cache_outbox_pending
  ON cache_invalidation_outbox (created_at)
  WHERE processed_at IS NULL;

워커는 4절의 큐 패턴, 즉 FOR UPDATE SKIP LOCKED로 이 테이블을 소비하면 됩니다. 이렇게 하면 "데이터베이스 커밋이 성공한 경우에만, 그리고 반드시 한 번은" 무효화가 일어납니다.

7. 캐시 스탬피드와 방어

인기 있는 키의 캐시가 만료되는 순간, 그 키를 조회하던 모든 요청이 동시에 캐시 미스를 겪고 전부 데이터베이스로 갑니다. 초당 수천 건의 동일 쿼리가 한꺼번에 도착합니다. 이것이 캐시 스탬피드입니다.

방어 수단은 세 가지입니다.

첫째, 만료 시각에 무작위 오프셋을 준다. 같은 시각에 만들어진 엔트리들이 같은 시각에 만료되지 않도록 흩뿌립니다. 가장 싸고 가장 효과적입니다.

둘째, 재계산을 한 요청만 하도록 잠근다. 미스가 난 요청 중 하나만 원본을 계산하고 나머지는 기다리거나 옛 값을 씁니다. 애플리케이션 프로세스가 여럿이면 분산 잠금이 필요한데, PostgreSQL의 트랜잭션 수준 어드바이저리 락이 이 용도로 쓸 만합니다.

-- 재계산 권한을 한 요청만 갖게 한다
BEGIN;
SELECT pg_try_advisory_xact_lock(hashtext('recompute:daily_sales:42'));
-- true를 받은 요청만 무거운 집계를 수행하고 캐시에 채운다
COMMIT;

세션 수준이 아니라 트랜잭션 수준 함수를 쓰는 것이 중요합니다. 커넥션 풀 뒤에서 세션 수준 락은 해제되지 않고 남을 수 있습니다.

셋째, 만료 전에 미리 갱신한다. 남은 수명이 일정 비율 아래로 떨어지면 백그라운드에서 미리 다시 계산합니다. 사용자 요청은 항상 캐시에서 응답받습니다. 트래픽이 예측 가능한 대시보드류에 잘 맞습니다.

여기에 하나 더. 캐시 계층이 죽었을 때의 동작을 미리 정하세요. 캐시 서버가 응답하지 않으면 원본으로 폴백할 것인지, 폴백한다면 데이터베이스가 그 부하를 견딜 수 있는지, 못 견딘다면 일부 요청을 빠르게 거절할 것인지. 이 결정을 장애 중에 내리면 늦습니다.

8. 무엇을 측정할 것인가

캐시는 지표 없이 운영하면 이득도 손해도 보이지 않습니다. 최소한 다음 네 가지는 있어야 합니다.

적중률. 낮으면 캐시가 일을 못 하는 것이고, 지나치게 높으면(예: 99.99%) 애초에 부하가 없었을 가능성도 있습니다. 키 그룹별로 나눠 보는 것이 중요합니다. 전체 평균은 대개 아무것도 말해 주지 않습니다.

원본 쿼리의 실행 빈도. 캐시 도입 전후로 pg_stat_statementscalls가 얼마나 줄었는지 봅니다.

SELECT calls, round(total_exec_time::numeric, 1) AS total_ms,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       rows, left(query, 70) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

pg_stat_statementsshared_preload_libraries에 등록하고 확장을 설치해야 동작합니다. pg_stat_statements.max의 기본값은 5000, track의 기본값은 top입니다.

무효화 지연. 데이터가 바뀐 시각과 캐시가 실제로 비워진 시각의 차이입니다. 아웃박스 테이블을 쓴다면 created_atprocessed_at의 차이로 바로 측정됩니다.

캐시 비었을 때의 원본 부하. 정기적으로(예: 배포 때마다) 캐시를 비운 직후의 데이터베이스 부하를 기록해 두세요. 이 값이 데이터베이스 용량에 근접해 있으면, 서비스는 캐시가 없으면 못 사는 상태입니다. 그 자체가 리스크로 관리되어야 합니다.

퀴즈: 실력을 확인해 보세요

퀴즈 1: 아래 코드의 문제는 무엇인가요?
BEGIN;
UPDATE products SET price = 19900 WHERE id = 42;
-- 애플리케이션이 여기서 Redis DEL products:42 호출
COMMIT;

정답: 캐시 삭제가 트랜잭션 안에 있습니다. 두 가지가 깨집니다.

설명: 첫째, COMMIT이 실패해 롤백되면 데이터는 옛 값인데 캐시는 비어 있습니다. 다음 조회가 옛 값을 다시 채우므로 최종적으로는 정합하지만, 그사이에 잘못된 상태가 존재합니다. 둘째, 더 심각하게, 캐시 서버 응답을 기다리는 동안 데이터베이스 트랜잭션이 열려 있습니다. 열린 트랜잭션은 데이터베이스 전체에서 죽은 행 회수를 막습니다. 캐시 서버가 느려지면 그 지연이 그대로 VACUUM 지연이 됩니다. 올바른 순서는 커밋을 먼저 완료하고 그다음 캐시를 지우는 것이며, 삭제 실패에 대비해 아웃박스 테이블에 무효화 이벤트를 트랜잭션과 함께 기록하는 방식이 가장 안전합니다.

퀴즈 2: 갱신할 때마다 캐시에 새 값을 넣는데도 가끔 옛 값이 보입니다. 왜일까요?

정답: 읽기와 쓰기의 경쟁 때문입니다. 캐시에 값을 채우는 대신 삭제만 해야 합니다.

설명: 요청 A가 캐시 미스로 데이터베이스에서 V1을 읽고, 그 직후 요청 B가 V2로 갱신하며 캐시에 V2를 씁니다. 그런데 A가 뒤늦게 자기가 읽은 V1을 캐시에 쓰면 최종 상태는 V1입니다. 만료될 때까지 옛 값이 남습니다. 쓰기 경로에서 캐시를 채우지 않고 삭제만 하면 다음 조회가 최신 값을 읽어 채우므로 이 경쟁의 창이 크게 줄어듭니다. 완전히 없어지지는 않으므로, 짧은 지연 후 한 번 더 지우는 지연 이중 삭제와 만료 시간을 함께 씁니다.

퀴즈 3: 대시보드용 집계 뷰를 REFRESH MATERIALIZED VIEW로 갱신하는데 갱신 중 대시보드가 멈춥니다.

정답: CONCURRENTLY가 빠졌습니다.

설명: CONCURRENTLY 없는 REFRESH MATERIALIZED VIEWACCESS EXCLUSIVE 잠금을 잡습니다. 이 잠금은 평범한 SELECT가 잡는 ACCESS SHARE와도 충돌하므로 조회가 전부 막힙니다. CONCURRENTLY를 쓰려면 문서가 명시한 조건을 만족해야 합니다. 컬럼 이름만 사용하고 모든 행을 포함하는 UNIQUE 인덱스가 최소 하나 있어야 하며, 표현식 인덱스이거나 WHERE 절을 포함해서는 안 됩니다. 또 이미 채워져 있는 뷰에만 쓸 수 있습니다.

CREATE UNIQUE INDEX uq_mv_daily_sales ON mv_daily_sales (tenant_id, sales_day);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales;

다만 문서는 CONCURRENTLY가 "영향받는 행이 적을 때 더 빠를 수 있다"고 하므로, 전량 재계산이고 그 시간대에 조회 트래픽이 없다면 일반 갱신이 더 나은 선택입니다.

퀴즈 4: 캐시 적중률이 98%인데 데이터베이스 부하가 줄지 않았습니다. 어디를 봐야 할까요?

정답: 적중률을 키 그룹별로 나눠 보고, pg_stat_statements에서 실제로 어떤 쿼리가 부하를 만드는지 확인해야 합니다.

설명: 전체 평균 적중률은 오해를 부르기 쉽습니다. 가벼운 키가 대량으로 적중하면서 평균을 끌어올리고, 정작 무거운 쿼리는 캐시를 거치지 않고 있을 수 있습니다. 확인 순서는 이렇습니다. 먼저 pg_stat_statementstotal_exec_time 내림차순으로 보고 상위 쿼리가 무엇인지 봅니다. 그 쿼리가 캐시 대상이 아니었다면 캐시 대상 선정이 잘못된 것입니다. 캐시 대상인데도 호출이 많다면 무효화가 너무 잦은 것이고, 갱신 빈도와 캐시 키 설계를 다시 봐야 합니다. 키를 너무 잘게 나눠 하나의 갱신이 수백 개 키를 무효화하고 있는 경우가 흔합니다.

퀴즈 5: 배포할 때마다 배포 직후 몇 분간 데이터베이스 CPU가 100%를 칩니다.

정답: 캐시가 비워진 상태에서 모든 부하가 원본으로 몰리는 콜드 스타트 문제입니다.

설명: 캐시는 정상 상태의 최적화이지 용량 대책이 아닙니다. 캐시가 빈 순간의 부하를 견딜 수 없다면, 서비스는 캐시 없이는 못 사는 상태이고 이는 리스크로 관리되어야 합니다. 대응은 세 가지입니다. 첫째, 배포가 캐시를 비우지 않게 합니다. 캐시 키에 배포 버전을 넣는 관행이 흔한 원인입니다. 스키마가 실제로 바뀐 키에만 버전을 붙이세요. 둘째, 워밍업 단계를 배포 절차에 넣습니다. 트래픽을 받기 전에 상위 키들을 미리 채웁니다. 셋째, 스탬피드 방어를 겁니다. 만료 시각 무작위화와 트랜잭션 수준 어드바이저리 락으로 동일 키의 재계산을 하나로 제한하면 순간 부하가 크게 줄어듭니다.

마치며

캐시 설계의 난이도는 캐시를 채우는 쪽이 아니라 비우는 쪽에 있습니다. 그리고 비우는 문제는 분산 시스템의 순서 문제입니다. 두 개의 저장소에 대한 두 개의 쓰기가 있고, 그 순서를 보장할 방법이 없다는 것이 문제의 본질입니다. 패턴 이름을 외우는 것으로는 이 문제가 풀리지 않습니다.

실무적인 결론은 세 줄입니다. 캐시를 붙이기 전에 데이터베이스 안의 캐시 계층을 먼저 확인한다. 쓰기 경로에서는 커밋 후에 삭제만 하고 채우지 않는다. 무효화가 실패할 것을 전제로 만료 시간과 아웃박스를 함께 둔다.

집계 쿼리와 머티리얼라이즈드 뷰를 직접 실험해 보려면 Postgres 놀이터를, 분석형 워크로드의 집계 성능을 비교해 보려면 DuckDB 놀이터를 활용하세요.

참고 자료

이어서 읽기

현재 단락 (1/142)

이 블로그에는 [Redis 캐싱 전략 완벽 가이드](/blog/database/2026-03-03-redis-caching-strategies)가 이미 있습니다. Cache-Asi...

작성 글자: 0원문 글자: 9,867작성 단락: 0/142