어느 한 온라인 쇼핑몰을 예로 들어보자. 세일 기간이나 특정 이벤트를 진행하면 사용자 트래픽이 어마어마하게 많아질 것이다. 과연 이러한 많은 트래픽 처리량을 달성하는 서비스는 얼마나 될까? 웬만한 서비스는 약간의 트래픽만 들어와도 CPU 처리량이 초과되고, 응답 시간은 계속해서 늘어난다.
어떻게 하면 성능을 개선할 수 있을까? 이에 대한 답으로 “캐싱 전략” 을 제안할 수 있다.
캐시는 데이터나 값을 미리 복사해놓는 임시 저장소를 말한다. 캐싱 전략은 웹 서비스 환경에서 데이터 접근 속도를 높이고 시스템의 호율성과 성능을 극대화하기 위해 사용되는 중요한 기술이다. 일반적으로 캐시는 메모리를 사용하기 때문에 DB보다 훨씬 빠르게 데이터 응답이 가능하다. 하지만, 모두 알다시피 메모리의 용량은 그리 크지 않다. 그렇기 때문에 어떤 종류의 데이터를 캐시에 저장할지, 데이터를 얼마나 많이 캐시에 저장할지, 오래된 데이터는 어떻게 할 것인지에 대한 “지침 전략” 을 알 필요가 있다.
<용어 설명>
Cache Hit(캐시 적중): 요청한 데이터가 이미 캐시에 존재해서, 캐시에서 바로 응답을 주는 경우로, 빠른 응답을 보장하고 서버나 DB에 부담이 적고, 사용자는 대기 시간 없이 결과 확인이 가능하다.Cache Miss(캐시 미스): 요청한 데이터가 캐시에 존재하지 않아서, 원래의 데이터 소스에서 데이터를 가져와야 하는 경우로, DB나 API까지 갔다 와야 하므로 응답 속도가 느리다.
캐시를 이용하게 되면 항상 “데이터 정합성” 이 거론된다. 데이터 정합성(Data Consistency)은 어느 한 데이터가 캐시와 데이터베이스, 이 두 곳에서 같은 데이터임에도 불구하고 데이터 정보값이 서로 다른 현상을 말한다. 예를 들어보면, 만약 고객A가 상품을 1개 구매하면 재고가 10개에서 9개로 줄어야 한다고 해보자. 근데 DB에는 9개, 캐시에는 여전히 10개라고 표시된다면 제품을 이중으로 결제할 수도 있는 문제가 발생할 수 있다. 정합성이 깨진 것이다.
예전에는 그냥 DB에서 데이터 조회와 작성을 처리했기 때문에 데이터 정합성 문제가 나타나지 않았지만, 이제 캐시라는 또 다른 데이터 저장소를 이용하기 때문에, 같은 종류의 데이터라도 서로 다른 두 저장소에서 저장된 값이 다를 수 있는 현상이 일어날 수 있는 것이다. 따라서 상황에 맞는 적절한 읽기와 쓰기 전략이 필요한 것이다.

Lazy-Loading라고도 부르는 이 방식은, 앱에서 데이터를 읽는 전략이 많을 때 사용하는 전략이다. Redis를 캐시로 쓸 때 가장 일반적으로 사용하는 방법이기도 하다.
이 전략은, 실제 요청된 데이터만 캐시에 저장되므로 불필요한 데이터 캐싱을 줄일 수 있다. 또한, 캐시에 문제가 발생해도 애플리케이션은 원본 데이터베이스에 직접 접근할 수 있기 때문에 서비스가 계속 작동할 수 있다는 장점이 있다. 하지만, 만약 캐시에 많은 커넥션이 붙어있는 상태에서 Redis에 장애가 생기면, 순간적으로 DB로 몰려서 부하가 발생할 수 있다. 그래서 데이터 정합성 유지가 어렵다. 그리고 초기 조회 시 무조건 DB를 접근한 다음, 캐시에 올려놓고 사용할 수 밖에 없기 때문에 DB에 부하가 발생한다. 따라서 단건 호출 빈도가 높은 서비스에는 적합하지 않다. 반복적으로 동일 쿼리를 수행하는 서비스에 적합한 아키텍처다.

