HotSpot은 가족 단위 데이터 사용량을 실시간으로 반영하고 임계치 도달 여부를 빠르게 판단해야 하는 서비스였다. 이런 요구사항만 놓고 보면 Redis는 매우 자연스러운 선택이었다. 빠른 읽기·쓰기 성능을 제공하고 여러 상태를 한 번에 갱신하는 데도 유리했기 때문이다. 실제로 초기 구조에서는 Redis를 중심으로 이벤트를 처리했고 실시간성 측면에서는 기대한 만큼의 효과를 얻을 수 있었다.
하지만 구조를 조금 더 길게 바라보자 다른 질문이 생겼다.
“이 시스템은 빠르게 처리되기는 하지만 장애가 나도 끝까지 설명 가능한가?”
처음에는 Redis에 잘 반영되면 충분하다고 생각했다. 그러나 운영 관점에서 보면 그것만으로는 부족했다. 사용량이 왜 이렇게 되었는지, 어떤 이벤트가 실제로 반영되었는지, 중간에 장애가 나면 어디까지 처리된 것인지, 그리고 유실이 발생했을 때 무엇을 근거로 복구할 수 있는지까지 설명할 수 있어야 했다.
이 글은 Redis를 포기한 이야기가 아니다.
오히려 Redis의 장점은 살리되 Redis에 너무 많은 책임을 맡기지 않도록 구조를 다시 설계한 과정에 대한 기록이다.
초기 설계는 비교적 단순했다. 이벤트가 들어오면 Redis에서 즉시 사용량을 반영하고 그 과정에서 필요한 상태도 함께 갱신하는 구조였다. 이렇게 하면 각 이벤트를 빠르게 처리할 수 있었고 여러 단계를 애플리케이션에서 따로 나눠 처리하지 않아도 되기 때문에 흐름도 단순했다.
이 방식의 장점은 분명했다.
첫째, 실시간 반영이 빨랐다.
둘째, 여러 상태를 하나의 흐름 안에서 다룰 수 있어 중간 불일치 가능성을 줄일 수 있었다.
셋째, 처리량이 높아져도 비교적 예측 가능한 방식으로 동작했다.
그래서 처음에는 이 구조가 꽤 괜찮은 답처럼 보였다. 실제로 서비스의 핵심 요구사항인 “빠르게 반영되는 사용량 처리”에는 잘 들어맞았다.
문제는 시간이 지나면서 이 구조가 실시간 처리에는 강하지만 장애 이후의 추적과 복구에는 약하다는 점이 점점 선명해졌다는 것이다.
가장 크게 걸렸던 부분은 Redis 반영과 실제 완료를 같은 의미로 볼 수 없다는 점이었다.
예를 들어 이벤트를 받아 Redis에 사용량을 반영한 직후 아직 DB 기록을 남기기 전에 프로세스가 종료된다고 가정해보자. 시스템 입장에서는 Redis에는 이미 상태가 반영되어 있다. 하지만 그 사실을 나중에 설명할 근거나, 후속 처리를 이어갈 기록은 남아 있지 않을 수 있다.
더 문제는 이런 상황에서 동일 이벤트가 다시 들어왔을 때였다. Redis에는 이미 처리 흔적이 있으니 시스템은 이를 단순 중복으로 보고 종료할 가능성이 크다. 그렇게 되면 Redis 상태는 바뀌어 있는데 정작 이를 추적할 로그나 후속 처리 정보는 빠진 채로 남게 된다.
즉, 내가 마주한 문제는 단순히 “장애가 날 수 있다”는 수준이 아니었다. 정확히는 “실시간 상태는 바뀌었는데 그 변경을 끝까지 책임질 근거가 없을 수 있다”는 점이었다.
이 지점에서 기존 구조가 가진 한계를 분명히 보게 됐다. 기존에는 Redis에 반영되었다는 사실이 곧 처리 완료처럼 느껴졌지만 실제로는 그렇지 않았다.
반영된 것과 끝난 것은 다르다.
처음에는 Redis가 문제라고 생각하지 않았다. 오히려 Redis는 자기 역할을 아주 잘하고 있었다. 실시간 반영, 빠른 조회, 상태 계산 같은 부분에서는 기대 이상으로 잘 맞았다.
진짜 문제는 Redis의 성능이 아니라 역할의 경계가 흐려지고 있다는 점이었다.
원래 Redis는 빠르게 상태를 읽고 쓰는 데 강한 도구다. 그런데 구조가 발전하면서 Redis는 단순한 실시간 상태 저장소를 넘어 사실상 이벤트 처리 결과의 유일한 근거처럼 사용되기 시작했다. 그렇게 되면 Redis에 반영되었다는 사실 하나에 너무 많은 의미가 실리게 된다.
하지만 서비스 운영에서 필요한 것은 현재 상태만이 아니었다.
이 요구사항까지 Redis 하나에 기대는 것은 무리라고 판단했다. 그때부터 Redis를 버릴 것이 아니라 Redis의 역할을 다시 정의해야 한다고 생각하게 됐다.

