일정(Schedule) 생성 시 알림을 자동으로 예약(30분 전)하고, 지정된 시각에 알림을 출력하는 서비스입니다.
Spring 기반 프로젝트이던 기존의 RainMind의 Schedule part를 FastAPI에서 구현하였습니다. Outbox pattern / Redis ZSet / Lua Script 이용 atomic operation을 FastAPI 환경으로 1:1 변환하는 것을 목표하였습니다.
원본 Spring 프로젝트 링크는 아래와 같습니다.
https://github.com/LOV-ING-U/project_rainmind
- Client: POST /schedules
- Schedule + Outbox(status = PENDING) 저장 with transaction
- event_publisher가 PENDING Outbox를 조회 후, redis zset에 알람 등록(score = alarm 시각)
- worker가 Lua script 이용한 atomic zset dequeue로 알람 1개 pop
- 알람 출력
일정 생성 후 알람을 redis에 등록하면 아래와 같은 redis와 DB의 데이터가 일치하지 않는 정합성 문제가 발생할 수 있습니다.
- DB에 일정 삽입에 성공했으나, Redis 알람 등록에 실패한 경우: 알람이 유실됩니다.
- Redis에 알람이 등록되었지만, DB 삽입은 실패한 경우: 유령 알람이 발생합니다.
- Worker 여러 대가 동시에 Redis를 보며 일정 확인을 위해 dequeue할 경우: 중복 알람 혹은 예기치 못한 동작이 발생합니다.
해당 프로젝트는 사용자가 알람을 등록할 때 발생하는 데이터 정합성 문제를 해결하기 위해 Outbox pattern을 적용합니다.
DB와 redis를 완벽히 동기화시키는 것은 불가능합니다. 따라서 서비스 설계 목적 상, 알람 유실이 유령 알람/중복 알람보다 치명적이기 때문에 알람 유실을 막기 위해 Outbox pattern을 적용합니다.
DB와 redis의 완전한 데이터 정합성 유지는 트랜잭션으로 그 작업들을 묶을 수 없으므로 현실적으로 불가능합니다. 따라서
- Schedule 생성 시 Outbox(PENDING)에 event를 transaction으로 함께 저장
- Publisher를 통한 PENDING Outbox 조회 및 Redis ZSet enqueue
- Worker가 Redis에서 atomic dequeue를 수행하여 알람을 출력
하게 된다면, DB나 Redis 둘 중 하나가 실패해도 알람이 유실되지 않고, 시스템 재시작 후에도 DB의 데이터를 통해 작업 수행이 이어서 가능합니다.
-
DB 정합성: Schedule, Outbox는 함께 Transaction 수행
-
Redis 미반영: Outbox 기반 event publisher의 지속적 재시도로 반영됨
-
Redis 알람 중복 dequeue 방지: Lua script 이용
-
전체 구조: Redis 중복 enqueue 가능성 존재(publisher의 재시도/중복 실행), Redis dequeue 이후 알람 유실 가능(at most once property), DB -> Redis 반영 유실 해결
서비스 설계 특성상 유실이 중복보다 심각한 문제이므로 이러한 trade off를 선택했습니다.
필요시 아래와 같은 확장이 가능합니다.
- Outbox status를 deleted 등 확장하여 알람 삭제 기능 지원
- 별도의 Kafka 등 메시지 브로커 사용
FastAPI의 AsyncClient를 이용하여 실제 router를 호출한 후 schedule/outbox 생성 및 event_publisher의 Redis enqueue 동작, 그리고 Worker의 redis dequeue 동작을 검증하는 테스트코드를 작성했습니다.
- docker 실행
- image 만들기
docker compose -f docker-compose.test.yml build --no-cache - container 실행
docker compose -f docker-compose.test.yml up --abort-on-container-exit
컨테이너 실행 종료 시 테스트가 통과합니다.