CS 지식/데이터베이스

실무에서 곧바로 적용 가능한 캐싱 전략 6가지

마늘냄새폴폴 2026. 4. 22. 22:53

안녕하세요! 오랜만에 공부할 시간이 생겨서 공부하고 포스팅을 적게 되었습니다. 요즘 X(구 트위터)에 저한테 굉장히 도움되는 패치가 이루어졌습니다. 바로 '자동 번역 기능'! 누군가는 굉장히 거슬리는 패치일 수 있지만 저는 X를 개발 인사이트를 얻는 용도로 쓰다보니 해외에 있는 개발자들이 올리는 양질의 게시글들을 통해 많은 인사이트를 얻고 있습니다. 

 

많은 인사이트를 얻음과 동시에 어떤걸 공부할까에 대한 소스도 제공해주고 있어서 개인적으로는 굉장히 쓸만한 패치가 된 것 같아서 기분이 좋습니다. 

 

한 개발자의 게시글로 시작된 이 포스팅은 다음과 같은 순서로 진행됩니다. 

 

  1. 읽기 전략
    1. 가장 기본적인 Cache Aside
    2. 캐싱 전략계의 전략패턴 Read Through
    3. TTL의 함정 Refresh Ahead
  2. 쓰기 전략
    1. 우린 일관성이 가장 중요해 Write Through
    2. DB의 쓰기 부하를 줄여보자 Write Behind
    3. 이건 캐시에 적으면 안돼 Write Around

이제 하나씩 알아보겠습니다! 

 

읽기 정책

Cache Aside

캐시를 사용한다고 하면 가장 일반적으로 볼 수 있는 형태인 Cache Aside입니다. 요청이 들어오면 먼저 애플리케이션이 반응하고 캐시를 조회한 다음 캐시에 있으면 캐시에 있는 값을 보내주고 없으면 DB를 조회해서 반환한 뒤 캐시에 등록하는 방법이죠. 

 

이 방식은 가장 구현하기 간단한데 단순히 분기를 추가해주기만 하면 됩니다. 

if (cache에 있으면)
	return cache.value

else
  	DB조회
  	cache 등록
  	return DB.value

 

이 방식은 가장 간단한만큼 두 가지 문제가 발생합니다. 

 

도메인이 늘어날 때마다 같은 코드가 반복된다. 

모든 코드에 if else 문이 들어가야하고 만약 도메인 이수십개가 된다면 모든 조회 로직에 캐시 조회 로직을 넣어햐하는게 가장 큰 문제인데요. 

 

이것은 객체지향 5대 원칙인 SOLID에서 OCP를 위배하게 됩니다. 확장해도 코드의 변경이 있으면 안된다는 OCP의 원칙에서 가장 중요한건 코드의 변경이 없다는 것입니다. 

 

만약 도메인이 하나 추가된다거나 캐시의 버전이 바뀌어서 구현 방법이 달라진다거나 캐시를 바꿔야한다거나 이런 변경에 모든 분기문을 고쳐야한다는 문제는 이 애플리케이션의 확장성을 크게 해칩니다. 

 

Thundering Herd의 문제

특정 키에 요청이 많이 몰리는 경우 이를 핫 키(Hot Key) 현상이라고 부릅니다. 만약 어떤 캐시 값이 TTL로 사라지는 찰나에 순간적으로 해당 키에 대한 요청이 들어오는 경우 순간적으로 DB에 조회 요청이 들어가게 되고 DB에 스파이크성 부하가 생기게 됩니다. 이는 심각한 경우 DB 서버의 다운으로 이어질 수 있는 문제입니다. 

 

이러한 문제를 Thundering Herd 문제라고 부르며 이 문제는 조회연산이 많이 필요한 이커머스 서비스나 하루에도 수백만건의 거래가 일어나는 증권사 앱같은 경우에 종종 발생하는 문제입니다. 

 

 

이제 다음 소개드릴 두 가지 캐싱 전략은 앞서 Cache Aside가 갖고 있던 문제를 해결하기 위해 나온 방법론입니다. 

 

Read Through

Read Through는 캐시에 데이터가 떨어질 것 같은 데이터를 DB에 요청해서 필요한 데이터를 관리하는 방법인데요. 이렇게 되면 애플리케이션에서 캐시에 데이터가 있는지 확인하는 분기문이 사라지게 됩니다. 

 

하지만 Redis나 Memcached 같은 인메모리 캐싱 솔루션에서는 기본적으로 제공해주지 않는 기능입니다. 

 

이 기능은 보통 라이브러리/프레임워크 레벨에서 지원하는데 일례로 Spring Boot의 경우 @Cacheable 어노테이션을 이용하면 프레임워크가 Read Through 처럼 동작하게 할 수 있습니다. 

 

Read Through를 이용하면 서비스 레이어에서 캐시와의 의존성을 제거할 수 있고 만약 변경이 생기면 어댑터 부분만 바꾸면 전체 서비스 레이어에 적용되어 코드의 변경 없이 확장할 수 있게 됩니다. 

 

Refresh Ahead

Refresh Ahead는 Thundering Herd를 해결하기 위해 나온 방법론인데 실제로 어떤 키가 핫키인지 확인한 뒤 TTL이 끝나기 전에 TTL을 갱신시키는 방법입니다. 

 

