클라이언트 사이드 캐싱
고친 사람 github-actions[bot]
클라이언트 사이드 캐싱은 데이터를 받아 가는 쪽이 받은 값을 자기 곁에 남겨 두고 다시 씁니다. 브라우저가 웹 응답을 보관하는 일도, 애플리케이션 서버가 데이터베이스에서 읽은 값을 제 메모리에 두는 일도 이렇게 부릅니다. 어려운 대목은 원본 값이 바뀌었을 때 여기저기 남은 값을 버리게 하는 일입니다.
쉽고 빠른 이해
요청을 보내는 쪽이 받은 값을 자기 메모리에 적어 둡니다. 주문 서버가 상품 가격을 한 번 읽어 오면 그다음부터는 저장소에 다시 묻지 않고 적어 둔 가격을 씁니다.
네트워크를 오가는 일이 가장 느리기 때문입니다. 자기 메모리에서 꺼내면 왕복이 아예 없습니다. 저장소도 같은 질문을 덜 받습니다.
- 값이 필요하면 먼저 자기 메모리를 봅니다
- 없으면 저장소에 묻습니다. 받은 값은 메모리에 남깁니다
- 시간이 다 되거나 저장소가 값이 바뀌었다고 알려 오면 그 값을 버립니다
대가는 낡은 값입니다. 저장소의 값이 바뀌어도 사본은 모릅니다. 주문 서버 여러 대가 저마다 사본을 들고 있으면 같은 상품의 가격이 주문 서버마다 다르게 보이는 순간이 생깁니다.
그래서 설정이나 상품 정보처럼 거의 안 바뀌고 자주 읽는 값에 씁니다. 잔고나 재고처럼 낡으면 바로 틀리는 값에는 쓰지 않습니다.
상세
이 절은 상품 가격을 읽는 예 하나로 사본이 놓이는 곳, 사본이 줄여 주는 비용과 낡는 문제, 사본을 버리게 하는 두 방법을 차례로 봅니다.
자주 보는 사내 규정집이 있다고 해 봅시다. 볼 때마다 자료실에 내려가는 대신 한 부 복사해 책상 서랍에 넣어 둡니다. 다음에 볼 일이 생기면 서랍만 엽니다.
캐싱은 원본보다 가까운 곳에 사본을 두고 다시 쓰는 일입니다. 그 사본을 캐시라고 부릅니다. 클라이언트는 요청을 보내는 쪽이고, 서버는 그 요청에 답하는 쪽입니다.
클라이언트 사이드 캐싱은 그 사본을 요청을 보내는 쪽에 둡니다. 중간 장비나 원본 쪽이 아니라 묻는 쪽 자신이 사본을 들고 있다는 점이 이름의 뜻입니다. 앞의 서랍 속 복사본이 바로 이 사본입니다.
백엔드에서 클라이언트가 되는 쪽
백엔드 개발자에게 클라이언트는 브라우저만이 아닙니다. 애플리케이션 서버도 데이터베이스나 키-값 저장소에 요청을 보낼 때는 클라이언트입니다. 키-값 저장소는 키 하나에 값 하나를 담아 두는 저장소입니다. 그래서 이 말은 두 곳에서 씁니다.
| 쓰는 곳 | 클라이언트 | 사본을 두는 곳 | 사본이 담는 것 |
|---|---|---|---|
| 웹·모바일 | 브라우저 · 앱 | 기기의 디스크나 메모리 | 서버가 준 응답 |
| 서버 사이 | 애플리케이션 서버 | 그 서버 프로세스의 메모리 | 저장소에서 읽은 값 |
이 문서는 둘째 뜻을 중심으로 씁니다. 값을 묻는 쪽은 「애플리케이션 서버」, 원본을 가진 쪽은 「저장소」라고 부릅니다. 웹 쪽 이야기는 마지막 소절에서 짧게 봅니다.
사본을 두는 두 위치
이 문서가 견주는 사본의 위치는 둘입니다. 묻는 쪽 안과, 묻는 쪽과 저장소 사이입니다. 원본은 맨 아래 저장소에 있습니다. 아래 그림에서 클라이언트 사이드 캐시는 맨 위 상자 안에 있습니다.
flowchart TD
subgraph A["애플리케이션 서버 · 클라이언트"]
A1["애플리케이션 코드"] --> A2["클라이언트 사이드 캐시"]
end
A2 -->|"없을 때만 네트워크로"| B["공유 캐시 서버"]
B -->|"없을 때만"| C["저장소 · 원본"]
가운데 상자는 공유 캐시입니다. 애플리케이션 서버 여러 대가 네트워크 너머의 캐시 서버 하나를 같이 씁니다. 사본이 한 벌뿐이라 지울 때도 한 번만 지우면 됩니다.
클라이언트 사이드 캐시는 애플리케이션 서버마다 따로 있습니다. 애플리케이션 서버가 열 대면 사본도 많게는 열 벌입니다. 이 차이가 아래 「사본이 낡는 문제」의 뿌리입니다.
사본이 줄여 주는 비용
사본을 자기 메모리에 두면 요청이 네트워크를 타지 않습니다. 같은 데이터센터 안이라도 네트워크를 한 번 오가는 시간은 메모리를 읽는 시간보다 몇 자릿수 깁니다. 이렇게 요청을 보내고 답을 받기까지 걸리는 시간을 왕복 시간이라고 합니다.
받은 바이트를 객체로 되돌리는 일도 줄어듭니다. 네트워크로 온 값은 바이트라서 쓰기 전에 객체로 바꿔야 합니다. 이 변환을 역직렬화라고 합니다. 사본을 객체로 들고 있으면 이 일을 다시 하지 않습니다.
저장소도 덜 바빠집니다. 상품 하나가 갑자기 많이 팔리면 모든 애플리케이션 서버가 같은 키를 쉴 새 없이 묻습니다. 사본이 있으면 그 질문이 애플리케이션 서버마다 한 번으로 줄어듭니다.
가장 단순한 모양
아래는 상품 가격을 읽는 함수입니다. local 은 이 프로세스 안에 둔 사본이고, store 는 네트워크
너머의 저장소입니다.
Map<String, Integer> local = new HashMap<>();
int price(String key) {
Integer v = local.get(key);
if (v == null) {
v = store.get(key); // 네트워크 왕복
local.put(key, v);
}
return v;
}
먼저 사본을 봅니다. 없을 때만 저장소에 갑니다. 받아 온 값은 다음을 위해 사본에 남깁니다. 같은 키로 두 번 부르면 아래처럼 됩니다.
price("item:42"); // 1000, 저장소에서
price("item:42"); // 1000, 메모리에서
두 번째 호출은 네트워크를 타지 않습니다. 사본에 값이 있어서 바로 돌려줍니다. 이렇게 사본에서 찾는 것을 캐시 히트라고 부릅니다. 못 찾아 원본에 가는 것은 캐시 미스입니다.
사본이 낡는 문제
규정집이 개정되어도 서랍 속 복사본은 저절로 바뀌지 않습니다. 위 함수도 같습니다. 사본을 버리는 코드가 없어서 다른 애플리케이션 서버가 가격을 1200 으로 바꿔도 이 애플리케이션 서버는 계속 1000 을 돌려줍니다. 원본과 달라진 사본을 낡은 데이터라고 부릅니다.
sequenceDiagram
participant 앱1 as 애플리케이션 서버 1
participant 저장소
participant 앱2 as 애플리케이션 서버 2
앱1->>저장소: 가격을 읽는다
저장소-->>앱1: 1000
Note over 앱1: 사본에 1000 을 남긴다
앱2->>저장소: 가격을 1200 으로 바꾼다
Note over 앱1: 사본은 여전히 1000
공유 캐시였다면 바꾸는 쪽이 캐시 서버의 사본 하나를 지우면 끝납니다. 클라이언트 사이드 캐시는 사본이 여러 애플리케이션 서버에 흩어져 있습니다. 바꾸는 쪽은 누가 사본을 들고 있는지 모릅니다.
흩어진 사본을 버리게 하는 일을 무효화라고 합니다. 이 방식의 핵심 과제가 바로 이것입니다. 방법은 크게 둘입니다.
첫째 방법 · 시간이 지나면 버린다
사본마다 유효 시간을 붙입니다. 이 시간을 TTL(Time To Live, 유효 시간)이라고 합니다. TTL 이 다 되면 그 사본은 만료됐다고 말합니다.
만료된 사본은 버리고 다음 요청 때 저장소에서 다시 읽습니다. 이 방법은 애플리케이션 서버 혼자 할 수 있습니다. 저장소는 사본이 있는 줄도 몰라도 됩니다.
대가는 TTL 동안 낡은 값이 보일 수 있다는 점입니다. TTL 을 30초로 잡으면 가격이 바뀐 뒤 최대 30초 동안 옛 가격이 나갑니다. TTL 을 짧게 잡으면 낡는 시간은 줄지만 저장소에 가는 횟수가 늘어납니다.
둘째 방법 · 저장소가 알려 준다
저장소가 값이 바뀔 때마다 사본을 가진 애플리케이션 서버에게 알립니다. 애플리케이션 서버는 알림을 받자마자 그 사본을 지웁니다. 이 알림을 무효화 메시지라고 부릅니다.
sequenceDiagram
participant 앱1 as 애플리케이션 서버 1
participant 저장소
participant 앱2 as 애플리케이션 서버 2
앱1->>저장소: 가격을 읽는다
Note over 저장소: 애플리케이션 서버 1 이 이 키를 읽었다고 적는다
앱2->>저장소: 가격을 1200 으로 바꾼다
저장소->>앱1: 이 키가 바뀌었다
Note over 앱1: 사본을 지운다
저장소가 누가 어떤 키를 읽었는지 기억하려면 그만큼 메모리를 씁니다. 읽는 애플리케이션 서버와 키가 많을수록 적어 둘 것도 늘어납니다.
다른 갈래는 구독입니다. 구독은 관심 있는 키 범위를 저장소에 미리 등록해 두는 일입니다.
「item: 으로 시작하는 키」를 등록해 두면 그 범위의 키가 바뀔 때마다 알림이 옵니다.
구독을 쓰면 저장소는 누가 무엇을 읽었는지 적지 않아도 됩니다. 대신 그 키의 사본이 없는 애플리케이션 서버도 알림을 받습니다.
알림은 연결을 타고 옵니다. 그 연결이 끊긴 동안에는 알림을 놓칩니다. 연결이 끊겼다 다시 붙으면 애플리케이션 서버는 사본을 전부 버리고 새로 시작해야 안전합니다.
두 방법은 같이 쓰는 일이 많습니다. 알림이 오면 바로 지웁니다. 놓친 알림에 대비해 TTL 도 붙여 둡니다.
| 시간이 지나면 버린다 | 저장소가 알려 준다 | |
|---|---|---|
| 누가 버리기로 정하나 | 애플리케이션 서버 혼자 | 저장소의 알림 |
| 낡은 값이 남는 시간 | 최대 TTL 만큼 | 알림이 닿기까지 |
| 저장소가 할 일 | 없음 | 누가 읽었는지 기억하거나 범위를 알림 |
| 필요한 것 | 시계 | 알림을 받을 연결 |
메모리 한도와 축출
클라이언트 사이드 캐시는 애플리케이션 서버의 메모리를 씁니다. 키가 늘수록 사본도 끝없이 늘어납니다.
위 예의 HashMap 처럼 한도 없는 맵을 쓰면 메모리가 바닥날 때까지 자랍니다. 그래서 한도를 정해 둡니다.
한도가 차면 사본 일부를 버리고 새 값에 공간을 내줍니다. 이 일을 축출이라고 합니다. 흔한 규칙은 LRU(Least Recently Used, 가장 오래 안 쓰인 것)로, 가장 오래 안 꺼낸 사본부터 버립니다.
사본은 메모리에서 객체로 삽니다. 프로그램이 만든 객체가 사는 메모리를 힙이라고 합니다.
가비지 컬렉션은 더 안 쓰는 객체를 런타임이 찾아 치우는 일입니다. 치우려면 힙의 객체를 살펴봐야 합니다. 사본이 많을수록 살펴볼 객체도 늘어납니다. 그만큼 치우는 시간이 길어질 수 있습니다.
쓰는 경우와 피하는 경우
판단 기준은 「이 값이 잠깐 낡아도 되나」와 「같은 값을 얼마나 자주 읽나」 둘입니다.
| 예 | 까닭 | |
|---|---|---|
| 맞는다 | 설정값 · 상품 정보 · 권한 목록 · 코드 표 | 거의 안 바뀌고 아주 자주 읽습니다 |
| 맞는다 | 많은 애플리케이션 서버가 한꺼번에 묻는 인기 키 | 저장소에 몰리는 질문을 애플리케이션 서버마다 하나로 줄입니다 |
| 피한다 | 계좌 잔고 · 남은 재고 | 낡은 값 하나가 바로 잘못된 결정이 됩니다 |
| 피한다 | 사용자마다 다른 값이 수없이 많을 때 | 다시 읽힐 일이 드물어 메모리만 차지합니다 |
애플리케이션 서버 여러 대가 같은 값을 다르게 보여 주면 곤란한 화면도 피합니다. 사용자가 새로고침할 때마다 다른 애플리케이션 서버로 가면 값이 왔다 갔다 하는 것처럼 보입니다.
웹 브라우저에서
브라우저도 같은 일을 합니다. 웹 서버는 응답에 「이 응답은 얼마 동안 다시 써도 된다」를 붙여 보냅니다. 브라우저는 그동안 사본을 씁니다. 이 지시를 담는 응답 헤더가 Cache-Control 입니다.
시간이 다 되면 브라우저는 응답 전체를 다시 받지 않고 바뀌었는지만 묻기도 합니다. 이렇게 묻는 요청이 조건부 요청입니다. 바뀌지 않았으면 웹 서버는 본문 없이 그대로라고만 답합니다.
물을 때는 버전 표시를 들고 갑니다. 그 표시가 ETag(entity tag, 엔티티 태그)입니다. 웹 서버가 앞선 응답에 붙여 준 것을 브라우저가 그대로 돌려보냅니다.
브라우저에는 웹 서버가 보낸 알림을 받아 사본을 지우는 통로가 기본으로는 없습니다. 그래서 웹 쪽은 대개 첫째 방법(시간이 지나면 버린다)에 기댑니다. 다만 시간이 다 된 사본을 바로 버리는 대신 조건부 요청으로 바뀌었는지만 묻는 변형을 씁니다. 자세한 동작은 브라우저 캐시가 다룹니다.
관련 항목
클라이언트 사이드 캐싱이 속하는 상위 개념
캐싱 · 캐시 · 클라이언트 · 서버 · 클라이언트-서버 모델
사본을 버리게 하는 수단
무효화 · 만료 · TTL · 무효화 메시지 · tracking · 발행-구독
사본이 낡아 생기는 문제
낡은 데이터 · 캐시 일관성 · 결과적 일관성 · 경쟁 상태 · 캐시 스탬피드
사본을 다른 위치에 두는 캐시
공유 캐시 · 분산 캐시 · 니어 캐시 · CDN · 리버스 프록시 캐시 · 사설 캐시
사본이 차지하는 메모리를 다루는 기법
축출 · LRU · 캐시 키 · 캐시 히트 · 캐시 미스 · 힙 · 가비지 컬렉션
이 방식과 함께 쓰는 캐시 읽기·쓰기 패턴
cache-aside · read-through · write-through · write-behind
브라우저에서 이 방식을 맡는 장치
브라우저 캐시 · HTTP 캐시 · Cache-Control · ETag · 조건부 요청 · 서비스 워커
이 방식을 지원하는 저장소와 라이브러리
Redis · Caffeine · Guava · Ehcache
사본이 줄여 주는 비용 요소
다른 이름: client-side caching · client-side cache · 클라이언트 측 캐싱 · 클라이언트 사이드 캐시 · 클라이언트 캐싱