항상 캐시를 통해 데이터를 읽는 전략이다. 위와 같이, 캐시는 앱과 DB 중간에 위치해 앱과 DB 모두 캐시만을 바라보게 된다.
읽기에 대한 앱의 관점에서 차이가 있다.
Look-Aside : 애플리케이션이 DB 조회와 캐시 갱신을 직접 관리해야 한다.Read-Through : 캐시에서 DB에 데이터를 직접 조회하여 로드한다. 그리고 캐시는 반드시 DB의 데이터 모델과 동일한 구조를 유지해야 한다.Read-Through Cache 방식에서는 동일한 데이터가 반복적으로 요청되는 읽기 중심 워크로드에서 가장 효과적이다. 그리고, 캐시와 DB 간의 동기화가 항상 이루어져 있기 때문에 항상 데이터의 정합성이 보장된다. 다만, 데이터가 처음 요청될 때는 항상 캐시 미스가 발생하며, 캐시를 채우는 추가적인 시간이 소요된다. 또한, 캐시가 죽어버리면, 애플리케이션에도 문제가 발생한다.
Look-Aside에서 캐시가 다운되면, 다시 새로운 캐시를 투입해야 하고 DB에 새로운 데이터를 넣으면 커넥션은 자연스럽게 DB로 몰리게 된다. 이런 경우, 초기에 캐시 미스가 많이 발생해서 성능 저하가 올 수 있다. 매번 첫 요청에 캐시 미스가 발생하는 Read-Through 패턴도 마찬가지다.
이럴 경우에는 캐시로 미리 데이터를 넣어주는 “Cache Warming” 작업을 해주면 된다. 실제 실무에서는 특정 이벤트 상품 오픈 전에 해당 상품의 조회수가 몰릴 것을 대비해서 상품 정보를 미리 DB에서 캐시로 올려두는 작업을 처리한다고 한다.


이 방식에서는 캐시에 데이터를 미리 한꺼번에 써 놓고, 나중에 DB에 기록한다. 다만, 기록할 때 DB에 바로 쿼리하지 않고, 캐시에 모아서 일정 시간이 지난 뒤에 한꺼번에 많은 양의 데이터를 한 번의 쓰기 요청으로 해결한다. 그렇게 DB에 저장된 데이터는 캐시에서 삭제한다. 이처럼 모아 놓았다가 한 번의 INSERT 문으로 처리하기 때문에 쓰기 쿼리 비용을 줄이고, 성능적으로 이점을 가져갈 수 있다.
쓰기 작업이 많고, 많은 양의 데이터를 읽어 들이는 서비스에 적합하다. Write-Back 패턴은 Read-Through 패턴과 결합하면 가장 최근 업데이트되고 액세스된 데이터를 항상 캐시에서 사용 가능하다. 다만, 캐시 스토어에만 데이터를 써 놓은 상태에서 캐시에 장애가 일어난다면, 데이터가 DB까지 직접 쓰여지지 않아 데이터 유실 문제가 발생할 수 있다.

이 방식은 항상 캐시를 통해서 쓰기를 진행한다. 캐시에 먼저 쓰고, DB에 바로 쓰기 작업을 진행하는 것이다. Write-Back과 마찬가지로, 캐시는 DB와 직렬로 연결되어 있으며, DB와 캐시가 완벽히 동기화되어 있어 캐시의 데이터는 항상 최신 상태로 유지되고, 캐시와 백업 저장소에 업데이트를 같이 하기 때문에 데이터 정합성을 유지할 수 있어서 안정적이다. 따라서 데이터 유실이 발생하면 안 되는 상황에 적합한 방식이다.
다만, 자주 사용되지 않는 불필요한 리소스가 저장되고, 매 요청마다 2번의 쓰기가 발생됨으로 빈번한 생성 및 수정이 발생하는 서비스에서는 성능 저하가 일어날 수 있다.

이 방식에서는 모든 데이터를 DB에 직접 기록하고 캐시를 갱신하지 않는다. 캐시 미스가 발생하는 경우에만 캐시에도 데이터를 저장하므로 캐시와 DB 내의 데이터가 일치하지 않아 데이터 정합성이 깨질 수 있다. 따라서 이 패턴은 데이터베이스에 데이터를 직접 쓰기 때문에 리소스를 아낄 수 있고, 성능이 매우 좋다. 다만, 캐시 미스가 발생한 경우에만, DB에서 캐시로 데이터를 저장하므로, 캐시와 DB 사이의 데이터가 일치하지 않아 데이터 정합성 유지가 어렵다.
따라서 Write-Around 패턴은 데이터가 한 번 쓰여지고, 자주 읽히지 않거나 읽지 않는 상황에서 좋은 성능을 제공할 수 있다.
<참고 자료>
[10분 테코톡] 저문, 라온의 Cache & Redis
캐싱과 캐싱 전략에 대해 알아보자
📚 캐시(Cache) 설계 전략 지침 총정리