IT, MIND & CAREER / EDITORIAL DESK

흐름을 읽고, 다음 선택을 설계합니다.

기술의 변화와 사람의 판단, 그리고 오래 가는 커리어를 한 장의 리포트로 정리합니다.

EDITOR'S SELECTION

지금, 먼저 읽을 리포트

변화의 신호를 읽는 네 편의 이야기

THE ARCHIVE

최근 리포트

IT 최신동향 조회 9

이벤트 소비자의 멱등성과 재처리 경계 설계

이벤트 소비자의 멱등성과 재처리 경계 설계
EDITORIAL BRIEF

이 글에서 먼저 가져갈 세 가지

중복 전달을 장애가 아닌 프로토콜의 정상 경로로 취급하는 이벤트 소비자 설계입니다.

  1. 01
    중복은 탐지보다 상태 변경 전에 차단한다.

    식별자 기록과 업무 변경을 분리하면 재시도 순간 구멍이 생긴다. 본문 1·2절

  2. 02
    키의 범위는 브로커가 아니라 업무 효과로 정한다.

    주문 생성·메일 발송·포인트 적립은 서로 다른 멱등 경계를 갖는다. 본문 2·3절

  3. 03
    재처리는 별도 운영 절차로 통제한다.

    과거 이벤트를 다시 읽을 때 순서와 외부 부작용을 명시적으로 관리한다. 본문 4·5절

1. ‘한 번만 처리된다’는 가정이 왜 결제와 재고를 두 번 바꿀까?

브로커가 at-least-once 전달을 제공하면 소비자는 처리 성공을 알리는 확인 응답 전에 프로세스가 죽거나 네트워크가 끊길 수 있다. 소비자는 실제 데이터베이스 변경을 끝냈지만 확인 응답은 브로커에 도착하지 않았을 수 있다. 브로커가 같은 메시지를 다시 보내는 것은 오류가 아니라 유실을 피하기 위한 정상 동작이다. 문제는 두 번째 전달이 첫 번째와 구분되지 않은 채 결제 요청, 재고 차감, 쿠폰 발급, 이메일 발송으로 이어질 때 생긴다.

‘정확히 한 번’이라는 표현은 이 위험을 가리기 쉽다. 특정 브로커와 특정 범위에서는 중복을 줄이는 기능이 있어도, 데이터베이스와 외부 결제사·메일 서비스까지 하나의 원자적 트랜잭션으로 묶기 어렵다. 따라서 프로덕션의 목표는 전달 횟수를 믿는 것이 아니라, 같은 업무 효과가 여러 번 발생하지 않게 만드는 것이다. 소비자가 멱등하면 재시도는 위험한 예외 처리에서 복구 가능한 표준 경로가 된다.

🔍 10초 자가진단: 중복 이벤트가 이미 데이터에 흔적을 남기고 있는가?
  • [ ] 데이터 변경 뒤 ACK 전에 프로세스 종료를 주입해 본 적이 없다.
  • [ ] 이벤트 ID를 기록하지만 업무 상태 변경과 다른 트랜잭션으로 저장한다.
  • [ ] 외부 API 호출에 공급자 측 멱등 키나 조회 절차가 없다.
  • [ ] 재처리 작업이 운영자 로컬 스크립트와 수동 SQL에 의존한다.
👉 2개 이상이라면 소비자는 중복을 견디는 것이 아니라 중복 효과를 만들고 있을 가능성이 크다.

그렇다면 멱등성은 어디에 저장해야 할까? 핵심은 메시지를 ‘봤다’는 기록이 아니라, 그 메시지가 만든 비즈니스 효과와 같은 성공 경계에 기록하는 데 있다.


2. ‘처리됨’ 표와 업무 데이터가 분리되면 어떤 경쟁 조건이 생길까?

가장 단순한 패턴은 이벤트의 고유 ID를 processed_messages에 저장하고, 이미 존재하면 본문을 실행하지 않는 방식이다. 그러나 먼저 ID를 저장하고 뒤에서 주문 상태를 바꾸다가 실패하면, 다음 재시도는 이미 처리된 메시지로 오인되어 업무 효과가 사라진다. 반대로 주문을 먼저 바꾸고 ID 저장 전에 죽으면 재시도에서 같은 효과가 다시 실행된다. 두 기록은 가능한 한 같은 데이터베이스 트랜잭션으로 성공하거나 함께 롤백되어야 한다.

BEGIN;
INSERT INTO processed_messages (consumer_name, event_id, processed_at)
VALUES ('inventory-reserver', :event_id, now());
-- unique constraint 충돌이면 이미 처리된 이벤트로 판단한다.
UPDATE inventory
SET reserved = reserved + :quantity
WHERE sku = :sku AND available - reserved >= :quantity;
COMMIT;