캐시에 데이터가 사라지지 않고 남아있기 때문에 Thundering Herd 문제가 절대로 발생하지 않는다는 장점이 있지만 중요한 것은 어떤 키가 핫키인지 파악하는 것입니다. 만약 의미없는 키를 핫키로 생각하고 등록했다가는 자원만 낭비할테니까요. 

 

cf) 만약 핫키에 요청이 캐시서버가 버틸 수 없는 정도로 (초당 100만번) 요청이 들어오면 어떻게 할까? 

이 문제에 대한 해결에는 크게 세 가지 방법이 있습니다. 

 

  1. 키를 샤딩한다
  2. 애플리케이션에 매우 짧은 TTL(100ms)의 로컬 캐시를 만든다
  3. 캐시 앞에 존재하는 CDN등에 데이터를 올린다. 

보통 이런 경우 가장 관리 포인트(Toil)가 적은 쪽을 선택하게 되는데 그것이 바로 2번입니다. 

 

애플리케이션에 매우 짧은 TTL(100ms)로 로컬 캐시를 만드는 것만으로도 부하의 90퍼센트를 줄일 수 있습니다. 

 

쓰기 정책

Write Through

Write Through는 쓰기 작업을 할 때 캐시와 DB에 동시에 씀으로써 두 데이터베이스에 일관성을 맞추기 위해 고려된 방법론입니다. 보통 이 경우 일관성이 중요한 금융권에서 필요한 방법론인데 이 방법은 일관성이 정확하게 맞춰지긴 하지만 두번의 쓰기 작업이 생긴다는 문제가 발생합니다. 

 

일부 NoSQL을 제외한 대부분의 데이터베이스는 인덱스로 데이터를 관리하고 있기 때문에 보통 읽기 연산에 특화되어있고 쓰기 연산에 취약한 모습을 보이기 때문에 데이터베이스에 쓰기 작업이 많이 생기는건 썩 좋은 상황은 아닙니다. 

 

그렇기 때문에 쓰기 연산이 두번 일어나는 상황은 꺼려지게 되고 보통 이런 경우 먼저 DB에 쓰고 캐시에 있는 키를 삭제해서 Cache Aside를 유발하는 쪽으로 사용하곤 합니다. 

 

 

Write Behind

Write Behind는 쓰기 작업을 할 때 DB의 부하를 줄여주기 위해서 나온 방법론입니다. 의의는 쓰기작업을 할 때마다 DB에 요청하지 말고 캐시에 한번 쓰고 일괄로 배치작업을 돌려서 밀어넣자는 것이 이 방법론의 핵심입니다. 

 

이 방법의 장점은 DB의 쓰기 부하를 줄여준다는 것과 쓰자마자 바로 화면에서 조회할 수 있기 때문에 UX측면에서 굉장한 이득이 되는겁니다. 

 

하지만 이 방법의 가장 큰 문제는 Redis가 모종의 이유로 장애가 생긴다면 모든 데이터가 유실된다는 문제가 생깁니다. 

 

또한, 단순히 DB의 쓰기 부하를 줄이기 위해 쓰기 연산을 뒤로 미루는건 MQ(Message Queue)를 이용하면 문제를 더 깔끔하게 해결할 수 있습니다. 

 

보통의 MQ는 메세지가 발행되는 시점에서 반드시 하나의 브로커에 전달되도록 설계되어있고 만약 브로커에 문제가 생겨 메세지가 전달되지 못하더라도 바로 리밸런싱이 되면서 다른 브로커에 전달되는 것이 일반적입니다. 또한, 메세지가 실패하는 경우 DLQ를 이용해서 얼마든지 재시도를 할 수 있습니다. 

 

다만, MQ를 사용한다면 MQ에 대한 이해가 있어야되고 메세지와 트랜잭션을 원자적으로 묶기 위해서는 Outbox Pattern이나 Log Tailling Pattern을 추가적으로 적용해야해서 관리포인트가 늘어난다는 단점이 있습니다. 

 

Write Around

Write Around는 쓰기 작업에서 캐시와 DB에 모두 써야하는 상황인데 캐시에 올리기에는 의미없는 데이터를 써야할 때 사용하는 방법론입니다. 보통 대용량의 로그 데이터가 해당되는데 이때 Write Around라는 이름에 걸맞게 캐시를 우회해서 곧바로 DB에 쓰는 방법을 사용합니다. 

 

하지만 이 방법 또한 더 나은 방법이 존재하는데 보통 이런 경우 ELK 스택을 사용하거나 GCP의 BigQuery등을 사용하면 더 깔끔하게 문제를 해결할 수 있어 범용적으로 많이 사용되는 방법론은 아닙니다. 

 

마치며

아무래도 캐싱이 읽기 부하를 줄이기 위해 쓰이는 만큼 쓰기 정책에서는 꽤나 다양한 대안책이 있어서 취사선택해서 사용하시면 될 것 같습니다. 모든 개발이 그렇지만 장/단이 존재하는 상황에서 최적지점을 찾아내는 과정이 중요하고 캐싱 전략의 경우 속도와 일관성 사이에서 선택해서 적절히 우리 서비스에 맞는 정책을 사용해야할 것 같습니다. 

 

이번 포스팅에서는 대부분의 서비스가 캐싱을 많이 사용하고 그렇기에 다양한 방향으로 발전해온 만큼 알아두면 좋을 6가지 캐싱 전략을 알아보았습니다. 

 

데이터베이스 공부는 역시 언제해도 재밌는 것 같습니다. 여러분들에게 좋은 인사이트가 되었길 바라면서 이번 포스팅은 여기서 마무리짓도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!