실시간 시스템의 핵심은 화면이 빠르게 갱신되는 것이 아닙니다. 여러 요청이 동시에 도착하고 일부 구성요소가 실패해도 최종 상태를 신뢰할 수 있어야 합니다. 특히 결제, 지갑, 베팅, 주문처럼 값이 한 번만 반영되어야 하는 시스템은 속도보다 처리 규칙이 먼저입니다.
중복 요청을 전제로 한 설계
사용자의 재클릭, 모바일 네트워크 재전송, 프록시의 재시도, 큐 소비자의 재시작으로 같은 명령이 여러 번 도착할 수 있습니다. 중복을 완전히 막으려 하기보다 같은 요청이 다시 실행되어도 결과가 한 번만 반영되도록 설계해야 합니다.
멱등성 키는 요청마다 고유한 업무 식별자를 부여하고 처리 결과와 함께 저장하는 방식입니다. 키만 캐시에 짧게 보관하면 캐시 장애나 만료 후 중복이 다시 발생할 수 있으므로, 금액이나 자산이 바뀌는 작업은 원장 데이터와 같은 트랜잭션 경계에서 중복 여부를 확정하는 편이 안전합니다.
순서가 보장되지 않는 이벤트
생성, 취소, 완료 이벤트를 전송한 순서와 소비자가 받는 순서는 다를 수 있습니다. 네트워크 지연이나 파티션별 병렬 처리 때문에 취소가 생성보다 먼저 도착할 수도 있습니다.
단순히 마지막으로 도착한 값을 저장하면 오래된 이벤트가 최신 상태를 덮어씁니다. 엔티티별 버전이나 순번을 두고 현재 버전보다 이전 이벤트는 무시하거나 별도 보류해야 합니다. 서로 다른 생산자가 같은 대상을 변경한다면 순서의 기준을 누가 발급하는지도 명확해야 합니다.
명시적인 상태 변경 규칙
상태 문자열을 어디서든 자유롭게 바꾸면 불가능한 전이가 생깁니다. 상태 머신은 허용된 전이를 코드와 데이터 양쪽에서 검증하게 합니다.
REQUESTED -> PROCESSING -> COMPLETED
| |
+-> CANCELED +-> FAILED -> RETRYING
완료된 거래가 다시 처리 중으로 돌아가거나, 취소된 작업이 뒤늦은 성공 응답으로 완료되는 것을 막아야 합니다. 전이에는 변경 원인, 요청자, 외부 응답 식별자와 시간을 함께 기록해야 장애를 재구성할 수 있습니다.
저장과 이벤트 발행 사이의 단절
데이터베이스 저장 후 큐 발행 전에 프로세스가 종료되면 상태는 바뀌었지만 후속 작업이 시작되지 않습니다. 반대로 이벤트를 먼저 발행하면 소비자가 아직 존재하지 않는 데이터를 읽을 수 있습니다.
트랜잭셔널 아웃박스는 업무 데이터와 발행할 이벤트를 같은 데이터베이스 트랜잭션에 저장한 뒤 별도 프로세스가 이벤트를 전달합니다. 전달 자체는 중복될 수 있으므로 소비자도 멱등성을 가져야 합니다. 중요한 것은 정확히 한 번 전송된다는 가정이 아니라, 여러 번 전달되어도 한 번만 반영되는 구조입니다.
최소 범위의 동시성 제어
분산 락은 유용하지만 모든 요청을 하나의 락으로 묶으면 처리량과 장애 범위가 커집니다. 잔액, 재고, 좌석처럼 충돌하는 업무 식별자 단위로 범위를 제한해야 합니다.
데이터베이스의 조건부 갱신이나 낙관적 버전 검사가 가능한지 먼저 검토합니다. 락을 사용한다면 만료 시간, 작업 시간 초과, 락 소유권 검증, 프로세스 중단 후 복구를 함께 설계해야 합니다. 락을 얻었다는 사실만으로 외부 API와 내부 데이터가 하나의 트랜잭션이 되지는 않습니다.
재시도가 실패를 증폭하는 조건
모든 오류를 즉시 재시도하면 장애가 난 외부 서비스에 더 큰 부하를 보내게 됩니다. 오류를 일시적 실패, 영구 실패, 판단 불가로 구분하고 재시도 가능 항목만 지수 백오프와 임의 지연을 적용합니다.
최대 횟수를 초과한 메시지는 폐기하지 말고 별도 실패 큐로 이동합니다. 운영자는 원인, 시도 횟수, 마지막 오류, 관련 업무 식별자를 확인하고 안전하게 재처리할 수 있어야 합니다. 재처리 역시 멱등성을 전제로 합니다.
시간 초과와 결과 미확정
외부 결제나 지갑 API가 시간 초과를 반환해도 상대 시스템에서는 성공했을 수 있습니다. 이때 곧바로 실패 처리하고 다시 요청하면 중복 거래가 생깁니다.
결과 미확정 상태를 별도로 두고 외부 거래 식별자로 조회하거나 정산 작업에서 확인해야 합니다. 사용자 화면에도 단순 실패가 아니라 처리 확인 중이라는 상태를 제공하는 편이 정확합니다.
요청 전체 경로를 연결하는 로그
서비스별 로그가 많아도 동일 거래를 찾을 수 없으면 운영에 도움이 되지 않습니다. 요청 ID, 업무 ID, 멱등성 키, 메시지 ID, 외부 거래 ID를 구조화된 필드로 남기고 서비스 사이에 전달합니다.
모니터링은 CPU 사용량뿐 아니라 업무 결과를 측정해야 합니다.
- 처리 대기 시간과 큐 적체량
- 성공, 실패, 재시도, 미확정 건수
- 중복 요청 차단 횟수
- 상태별 체류 시간
- 외부 연동별 지연과 오류율
- 원장 합계와 파생 잔액의 불일치
출시 전 체크리스트
- 동일 명령을 여러 번 보내도 결과가 한 번만 반영되는가
- 이벤트가 역순으로 도착해도 상태가 후퇴하지 않는가
- 처리 도중 프로세스를 종료해도 작업을 찾고 재개할 수 있는가
- 외부 응답이 늦거나 결과를 알 수 없을 때 별도 상태가 있는가
- 큐와 캐시가 중단돼도 원본 상태를 데이터베이스에서 확인할 수 있는가
- 실패 메시지를 원인 확인 후 개별 재처리할 수 있는가
- 한 업무 ID로 모든 로그와 이벤트를 추적할 수 있는가
실시간 시스템의 안정성은 특정 메시지 브로커나 락 라이브러리만으로 확보되지 않습니다. 중복, 순서, 부분 실패가 반드시 발생한다는 전제에서 상태와 복구 절차를 설계해야 합니다. 관련 구현 범위는 실시간 베팅·입출금 플랫폼 사례에서 볼 수 있습니다.