이 예시는 원리를 보여 줄 뿐이다. 재고 예약이 0건 갱신됐을 때는 중복이 아니라 재고 부족일 수 있으므로 결과 상태와 오류 정책을 따로 기록해야 한다. 또한 식별자는 전역 UUID 하나로 충분하지 않을 수 있다. 같은 주문의 ‘결제 승인’과 ‘환불 완료’는 서로 다른 효과이므로 소비자 이름, 이벤트 유형, 업무 엔터티와 버전을 포함한 복합 키가 필요할 수 있다. 반대로 너무 넓은 키는 정상적인 후속 이벤트까지 막는다.

여기서 자주 놓치는 지점이 있다. 데이터베이스 안에서는 원자성을 얻어도, 외부 시스템 호출은 트랜잭션 밖에 남는다. 그래서 아웃박스와 공급자 멱등 키, 결과 조회가 함께 필요하다.


3. ‘DB는 한 번인데 이메일은 두 번’인 외부 부작용을 어떻게 막을까?

외부 결제·문자·메일 API를 호출한 뒤 응답을 받기 전에 타임아웃이 나면 호출이 실패했는지 성공했는지 알 수 없다. 무작정 재시도하면 중복 청구나 중복 알림이 생기고, 재시도하지 않으면 실제 성공을 놓친다. 이 경우 소비자 자신의 테이블만으로는 부족하다. 외부 공급자가 지원한다면 같은 업무 효과마다 안정적인 idempotency key를 보내야 하며, 타임아웃 뒤에는 같은 키로 재시도하거나 공급자의 상태 조회 API로 결과를 확인해야 한다.

이벤트 토큰의 중복 경로와 데이터 저장 경계를 분리해 검토하는 작업 장면

▲ 중복 메시지는 입력의 문제지만, 중복 효과는 저장 경계와 외부 호출 경계가 분리될 때 발생한다.

아웃박스 패턴은 업무 트랜잭션에서 변경 데이터와 ‘발행할 이벤트’를 한 테이블에 함께 저장한다. 별도 릴레이가 아웃박스를 읽어 브로커에 전송하고 전송 상태를 기록한다. 릴레이도 전송 뒤 상태 기록 전에 죽을 수 있으므로 소비자는 여전히 멱등해야 한다. 아웃박스는 중복을 마법처럼 제거하는 도구가 아니라, DB 변경과 이벤트 발행 사이의 유실 창을 관리하는 도구다.

측정 항목기존 위험 신호도입 후 확인 기준함께 볼 트레이드오프
중복 차단 수같은 이벤트가 업무 변경을 반복고유 제약 충돌을 별도 메트릭으로 기록급증하면 ACK·재시도 문제를 조사
처리 지연재시도가 무한 대기열을 만든다이벤트 생성부터 효과 확정까지 측정중복 방지가 병목이 되지 않는지 확인
아웃박스 적체발행 지연이 보이지 않는다가장 오래된 미발행 레코드 알림보존·정리 정책이 필요

정상 경로에서 중복을 막았다면 다음 질문은 과거 이벤트를 다시 처리할 때다. 재처리는 버그 수정의 강력한 수단이지만, 새 코드로 과거의 외부 부작용을 다시 만들 수 있다.


4. ‘재처리’는 왜 배포보다 더 위험한 운영 작업이 될까?

재처리는 단순히 오프셋을 되돌리는 명령이 아니다. 이벤트 스키마가 변했고, 당시에는 없던 검증 규칙이 추가됐으며, 외부 API의 계약도 달라졌을 수 있다. 특히 현재 소비자 그룹을 그대로 되감으면 실시간 트래픽과 과거 트래픽이 섞여 순서 가정이 깨질 수 있다. 안전한 재처리는 별도 소비자 그룹, 명시적 시간 범위, dry-run 결과, 처리량 제한, 중단 스위치를 갖는다.

운영자는 먼저 재처리의 목적을 분류해야 한다. 분석용 투영을 다시 만들려는 것인지, 업무 데이터를 복구하려는 것인지, 외부 통지를 보완하려는 것인지에 따라 허용되는 부작용이 다르다. 분석 투영은 새 테이블에 재구성한 뒤 원자적으로 교체할 수 있다. 반면 결제·알림처럼 외부 효과가 있는 경로는 원본 이벤트를 그대로 재실행하기보다, 누락된 효과만 담은 보정 이벤트를 새로 만들고 승인 절차를 두는 편이 안전하다.

순서 보장과 멱등성을 혼동하면 무엇을 놓칠까?

동일 키의 이벤트 순서를 보장하는 파티션은 유용하지만, 멱등성을 대신하지 않는다. 소비자 재시작, 재균형, 외부 시스템의 비동기 완료는 이미 처리한 이벤트의 재전달을 만들 수 있다. 반대로 멱등하다고 해서 순서 문제가 사라지는 것도 아니다. 배송 시작보다 주문 취소가 먼저 도착하면 각 이벤트를 한 번씩만 적용해도 잘못된 상태가 된다. 업무 엔터티에 버전이나 상태 전이 규칙을 두고, 기대한 이전 상태가 아닐 때는 무조건 덮어쓰지 말고 보류·조사·보정 경로로 보내야 한다.

