로컬 캐시는 네트워크 왕복이 없어 빠르지만 서버마다 값이 달라 일관성이 깨집니다. 분산 캐시는 반대입니다.
로컬 캐시 vs 분산 캐시
비교
| 구분 | 로컬 (인프로세스) | 분산 (Redis 등) |
|---|
| 조회 지연 | 메모리 접근 수준 | 네트워크 왕복 포함 |
| 용량 | 프로세스 힙 한도 | 수평 확장 가능 |
| 정합성 | 인스턴스마다 값이 다를 수 있음 | 모든 인스턴스가 같은 값 |
| 재시작 | 매번 비워짐 | 유지됨 |
| 운영 비용 | 추가 인프라 없음 | 별도 서버와 운영 필요 |
2단계 캐시
요청 -> [L1 로컬] 미스 -> [L2 Redis] 미스 -> [DB]
짧은 TTL 긴 TTL
로컬 캐시가 만드는 문제
빠르지만 서버마다 다른 값을 들고 있을 수 있습니다. 그것이 이 방식의 값이자 대가입니다.
| 상황 | 무슨 일이 일어나나 |
|---|
| 새로고침했는데 값이 오간다 | 요청이 서버 A와 B에 번갈아 닿았다 |
| 한 서버만 옛 값을 준다 | 그 서버의 캐시만 안 지워졌다 |
| 배포 직후 느리다 | 새 서버의 캐시가 비어 있다 |
둘째를 지우려면 모든 서버에 알려야 합니다. 그 통로가 없으면 로컬 캐시는 수명으로만
다룰 수 있고, 그래서 수명이 곧 어긋남의 상한이 됩니다.
무엇을 어디에 두나
같은 캐시에 다 넣지 않고 성격으로 나눕니다.
| 성격 | 어디 |
|---|
| 거의 안 바뀌고 작다 | 로컬. 설정, 코드표 |
| 자주 바뀌고 정확해야 한다 | 공용 |
| 크고 서버마다 들고 있기 아깝다 | 공용 |
| 매우 자주 읽고 조금 틀려도 된다 | 로컬 짧은 수명 + 공용 |
마지막이 2단계 구성의 이유입니다. 로컬이 대부분을 받아내고 공용이 정답을 쥡니다.
실무 포인트
- 로컬 캐시의 진짜 문제는 무효화다. 인스턴스가 10대면 지울 곳도 10군데다
- 그래서 로컬 TTL은 짧게 두고 불일치가 남는 시간을 스스로 제한한다
- 거의 변하지 않는 설정과 코드 테이블은 로컬 캐시가 압도적으로 유리하다
- 항목 수 상한을 반드시 설정한다. 무제한 로컬 캐시는 힙을 늘려 GC 지연을 만든다
- 무효화 전파가 필요하면 Pub/Sub 알림이나 서버 보조 클라이언트 캐싱(트래킹)을 쓴다
Q.로컬 캐시와 분산 캐시를 어떤 기준으로 나눠 쓰나요?
데이터의 성격으로 나눕니다.
| 성격 | 어디 | 이유 |
|---|
| 거의 안 바뀌고 작다 | 로컬 | 설정, 코드표. 어긋나도 영향이 없다 |
| 자주 바뀌고 정확해야 한다 | 분산 | 서버마다 다르면 사고가 난다 |
| 크다 | 분산 | 서버마다 들고 있으면 메모리 낭비다 |
| 매우 자주 읽고 조금 틀려도 된다 | 로컬 짧은 수명 + 분산 | 2단계 구성 |
| 사용자별로 다르다 | 분산 | 로컬에 두면 적중률이 서버 수만큼 떨어진다 |
마지막 줄을 자주 놓칩니다. 사용자별 데이터를 로컬에 두면, 그 사용자가 다른 서버로
가는 순간 캐시가 없습니다. 서버가 10대면 적중률이 10분의 1이 됩니다.
로컬의 값은 네트워크 왕복이 없다는 것이고 대가는 서버마다 다를 수 있다는 것입니다.
분산은 반대입니다. 그래서 질문은 늘 같습니다. 이 데이터가 서버마다 잠깐 달라도 되는가.
흔한 실수: 빠르다는 이유로 로컬을 기본값으로 두는 것. 같은 사용자가 새로고침할 때마다
값이 오가는 증상이 나오고, 원인을 찾기 어렵습니다. 어긋나도 되는지를 먼저 정하고
그 다음에 속도를 봅니다.
Q.로컬 캐시를 여러 인스턴스에서 쓸 때 무효화는 어떻게 하나요?
로컬 캐시는 원격에서 지울 수 없습니다. 그래서 셋 중 하나를 고릅니다.
| 방법 | 내용 | 대가 |
|---|
| 수명으로만 | 짧은 TTL 을 두고 알아서 만료되게 한다 | 그 수명이 곧 어긋남의 상한 |
| 알림 | 변경 시 모든 인스턴스에 알려 각자 지우게 한다 | 통로가 필요하고 유실될 수 있다 |
| 버전 키 | 공용 저장소의 버전 번호를 함께 읽어 다르면 버린다 | 읽을 때마다 확인이 필요하다 |
첫 번째가 기본값이어야 합니다. 가장 단순하고 실패할 부품이 없습니다. 수명을 몇 초로
잡으면 대부분의 경우 충분합니다.
알림 방식은 발행 구독 통로를 씁니다. 다만 알림이 유실되면 그 인스턴스만 옛 값을
계속 들고 있습니다. 그래서 알림을 쓰더라도 수명을 함께 두어야 합니다. 알림은 빠르게
맞추는 수단이고, 수명은 최악을 막는 안전장치입니다.
배포도 고려해야 합니다. 인스턴스가 새로 뜨면 캐시가 비어 있어서, 배포 직후에 원본으로
요청이 몰립니다. 인스턴스를 한 번에 다 바꾸지 않는 것으로 완화합니다.
흔한 실수: 알림만 믿고 수명을 길게 잡는 것. 알림 한 번이 유실되면 그 인스턴스는
수명이 다할 때까지 틀린 값을 줍니다. 유실을 전제로 상한을 둡니다.
Q.2단계 캐시 구성에서 각 계층의 TTL은 어떻게 정하나요?
로컬을 짧게, 공용을 길게 둡니다. 역할이 다르기 때문입니다.
| 계층 | 역할 | 수명 |
|---|
| 로컬 | 대부분의 읽기를 받아낸다 | 초 단위. 어긋남의 상한이 된다 |
| 공용 | 정답을 쥐고 원본을 보호한다 | 분 단위 이상 |
로컬 수명이 곧 사용자가 볼 수 있는 최대 지연입니다. 값이 바뀐 뒤 로컬 수명만큼은
옛 값이 나올 수 있습니다. 그래서 그 데이터가 몇 초까지 틀려도 되는지가 그대로 숫자가
됩니다.
공용을 길게 두는 이유는 원본 보호입니다. 로컬이 만료돼도 공용이 받아 주면 원본까지
안 갑니다. 두 계층이 동시에 만료되면 그 보호가 사라지므로, 로컬 수명이 공용 수명의
약수가 되지 않게 하고 약간의 무작위를 섞습니다.
값을 고칠 때는 공용을 먼저 지우고 로컬은 수명에 맡깁니다. 순서를 뒤집으면 로컬이
지워진 직후 공용의 옛 값을 다시 읽어 와 로컬이 옛 값으로 채워집니다.
흔한 실수: 두 계층에 같은 수명을 주는 것. 그러면 함께 만료돼 원본으로 한꺼번에
몰리고, 2단계로 나눈 이득이 사라집니다.
Q.로컬 캐시가 GC에 미치는 영향은?
로컬 캐시는 오래 살아남는 객체를 많이 만듭니다. 그래서 수집기가 가장 싫어하는
모양입니다.
| 현상 | 이유 |
|---|
| 오래 사는 영역이 계속 찬다 | 캐시 객체가 짧은 수명 영역을 통과해 넘어간다 |
| 정지 시간이 길어진다 | 살아 있는 객체가 많을수록 훑을 것이 많다 |
| 만료 시 한꺼번에 쓰레기가 된다 | 동시에 버려지면 그 순간 부담이 몰린다 |
| 메모리 부족으로 죽는다 | 상한 없는 캐시가 계속 자란다 |
마지막 줄이 가장 흔한 사고입니다. 크기 상한이 없는 캐시는 캐시가 아니라 메모리
누수입니다. 개수든 용량이든 상한을 반드시 둡니다.
정지 시간이 문제라면 캐시 크기를 줄이는 것이 먼저입니다. 수집기 옵션을 만지는 것보다
살아 있는 객체를 줄이는 쪽이 확실합니다. 큰 데이터를 굳이 로컬에 둘 이유가 있는지
다시 봅니다.
만료가 몰리는 것은 수명에 무작위를 섞어 흩습니다. 같은 순간에 채워진 항목이 같은 순간에
버려지면 그때 부담이 집중됩니다.
흔한 실수: 힙이 남으니 캐시를 크게 잡아도 된다고 보는 것. 힙이 커질수록 한 번의
수집이 훑을 것이 늘어 정지 시간이 길어집니다. 응답 지연의 상위 구간이 그 시간에
그대로 얹힙니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
캐시/Redis 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.