그래서 내린 결론은 명확했다.
즉, Redis를 “진실의 근원”으로 두는 것이 아니라 실시간 상태 레이어로 위치를 조정하고 장애 대응과 추적, 복구의 근거는 DB가 맡는 구조로 바꾸는 것이다.
이때 핵심은 단순히 저장소를 하나 더 추가하는 데 있지 않았다. 더 중요한 건 완료의 기준 자체를 바꾼 것이었다. 기존에는 Redis에 반영되면 사실상 끝난 처리처럼 느껴졌다면 구조를 전환한 뒤에는 처리 단계를 분리해서 보기 시작했다.
이를 구현하기 위해 Redis 반영 이후에는 별도의 비동기 Writer가 처리 결과를 받아 DB에 로그와 후속 이벤트를 영속화한 뒤에만 ACK 하도록 구조를 바꿨다.
즉, 더 이상 “Redis 반영 성공 = 처리 완료”가 아니고 DB까지 durable하게 남아야 비로소 완료로 간주하는 구조로 경계를 재정의한 것이다. 이렇게 경계를 다시 나누자 시스템이 어디까지 처리되었는지 훨씬 더 명확하게 설명할 수 있게 되었다.
구조를 바꾸고 나서도 한 가지 고민은 남아 있었다.
Redis 반영은 성공했지만 그 뒤 단계가 끝나기 전에 중단된 이벤트를 어떻게 다룰 것인가 하는 문제였다.
처음에는 중복 이벤트를 단순히 버리면 된다고 생각할 수도 있다. 하지만 이 경우에는 “완전히 끝난 중복”과 “중간에 멈춘 반쪽짜리 처리”를 구분할 수 없다. 바로 이 지점이 위험했다. 중간에 멈춘 이벤트를 완전한 중복으로 오해하면 누락된 기록은 영영 복구되지 않을 수 있기 때문이다.

그래서 완료 상태를 더 세밀하게 나눠서 보게 됐다.
이렇게 나누면 “이미 처리된 이벤트”라는 말이 더 이상 모호하지 않다. Redis까지만 끝난 상태와 끝까지 책임이 완료된 상태를 구분할 수 있기 때문이다. 여기서 더 나아가 재수신된 이벤트를 단순 중복으로 버리지 않고 APPLIED 상태라면 DB 기록이 실제로 남아 있는지 확인한 뒤 누락되었다면 이전 처리 결과를 바탕으로 다시 영속화하는 방식까지 함께 고려했다.
이 판단이 중요했던 이유는 단순히 상태를 예쁘게 나누는 것이 목적이 아니었기 때문이다. 반쯤 끝난 이벤트를 실제로 끝까지 완료 상태로 끌고 가는 구조를 만들고 싶었다.
즉, 핵심은 이거였다.
중복 이벤트를 무조건 버리는 것이 아니라
반쯤 처리된 이벤트를 복구 가능한 대상으로 바라보는 것
이 관점이 들어가면서 구조는 단순한 비동기 처리 설계가 아니라 중간 실패까지 포함해 완료를 정의하는 구조로 바뀌었다.
구조를 DB 영속화 기반으로 바꾸면서 또 하나 중요했던 것은 DB 저장 실패를 모두 같은 실패로 다루지 않았다는 점이었다.
실제 운영에서는 실패 원인이 서로 다르고 그에 따라 대응 방식도 달라져야 한다.
예를 들어 DB timeout이나 일시적인 네트워크 지연처럼 복구 가능성이 있는 장애는 재시도를 통해 정상 처리로 이어질 수 있다. 반면 JSON 파싱 실패나 스키마 불일치처럼 재시도해도 성공 가능성이 낮은 장애는 같은 메시지를 계속 붙잡고 있어도 해결되지 않는다.
그래서 DB 영속화 실패는 두 가지로 나눠서 처리했다.

