캐시와 TTL로 효율적인 자동 저장 시스템 만들기
Write-Behind & Cache Aside 패턴과 TTL 관리로 초안 자동 저장 최적화하기
개요
프로젝트에서 게시물 초안 저장 기능을 구현하기 위해 Pinterest의 초안 저장 기능을 뜯어보았다. Pinterest는 사용자가 작성중인 게시물을 임시로 저장하는 초안 단계와 실제로 등록하는 발행 단계로 나뉜다. 초안 단계에서는 사용자의 입력을 자동으로 저장하는 기능을 제공하는데, 살펴보니 조금 특이한 점이 있었다.
- 사용자가 입력하지 않을 때는 주기적으로
/create요청을 보낸다. - 사용자가 입력을 시작하면, 입력이 끝나기 전까지 1-2초 간격으로
/update요청을 보낸다.
이를 분석하며 두 가지 궁금증이 생겼다.
- 초안을 이미 생성한 상태에서 왜
/create요청을 주기적으로 보낼까? /update요청이 이렇게 빈번하다면, DB에 쓰기 부하가 발생하지 않을까?
이러한 의문을 해결하기 위해 빈번한 쓰기 요청을 처리하면서 DB 부하를 줄일 수 있는 캐시 전략을 도입하였다. Redis 같은 인메모리 캐시를 활용하면 즉시 저장은 빠르게 처리하고, DB 반영은 지연시킬 수 있다. 아래에서 해당 내용들을 하나씩 풀어보겠다.
📒 캐시 전략
1️⃣ Write Behind: DB에는 나중에 저장하자
먼저 두 번째 의문부터 살펴보았다.
빈번한 자동 저장 요청이 1-2초마다 DB에 직접 쓰기를 발생시킨다면, DB에 부하가 걸리기 마련이다. 이를 해결하기 위해 Write-Behind 패턴을 적용했다. Write-Behind는 데이터를 즉시 DB에 반영하지 않고, 먼저 캐시에 저장한 뒤, 주기적으로, 그리고 비동기적으로 DB에 반영하는 전략이다.
구현 방식은 다음과 같다:
- 사용자의 입력 데이터를 Redis에 저장한다.
- 스케줄러를 사용하여 주기적으로 Redis의 데이터를 DB에 반영한다.
- 최신 데이터만 DB에 저장하므로 중복 쓰기를 줄여, 성능을 최적화한다.
해당 방식은 DB에 반영하는 시점에 최신 업데이트된 데이터만 반영하면 된다. 따라서 DB connection 수를 줄여 성능을 개선할 수 있고, 사용자에게 빠른 응답을 제공할 수 있다는 장점이 있다.
2️⃣ Cache Aside: 캐시부터 확인하자
Write-Behind 패턴을 통해 DB 부하를 줄였지만, 새로운 문제가 생겼다.
최신 데이터가 Redis에만 존재하고 DB에는 늦게 반영되므로, 사용자가 실수로 새로고침하거나 다른 기기에서 접속했을 때, 최신 데이터를 볼 수 없는 상황에 맞닥뜨릴 수 있다. 예를 들어, 사용자가 모바일에서 초안을 작성하다가 PC에서 접속하면 DB에 반영되지 않은 초안 데이터는 유실된 것처럼 보일 수 있다.
이를 해결하기 위해 Cache Aside 패턴을 적용했다:
- 초안 조회 시, 먼저 Redis에서 데이터를 확인한다.
- 만약 Redis에 데이터가 없다면 DB에서 데이터를 가져와 Redis에 채워 넣는다.
- 이후 요청은 Redis에서 빠르게 데이터를 제공해준다.
이를 통해 데이터의 일관성을 유지할 수 있고, Redis의 빠른 응답 속도 또한 기대할 수 있게 됐다.
❌ 캐시 무효화 정책
⌛ 캐시의 수명을 정해주자
Redis는 한정적인 자원이기 때문에, 무효화 정책을 반드시 설정해주어야 한다.
가장 단순한 방법은 TTL(Time to Live)를 설정하여, 일정 시간 후 캐시를 자동으로 삭제되도록 하는 것이다.
하지만 TTL을 어느 정도로 설정하는 데도 고민이 필요하다.
- TTL을 너무 길게 잡으면, 사용하지 않는 초안이 Redis에 오래 남아 메모리를 낭비하게 된다.
- TTL을 너무 짧게 잡으면, 사용자가 작성중인 초안이 Redis에서 삭제되어, 데이터 손실 문제가 발생할 수 있다.
따라서 사용자가 머무르는 시간을 추적하면서 적정선을 맞춰나가는 것이 이상적이라 생각하지만, 개발 환경이기 때문에 임시로 30분으로 설정했다.
/create요청의 정체?TTL 설정을 고민하는 과정에서 문득 Pinterest의
/create요청의 역할을 떠올렸다. Pinterest는 초안이 이미 생성된 상태에서도/create요청을 주기적으로 보내는데, 해당 요청은 탭이 활성화되어 있을 때만 꾸준히 발생했다.이를 통해 알 수 있는 것은 사용자가 활동중임을 서버에 알려 세션을 유지하는 요청이라는 것이다. 추측컨데 해당 요청을 통해서 TTL 만료를 갱신하는 역할도 겸하고 있는 것으로 보여진다. 즉 사용자가 여전히 초안을 작성 화면에 머물러 있다는 사실을 서버에 알림과 동시에 TTL을 연장함으로써 사용자가 작성중인 초안이 캐시에서 삭제되지 않도록 보장해주는 것이다.
마무리
Pinterest의 초안 저장 메커니즘을 분석하며, 빈번한 DB 쓰기를 줄이고 데이터의 일관성을 유지하는 설계를 고민해보았다.
- Write-Behind 패턴으로 Redis를 활용하여, DB 부하를 최소화하고 빠른 응답을 제공하였다.
- Cache Aside 패턴으로 Redis와 DB 간 데이터 일관성을 유지하며, 최신 데이터를 빠르게 제공하였다.
- TTL과
/create요청을 통해 캐시를 효율적으로 관리하여, 초안 데이터의 유효성을 보장하였다.
다만, 이러한 전략은 Redis 장애 시 최신 데이터가 유실될 수 있다는 단점이 있다. 초안을 자동 저장한다는 요구사항의 주목적은 사용자에게 편의를 제공하는 것이므로, 최악의 경우 DB에 저장된 이전 버전으로 복구 가능하다는 점에서 이 정도 손실은 허용 가능한 수준이라고 판단하였다.
물론 Redis 복제 혹은 클러스터링으로 고가용성을 확보하여, 데이터 손실 위험을 최소화하는 방법도 추가로 고려해볼 수 있다.
다음 글에서는 이러한 설계를 바탕으로 구현하는 과정에서 마주친 주요 이슈들(Redis Pipeline을 통한 호출 최적화, JPA Entity 직렬화 문제)에 대해 공유할 예정이다.