이벤트 오픈 직후 서비스가 느려집니다. "캐시 히트율이 20%밖에 안 돼서 DB가 부하를 다 받아요." 인스턴스를 늘렸더니 이번엔 "로그인이 자꾸 풀려요"(세션이 로컬에). "TPS가 한계라는데 어디가 병목인지 모르겠어요." PM·인프라인 당신은 캐시·성능·세션 용어를 알아야 이 문제들의 방향을 잡습니다. 이 사전은 캐시·성능·세션 용어를 빠르게 해독합니다. 깊은 내용은 Redis·성능 관련 모듈로 연결합니다.
- 1캐시 전략(cache-aside·write-through 등)과 히트/미스를 설명할 수 있다
- 2TTL·LRU·eviction으로 캐시 동작과 용량을 이해할 수 있다
- 3세션 외부화가 수평 확장에 필요한 이유를 설명할 수 있다
- 4부하/스트레스/스파이크 테스트와 TPS/QPS·병목으로 성능을 진단할 수 있다
캐시 — 전략과 동작
확대
위 그림처럼 Cache-Aside는 캐시 히트 시 DB를 거치지 않아 빠르고, 미스 시 DB 조회 후 캐시에 저장해 다음 요청을 히트로 전환합니다. 히트율이 낮으면(50% 미만) 캐시 키 설계·TTL·용량을 점검하고, 쓰기 시 캐시 무효화는 앱이 직접 관리합니다.
무엇을 어떻게 캐시하나
| 용어 | 한 줄 뜻 | 비고 | 중요도 |
|---|---|---|---|
| Redis / Memcached | 인메모리 캐시·저장소 | 대표 → 캐시(Cache) 전략, 세션 스토어, Pub/Sub 실무 패턴 | ★★ |
| Local / Distributed Cache | 프로세스 내 / 공유 캐시 | 분산=일관성↑ | ★★ |
| Cache Hit / Miss / Eviction | 적중 / 빗나감 / 쫓겨남 | 히트율 핵심 지표 | ★★★ |
| TTL / LRU | 수명 / 오래안쓴것부터 제거 | 용량 관리 | ★★ |
| Write-through / Write-back / Read-through / Cache-Aside | 캐시 쓰기·읽기 전략 | Cache-Aside 흔함 | ★★ |
| CDN Cache / Browser Cache / ETag / Last-Modified | 가장자리·브라우저 캐시 | 정적 가속 → 웹방화벽 403 분석과 CDN 캐시 관리 실무 | ★★ |
핵심: 캐시는 '자주 읽고 덜 바뀌는' 데이터에 효과적입니다. 히트율이 낮으면(많이 miss) 캐시 의미가 없으니 키 설계·TTL·용량을 점검합니다. 쓰기 시 캐시-DB 일관성(무효화)이 캐시의 가장 어려운 부분입니다(캐시(Cache) 전략, 세션 스토어, Pub/Sub 실무 패턴).
캐시가 응답을 빠르게 하는 법 — 조회·적중·무효화
요청 하나가 캐시를 지나는 5단계 — hit·miss·저장·무효화·축출
캐시가 "빠르다"는 건 알지만, 정작 요청 하나가 들어왔을 때 캐시 안에서 어떤 판단이 순서대로 일어나는지를 그려보지 않으면 히트율·stale·스탬피드 같은 문제가 왜 생기는지 감이 안 옵니다. 캐시의 핵심은 단 하나 — 원본(느린 DB·API)보다 가까운 곳에 답을 복사해 두고, 다음엔 그 사본으로 답하는 것입니다. 아래 흐름은 조회(hit/miss)부터 쓰기·무효화·축출까지, 캐시가 응답을 빠르게 하면서 동시에 어떤 위험을 떠안는지를 한눈에 보여줍니다.
[앱] getProduct(42) 요청
│
① 캐시 조회 GET product:42
│
├─ HIT (값 있고 안 만료) ─────────→ 즉시 반환 (수십 마이크로초, DB 안 감)
│
└─ MISS (없거나 TTL 만료)
│
② 원본 조회 SELECT * FROM product WHERE id=42 (수 ms~수십 ms)
│
③ 캐시에 저장 SET product:42 ... EX 300 (TTL 300초 부여)
│
▼ 반환 → 이후 같은 키 요청은 ①에서 HIT
[쓰기] updateProduct(42)
├─ write-through : DB와 캐시를 즉시 함께 갱신 (동기, 일관성↑)
├─ write-back : 캐시만 먼저, DB는 나중에 (빠르나 유실 위험)
└─ 무효화 : DEL product:42 → 다음 조회 때 새로 채움 (가장 흔함)
[용량 초과] maxmemory 도달 → LRU·LFU로 오래·덜 쓴 키부터 축출(eviction)
각 단계가 하는 일과, 여기서 어긋나면 나타나는 증상:
| 단계 | 하는 일 | 여기서 어긋나면 |
|---|---|---|
| ① 조회(hit/miss 판정) | 캐시 키로 값이 있고 안 만료됐는지 확인. 있으면 원본을 건너뜀 | 키를 너무 잘게 설계(사용자별·파라미터별)하면 대부분 miss → 히트율 바닥, 캐시 무의미 |
| ② miss 시 원본 조회 | 없을 때만 느린 DB·API로 감. 정상 캐시는 이 경로가 소수 | 인기 키가 동시에 만료되면 수많은 요청이 한꺼번에 miss로 DB에 몰림 = cache stampede(원본 휘청) |
| ③ 캐시에 저장 + TTL | 읽어온 값을 캐시에 넣고 만료 시간 부여. TTL이 일관성·히트율을 동시에 좌우 | TTL 너무 짧으면 금방 miss(효과↓), 너무 길면 원본과 어긋난 옛 값(stale)을 오래 반환 |
| ④ 쓰기·무효화 | 원본이 바뀔 때 캐시를 함께 갱신하거나 지워 stale 방지 | 무효화를 빠뜨리면 DB는 바뀌었는데 캐시가 옛 값을 계속 반환 — "DB엔 맞는데 화면은 옛날 값" |
| ⑤ 축출(eviction) | 용량이 차면 LRU(오래 안 쓴)·LFU(덜 쓴) 키부터 밀어냄 | 용량이 작으면 채우자마자 쫓겨나(잦은 eviction) 히트율이 안 오름 |
즉 캐시는 "빠른 저장소"가 아니라 "원본의 사본을 가까이 두는 규칙" 이고, 그 대가로 원본과 어긋날(stale) 위험·동시 miss 폭주(stampede) 위험을 떠안습니다. 그래서 문제를 좁힐 때는 먼저 어느 계층 캐시인지(브라우저 → CDN → 앱 인메모리 → 분산 캐시(Redis) → DB 버퍼)를 정하고, 그 계층의 히트율(효과 있나)·stale(정확한가)·stampede·지연(위험한가) 세 축으로 봅니다. 히트율이 높은데도 느린 순간이 있다면 원인은 캐시 바깥이 아니라 캐시 내부 동작에 있습니다(아래 심화 참고).
세션 · 연결 재사용
확대
위 그림처럼 실무 기본은 Cache-Aside(앱이 직접 캐시 관리)이며, Write-through는 강한 일관성이 필요할 때, Write-back은 쓰기 부하가 극심할 때 선택합니다. 쓰기가 많은 데이터는 캐시 효과보다 일관성 복잡도가 올라가므로 캐시 대상 선정이 중요합니다.
상태와 연결을 효율적으로
| 용어 | 한 줄 뜻 | 비고 | 중요도 |
|---|---|---|---|
| Session Store / Sticky Session | 세션 저장소 / 같은서버 고정 | 외부화 → 12-Factor App | ★★★ |
| Token Cache / CDN Cache / Browser Cache | 토큰·CDN·브라우저 캐시 | 계층별 캐시 | ★★ |
| Compression / Gzip / Brotli | 응답 압축 | 전송량↓ | ★★ |
| Lazy / Eager Loading | 지연 / 즉시 로딩 | N+1 관련 → Prisma, JPA, TypeORM, SQLAlchemy의 성능 차이와 올바른 사용법 | ★★ |
| Connection Reuse / Keep-Alive / Pooling | 연결 재사용 / 풀링 | 커넥션 풀 → DB 연결 지연을 없애는 HikariCP 설정과 대기 성능 튜닝 | ★★ |
핵심: 세션을 로컬에 두면 인스턴스 확장 시 풀립니다 → Redis 등으로 외부화하면 무상태·수평확장이 됩니다(12-Factor App·Tomcat 세션 클러스터링과 Redis 세션 외부화). 연결 풀링·Keep-Alive는 매 요청 연결 비용을 줄입니다(DB 연결 지연을 없애는 HikariCP 설정과 대기 성능 튜닝).
성능 지표 · 테스트
확대
위 그림처럼 성능 개선은 "가장 바쁜 곳"이 아니라 트레이스로 특정한 "가장 느린 구간(병목)"을 푸는 것입니다. TPS가 특정 값에서 천장을 치면 커넥션 풀·스레드 풀·CPU 한계를 의심하고, Spike Test로 이벤트 급증을 사전에 검증합니다.
얼마나 빠르고 얼마나 견디나
| 용어 | 한 줄 뜻 | 비고 | 중요도 |
|---|---|---|---|
| Bottleneck | 병목(가장 느린 구간) | 전체 속도를 지배 | ★★★ |
| Throughput / Latency / TPS / QPS | 처리량 / 지연 / 초당 트랜잭션·쿼리 | → 용어사전 | ★★ |
| Timeout | 시간 제한 | 슬로우 동반 | ★★ |
| Load / Stress / Spike Test | 부하 / 한계 / 급증 테스트 | 출시 전 검증(테스트 전략) | ★★ |
| Benchmark | 성능 기준 측정 | 비교 기준 | ★★ |
핵심: 성능 개선은 '가장 바쁜 곳'이 아니라 병목(가장 느린 구간) 을 푸는 것입니다(칸반 vs 스크럼 vs 워터폴의 흐름 원리와 동일). 이벤트·세일 전엔 Spike Test로 급증을 견디는지 미리 검증합니다.
성능 병목 진단 — 직접 확인
성능 저하는 캐시·세션·병목 세 축으로 좁힙니다.
1) 캐시 히트율: 낮으면(예: 20%) DB가 부하 → 캐시 키/TTL/용량 점검
2) 세션 위치: 로컬이면 scale-out 시 로그인 풀림 → 외부화([[twelve-factor-app]])
3) 병목: 트레이스로 가장 느린 구간 특정([[glossary-observability]])
- DB(슬로우쿼리/N+1)? 외부 동기호출? GC? 커넥션 풀 고갈?
캐시 히트율 22% → 캐시 키가 사용자별로 너무 잘게 나뉨, 재설계
세션: 톰캣 로컬 → 인스턴스 3개로 늘리자 로그인 풀림 → Redis 세션화
병목 트레이스: DB 조회 80% 점유, N+1 패턴 → eager loading([[orm-basics]])
TPS 한계 1200 → 커넥션 풀(10) 고갈이 천장([[connection-pooling]])
echo '히트율 + 세션위치 + 병목구간 점검'- 캐시 히트율이 낮으면(50% 미만) 캐시 효과 미미 → 키 설계(너무 잘게 나뉨)·TTL(너무 짧음)·용량(잦은 eviction) 점검. 무엇을 캐시할지 재검토
- 인스턴스 늘렸는데 로그인 풀리면 세션이 로컬에 있는 것 → Redis 등으로 외부화 전엔 scale-out이 무의미(12-Factor App)
- 병목은 "가장 바쁜 곳"이 아니라 "가장 느린 구간" — 트레이스로 특정(용어사전). DB·외부호출·GC·커넥션풀 중 무엇인지 가른다
- TPS가 특정 값에서 천장을 치면 자원 한계(커넥션 풀·스레드 풀·CPU) 의심 → 그 한도를 늘려 다시 측정(DB 연결 지연을 없애는 HikariCP 설정과 대기 성능 튜닝)
상황: 세일 이벤트 오픈 직후 트래픽이 평소의 20배가 되자 서비스가 느려지다 멈춥니다. DB CPU가 100%, 커넥션 풀은 고갈, 응답은 타임아웃됩니다.
원인: 캐시 부재 + 풀 한계입니다. 자주 읽는 상품/재고 데이터를 캐시하지 않아 모든 요청이 DB로 직행하고, 커넥션 풀이 작아 동시 요청을 못 받아냅니다(DB 연결 지연을 없애는 HikariCP 설정과 대기 성능 튜닝). 스파이크를 견딜 설계가 없었습니다.
진단:
□ 자주 읽는 데이터에 캐시가 있나? (히트율 측정)
□ 커넥션 풀·스레드 풀 크기가 부하 대비 적정한가?
□ Spike Test로 이 트래픽을 미리 검증했나?
해결: (1) 자주 읽고 덜 바뀌는 데이터(상품·재고)를 Cache-Aside로 캐시해 DB 부하를 덜기(캐시(Cache) 전략, 세션 스토어, Pub/Sub 실무 패턴). (2) 커넥션/스레드 풀을 부하 기준으로 재산정(DB 연결 지연을 없애는 HikariCP 설정과 대기 성능 튜닝). (3) 비핵심 작업은 비동기로 빼 큐가 스파이크를 완충(동기 vs 비동기). (4) 이벤트 전 Spike Test로 미리 검증(테스트 전략). 성능은 사고 후가 아니라 사전에 설계·측정하는 것입니다.
심화 — 히트율이 높은데도 느린 순간들
심화: Redis 내부 동작 — 단일 스레드·빅키·핫키가 만드는 지연
히트율과 병목으로 문제를 좁히는 법을 익혔다면, 다음은 히트율이 높은데도 느려지는 경우입니다. 이때 원인은 대개 캐시 바깥이 아니라 캐시 내부 동작에 있습니다.
- Redis는 명령을 한 줄로 처리합니다: Redis는 명령 실행이 단일 스레드라, 오래 걸리는 명령 하나가 뒤의 모든 명령을 줄 세웁니다. 수십만 키를 한 번에 훑는 KEYS, 거대한 컬렉션을 통째로 읽는 명령이 대표적입니다 — 그 몇백 ms 동안은 히트율 99%의 캐시도 전체 요청에 대해 멈춥니다. 운영 Redis에서 KEYS를 금지하고 조금씩 나눠 훑는 SCAN으로 대체하는 이유입니다.
- 빅키(big key): 값 하나가 수 MB면 읽을 때마다 직렬화·네트워크 전송 비용이 크게 붙고, 지울 때(DEL)도 동기 삭제라 그 순간 전체가 막힙니다. 큰 값은 나눠 저장하고, 큰 키 삭제는 백그라운드로 처리하는 UNLINK를 씁니다.
- 핫키(hot key): 특정 키 하나(오늘의 메인 상품 등)에 트래픽이 집중되면 클러스터로 노드를 늘려도 소용없습니다 — 한 키는 한 노드에만 살기 때문입니다. 이때는 노드 추가가 아니라, 앱 인스턴스 안의 수명 짧은 로컬 캐시로 그 키의 원격 조회 자체를 줄이거나 키를 여러 사본으로 복제해 분산합니다.
- 평균이 아니라 p99를 봅니다: 위 현상들은 대부분 순간 스파이크라 평균 지연에는 거의 드러나지 않습니다. 요청 100건 중 99건이 1ms, 1건이 500ms면 평균은 6ms지만, 화면 하나가 캐시를 수십 번 조회하는 구조에선 거의 모든 화면이 그 느린 1건을 한 번은 밟습니다(꼬리 지연 증폭). 캐시 성능은 평균이 아니라 p99(느린 쪽 1%)로 봐야 이런 문제가 보입니다.
히트율은 캐시가 일을 하는지를, p99는 캐시가 발목을 잡는 순간을 알려줍니다 — 둘을 함께 봐야 온전한 그림입니다.
상황: 대시보드의 히트율·메모리·DB 지표는 모두 정상인데, 매시 정각 무렵 전체 API 응답시간이 일제히 수백 ms 튀어 오릅니다. 특정 API가 아니라 캐시를 쓰는 모든 API가 같은 순간 함께 느려지는 것이 특징입니다.
원인: 정각마다 도는 정산 배치가 Redis에서 KEYS 명령으로 수십만 키를 한 번에 훑고 있었습니다. Redis는 명령 실행이 단일 스레드라, 이 한 명령이 도는 몇백 ms 동안 다른 모든 조회·저장 명령이 그 뒤에 줄을 섭니다. 히트율은 멀쩡한데 전체 API가 동시에 느려진 이유입니다 — 병목이 DB도 앱도 아닌 캐시 자신이었던 것입니다.
진단: Redis의 SLOWLOG(느린 명령 기록)를 조회해 정각 무렵 수백 ms짜리 KEYS 명령을 찾습니다. 느린 명령의 실행 시각이 지연 스파이크 시각과 겹치고 명령의 출처가 배치 서버로 확인되면 확정입니다.
해결: 배치의 KEYS를 SCAN으로 바꿔 조금씩 나눠 훑게 하고, 애초에 전체 키를 훑지 않도록 배치 대상 키 목록을 별도 자료구조로 관리합니다. 재발 방지로 운영 Redis에서 KEYS 같은 위험 명령을 설정으로 비활성화하고, 평균이 아니라 p99 지연에 알림을 걸어(용어사전) 이런 순간 스파이크가 지표에 드러나게 합니다. 캐시는 빠른 저장소가 아니라, 빠르게 쓸 때만 빠른 저장소입니다.
용어 식별 실습으로 굳히기: 용어 식별 — Cache / 성능 / 세션 — 증상을 보고 캐시·성능·세션 용어를 가려내고 헷갈리는 짝을 구분합니다.