먼저 일시적 장애는 재시도 가치가 있는 경우로 보고 일정 횟수 이상 실패가 반복되면 Consumer를 일시 중지했다. 이때는 ACK를 하지 않아 오프셋을 커밋하지 않고 DB나 네트워크가 복구된 뒤 저장이 성공하면 그 시점에 ACK한 후 Consumer를 다시 재개하도록 했다. 즉, 처리 순서를 무너뜨리지 않으면서도 장애가 복구되면 자연스럽게 이어서 처리할 수 있게 한 것이다.
반대로 영구적 장애는 재시도로 해결되지 않는 경우로 보고 즉시 영구 실패로 분류했다. 그리고 해당 이벤트는 실패 테이블에 원인과 함께 기록한 뒤 ACK해서 처리 불가능한 메시지 하나가 전체 파이프라인을 계속 막는 상황을 피하도록 했다. 이는 처리 불가 메시지가 전체 흐름을 멈추게 하지 않도록 하기 위한 선택이었다.
이렇게 실패를 일괄적으로 재시도하지 않고 복구 가능한 실패와 복구 불가능한 실패를 구분하자 시스템은 단순히 데이터를 저장하는 구조를 넘어 실제 운영 중 장애까지 감안한 구조로 한 단계 더 정교해졌다.
이 전환 이후 가장 크게 달라진 점은 처리 단계의 의미가 명확해졌다는 것이다.
이전에는 Redis에 반영되면 완료처럼 보였지만 이제는 실시간 반영과 최종 완료를 분리해서 설명할 수 있게 되었다. 덕분에 장애가 발생해도 어디까지 처리되었는지 더 분명하게 말할 수 있다.
또한 중간 종료 상황에서 발생할 수 있는 기록 누락 가능성도 줄일 수 있었다. 재수신된 이벤트를 단순히 중복으로 버리지 않고 실제로 APPLIED 상태인지, DB 영속화가 누락된 상태인지를 확인한 뒤 필요한 경우 다시 영속화하는 관점을 도입했기 때문이다.
무엇보다 중요한 변화는 Redis에 장애가 나더라도 시스템을 다시 세울 수 있는 기준이 생겼다는 점이었다. DB에 남겨둔 처리 이력과 후속 처리 근거를 바탕으로 상태를 다시 맞출 수 있다는 것은 실시간성만 강조하던 구조에서 한 단계 더 나아갔다는 뜻이었다.
결국 시스템은 단순히 “지금 빠르게 반영되는 구조”에서 “장애 이후에도 설명 가능하고 복구 가능한 구조”로 진화하게 됐다.
물론 이 과정에서 복잡도는 분명히 증가했다.
처리 단계를 나눠야 했고 중간 상태를 관리해야 했으며 재수신 시 어떤 기준으로 복구할지도 고민해야 했다. 이전처럼 “받아서 Redis에 반영하면 끝”인 구조보다는 분명 더 복잡하다.
하지만 이 복잡도가 불필요한 복잡도라고 생각하지 않았다.
오히려 서비스가 실제로 운영되는 환경에서 발생할 수 있는 리스크를 생각하면 이 정도의 복잡도는 감수할 만한 가치가 있었다. 빠른 처리만 보장하는 구조보다 처리 이력을 설명할 수 있고 장애 이후에도 복구 가능한 구조가 훨씬 운영 친화적이기 때문이다.
이 경험을 통해 내가 배운 건 좋은 설계란 무조건 단순한 구조를 만드는 것이 아니라는 점이다.
때로는 시스템이 실제로 마주할 실패를 줄이기 위해 필요한 복잡도를 받아들여야 한다. 중요한 것은 복잡도를 늘리는 것이 아니라 어떤 복잡도는 제거하고 어떤 복잡도는 책임 있게 선택하는가였다.
처음의 HotSpot은 Redis를 중심으로 빠르게 동작하는 시스템이었다. 하지만 서비스를 진짜 운영 가능한 구조로 바라보기 시작하면서 빠르기만 한 구조로는 부족하다는 걸 알게 됐다.
이 질문들을 따라가며 결국 Redis를 버리는 대신 Redis의 역할을 다시 정의했다.
Redis는 실시간성을 담당하고 DB는 기억과 복구를 담당하도록 구조를 나눴다.
그리고 이 과정에서 가장 크게 배운 점은 시스템 설계는 단순히 빠르게 처리하는 방법을 찾는 일이 아니라는 것이다.
진짜 중요한 것은 장애 이후에도 같은 결과를 설명할 수 있도록 만드는 것이다.