Redis와 MySQL 데이터 정합성 관리: TTL과 Lazy Loading을 활용한 설계 회고

이준우·2025년 4월 29일

Memetory

목록 보기
5/5

이전 글

📝 Redis와 MySQL 데이터 정합성 관리 설계 회고

문제 인식

리더보드 기능을 운영하면서, DB(MySQL)와 Redis(캐시) 간 데이터 불일치 문제가 발생할 수 있다는 점을 인식했습니다.
특히 게시물이 DB에서 삭제된 후에도 Redis 리더보드에 남아 있는 경우, 사용자에게 잘못된 정보가 노출될 수 있다는 문제를 발견했습니다.

단순히 데이터를 저장하고 조회하는 것을 넘어서,
DB와 Redis 간 정합성(Consistency)을 보장하는 구조를 고민할 필요가 있었습니다.


원인 분석

Redis는 NoSQL 기반이라 ACID 트랜잭션을 완전히 지원하지 않습니다.
Spring Boot의 @Transactional은 DB 트랜잭션만 관리할 수 있고, Redis 작업은 포함되지 않습니다.

또한 Redis의 MULTI/EXEC를 사용해도 중간 실패 시 롤백이 불가능합니다.
(명령어 순서만 보장할 뿐, 실패하면 자동 복구되지 않습니다.)

게다가 Redis는 싱글 스레드 이벤트 루프 모델이기 때문에,

  • 삭제 명령어와 생성 명령어 사이에
  • 다른 클라이언트 요청이 끼어들 경우
  • 사이드 이펙트(일관성 깨짐) 가 발생할 수 있습니다.

초기 해결 시도: 스케줄러 기반 데이터 최신화

처음에는 매일 새벽 03시에 스케줄러를 돌려

  • 전날 데이터를 삭제하고
  • 다음 날 조회를 대비해 Sorted Set을 미리 생성하는 구조를 생각했습니다.

그러나 여러 문제를 발견했습니다.

  • 싱글 스레드 특성: 삭제와 생성 사이에 외부 명령이 끼어들 수 있다.
  • MULTI/EXEC 한계: 중간 실패를 롤백할 수 없다.
  • 스케줄러 복잡성:
    • 실패했을 때 재시도 정책을 따로 관리해야 한다.
    • 서버 확장 시(다중 인스턴스) 스케줄러가 중복 실행되거나 충돌할 수 있다.
    • 이걸 막으려면 분산 락, 트리거 제어 로직 등을 추가해야 한다.

스케줄러만으로 관리하려 하다 보니
운영 복잡도와 장애 리스크가 지나치게 커질 것이라는 문제를 명확히 인식했습니다.


대안 탐색: Lazy Cache Loading 전략 활용

이미 리더보드 조회 API는 Lazy Cache Loading 방식을 사용하고 있었습니다.
Redis에 데이터가 없을 때만 DB를 조회해 채우는 방식입니다.

이 구조를 적극 활용하면,

  • 스케줄러는 삭제까지만 책임지고
  • 데이터 생성은 조회 시점에 Lazy하게 처리할 수 있습니다.

즉,
"필요할 때 채우자"는 자연스러운 흐름으로 구조를 전환할 수 있었습니다.


최종 구조 전환: TTL 기반 자동 삭제

스케줄러조차 삭제를 직접 하지 않고,
Redis 데이터 자체에 TTL(Time-To-Live) 을 설정하기로 했습니다.

  • 데이터를 저장할 때 TTL을 함께 설정하고
  • 하루가 지나면 자동으로 삭제되게 했습니다.

이후 사용자가 조회하면

  • Lazy Cache Loading을 통해 다시 DB 기준으로 신선한 데이터를 가져와 Redis에 저장합니다.

TTL 기반 삭제의 장점

  • 스케줄러 실패/중복 실행 걱정이 사라진다.
  • 별도의 재시도 로직이 필요 없다.
  • 서버 확장 시에도 락 없이 안전하게 관리된다.
  • 운영 복잡도가 크게 줄어든다.

결국, TTL을 활용하면
운영 리스크 없이 Redis 캐시 수명을 자연스럽게 관리할 수 있게 되었습니다.


최종 시스템 구조 요약

  1. Redis 조회 요청
  2. 데이터가 있으면 그대로 반환
  3. 데이터가 없으면 Redisson Lock 획득
  4. 락을 잡은 프로세스가 DB 조회 후 Redis에 저장
  5. Redis TTL 만료로 데이터가 자연스럽게 삭제
  6. 삭제 후 조회 시 Lazy Cache Loading으로 다시 채움

회고

처음에는 스케줄러를 통한 데이터 삭제/재생성 방식이 현실적인 방법처럼 보였습니다.
하지만 Redis의 특성과 스케줄러 운영 리스크를 충분히 고민한 끝에,

  • TTL을 통한 자연스러운 데이터 만료
  • Lazy Cache Loading을 통한 신선한 데이터 재생성

이 조합이 훨씬 단순하고 강건한 구조라는 결론에 도달했습니다.

특히,
- 운영 복잡도를 최소화하고
- 시스템 확장성(Scale-out 대응성)도 높이며
- 장애 복구성(failover recovery)까지 자연스럽게 확보한 점이 만족스러웠습니다.

✨ 주요 교훈

  • 캐시는 항상 즉각적으로 최신 상태를 유지할 필요는 없다.
    필요할 때 자연스럽게 갱신하는 구조가 운영 비용을 낮추고 장애 대응에도 강하다.
  • 시스템이 커질수록 복잡한 스케줄링 제어보다, 자연 만료(TTL) + Lazy Loading 설계가 리스크를 줄인다.
  • Redis의 싱글 스레드 특성과 트랜잭션 한계를 이해하고 설계에 반영해야 안정성을 확보할 수 있다.
profile
잘 살고 싶은 사람

0개의 댓글