보존 기간도 설계의 일부다. 중복을 막기 위한 처리 기록을 이벤트 보존 기간보다 먼저 지우면, 오래된 재전달이 새 이벤트처럼 통과할 수 있다. 반대로 모든 식별자를 무기한 보관하면 저장 비용과 개인정보 보존 문제가 커진다. 따라서 이벤트의 최대 재전달 창, 재처리 정책, 법적 보존 요구를 기준으로 처리 기록 TTL을 정하고, TTL 변경은 브로커 설정과 함께 검토해야 한다. 이 결정은 데이터베이스 용량 조정이 아니라 정확성 계약의 변경이다.

장애 시험에서는 정상 재시도만 보지 않는다. DB 커밋 직후 프로세스 종료, 외부 API 성공 뒤 응답 유실, 같은 이벤트의 동시 전달, DLQ에서의 부분 재투입, 소비자 코드 버전 교체를 각각 주입한다. 이때 확인할 값은 단순 오류율이 아니다. 동일한 주문이나 계정의 최종 상태가 한 번의 처리와 동일한지, 중복 차단이 어느 경로에서 일어났는지, 운영자가 보정 결정을 내릴 충분한 증거를 갖는지 확인해야 한다. 이 테스트 결과가 있어야 멱등성은 문서상의 약속이 아니라 배포 가능한 속성이 된다.

관측성 역시 입력과 효과를 연결해야 한다. 이벤트 ID, 소비자 버전, 엔터티 키, 처리 결과, 외부 요청 키를 같은 추적 문맥에 남기면 운영자는 중복 차단이 실패인지 정상 재전달인지 빠르게 구분할 수 있다. 단, 원문 페이로드를 무분별하게 로그에 남기면 개인정보와 비용 문제가 생기므로 필요한 식별자와 해시만 보존하는 정책을 함께 둔다.

가장 먼저 자동화할 것은 재시도 횟수가 아니라, 불확실한 결과를 사람이 판단할 수 있게 하는 증거 수집이다.

이 원칙은 트래픽 규모와 무관하게 모든 이벤트 경로에 적용된다.

기록부터 시작하자.

오늘부터.

시니어 아키텍트의 실전 방어 수칙: DLQ를 삭제 대기실로 쓰지 말 것
Dead Letter Queue는 실패 메시지를 숨기는 곳이 아니라, 실패 이유·시도 횟수·원본 위치·재처리 가능성을 보존하는 조사 대기열이다. 원인을 고치지 않은 채 자동 재투입하면 장애를 다시 증폭한다.
💡 실무 원칙: DLQ 항목은 재시도·보정·폐기 중 하나의 명시적 결론과 근거를 남기고, 보존 기간 전에 알림을 만든다.

5. ‘2주 안에’ 멱등 소비자를 검증하는 로드맵

단계핵심 실행 과제산출물 및 검증 지표
Phase 1중복 효과의 비용이 큰 소비자 하나를 고르고 이벤트 ID·업무 효과·외부 호출을 흐름도로 기록한다.멱등 키의 범위와 실패 시나리오 목록
Phase 2고유 제약과 업무 변경을 같은 트랜잭션에 넣고, ACK 전 강제 종료·타임아웃·중복 전달을 테스트한다.같은 이벤트 N회 전달 뒤 효과 1회 검증
Phase 3별도 소비자 그룹으로 제한된 재처리를 연습하고 DLQ 관측·중단·보정 절차를 런북으로 만든다.재처리 범위·처리량·잔여 항목이 보이는 운영 대시보드

멱등성은 이벤트 기반 시스템의 장식이 아니다. 네트워크와 프로세스 실패를 전제로 한 상태 변경의 계약이다. 브로커의 전달 보장을 과신하기보다, 업무 효과가 같은 입력에 대해 한 번의 상태로 수렴하도록 만들면 재시도와 복구는 시스템을 흔드는 예외가 아니라 운영 가능한 도구가 된다.

좋은 소비자는 메시지를 한 번만 받기를 바라지 않는다. 같은 메시지를 다시 받아도 고객의 상태를 다시 망가뜨리지 않도록 설계한다.

공식 참고 자료

원문 참고 자료

이 글의 사실 확인과 추가 읽기를 위한 원문입니다. Microservices.io

테크 아키텍처 데스크
IT & Mind Trends 에디토리얼 팀 — 클라우드 분산 아키텍처 및 행동 과학 트렌드를 연구하고 실무 트레이드오프를 검증하여 전달합니다.
이전 글
다음 글