ONE QUESTION AT A TIME.

천천히 생각하고, 분명하게.

TODAY’S QUESTION938 / 1500
분산 시스템난이도 하5분 답변 목표 · 보강본

메시지에 따라 포인트를 지급한 뒤 ACK 전에 소비자가 죽었습니다. 같은 메시지를 다시 받아도 포인트가 한 번만 지급되도록 어떻게 처리하나요?

잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?

답변과 점검 포인트 펼치기답변 접기
01

구두 답변

정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.

메시지 중복 전달은 소비자가 효과를 커밋한 뒤 ACK나 offset 커밋 전에 죽었을 때 자연스럽게 발생하는 재시도 경로입니다. 따라서 “몇 번 받았는가”와 “포인트 지급이 몇 번 반영됐는가”를 분리하고, 중복을 제거할 안정적인 이벤트 ID와 내구 저장소가 필요합니다.

예를 들어 points 테이블과 inbox(처리한 이벤트 ID를 기록하는 표)인 inbox(event_id, processed_at)를 같은 DB transaction으로 갱신하겠습니다. event_id에 unique 제약을 두고 먼저 처리 기록을 삽입한 뒤, 이미 존재하면 포인트 변경 없이 성공으로 간주합니다. 두 소비자가 동시에 같은 ID를 처리해도 한쪽만 기록 삽입에 성공하도록 합니다. 트랜잭션이 함께 커밋되므로 기록만 남고 효과가 빠지거나, 효과만 남고 기록이 없는 창을 줄일 수 있습니다. ACK는 이 커밋 뒤에 보내야 하며, 먼저 보내면 작업 유실이 가능합니다.

외부 API 호출은 로컬 inbox와 원자적이지 않으므로 외부 멱등 키·상태 조회·대사가 필요합니다. 처리 기록 보관 기간은 늦은 재전달·재생 기간보다 짧아서는 안 되며, payload가 같은 event ID로 바뀌어 오면 조용히 성공시키지 말고 불일치로 격리하겠습니다. 커밋 후 ACK 유실, 동시 중복, 오래된 재생과 외부 호출 실패를 시험해 실제 최종 효과가 한 번인지 확인하겠습니다.

전달 횟수와 반영 횟수를 나눕니다

브로커가 ACK를 받지 못했을 때 다시 보내는 것은 아직 처리가 끝나지 않았을 수 있기 때문입니다. 성공을 확인할 때까지 다시 전달할 수 있는 계약을 최소 한 번 전달(at-least-once delivery)이라고 부릅니다. 이 계약에서 소비자가 반복 실행돼도 포인트 지급 결과는 한 번 실행한 것과 같게 만드는 것이 멱등 소비자(idempotent consumer)입니다. 브로커 전달 자체를 한 번으로 만드는 것과 애플리케이션 반영을 한 번으로 제한하는 것은 다릅니다. 따라서 무엇에 대해 ‘한 번’을 보장하는지 범위를 붙여 설명해야 합니다.

처리한 이벤트를 기록하는 저장소를 인박스(inbox)라고 부릅니다. 여기서는 inbox 기록과 포인트 갱신을 같은 DB 트랜잭션에 넣는 것이 중요합니다. 먼저 기록만 커밋하고 나중에 지급하면 중간 장애 후 재시도가 기록을 보고 지급을 건너뛸 수 있습니다. 반대로 지급부터 별도 커밋하고 기록하면 같은 지급을 반복할 수 있습니다. 같은 트랜잭션에서 둘을 확정하면 커밋 전 실패는 둘 다 남지 않고, 커밋 뒤 실패는 둘 다 남으므로 재전달을 판별할 수 있습니다. 이 보장은 선택한 DB의 내구성·격리와 실제 트랜잭션 범위에 의존합니다.

같은 ID를 동시에 받은 두 소비자가 모두 ‘없다’고 읽지 않도록 DB의 고유 제약이 최종 판정을 해야 합니다. 삽입 성공한 소비자만 포인트를 변경하고, 충돌한 소비자는 이미 커밋된 처리 기록인지 확인한 뒤 ACK합니다. 특정 DB에서는 고유 제약 오류로 트랜잭션 전체가 실패 상태가 되므로 롤백 후 확인하거나 지원되는 충돌 처리 구문을 사용해야 합니다. 실행 중인 다른 트랜잭션 때문에 기다리다 타임아웃됐다는 사실을 ‘이미 성공했다’로 바꾸지는 않겠습니다. 확정 여부를 확인하지 못하면 재시도 가능한 상태로 남겨야 합니다.

중복 판별 키와 상태 변경 규칙은 별개입니다

한 이벤트를 분석 서비스와 보상 서비스가 각각 소비한다면 처리 기록도 소비 목적별로 분리해야 합니다. 같은 테이블을 공유한다면 (consumer_name, event_id)처럼 범위를 정할 수 있습니다. 반대로 한 이벤트에서 동일 보상을 두 번 발행하는 발행자 버그는 서로 다른 event ID를 가질 수 있으므로, 계정·시즌·보상종류 같은 지급 권리의 고유 제약이 더 필요한 경우도 있습니다. 같은 계정이라는 이유로 모든 이벤트를 중복으로 보면 정당한 다음 지급까지 막으므로, 식별자가 무엇을 한 번으로 세는지 먼저 정의하겠습니다.

이벤트 ID가 같아도 payload가 달라졌다면 단순 중복이 아닙니다. 원문 또는 의미를 정규화한 해시를 확인해 불일치를 격리하고, 원래 반영한 내용과 새 내용을 추적할 수 있게 해야 합니다. 기록을 삭제한 뒤 오래된 로그를 재생하면 다시 지급할 수도 있으므로 브로커 보관 기간뿐 아니라 운영자의 수동 재생 기간까지 고려해야 합니다. 장기 지급 원장에 고유 제약을 남길지, 오래된 이벤트는 자동 반영하지 않고 검토할지 명시하겠습니다.

마지막으로 이 구조는 이벤트 사이의 순서를 정하지 않습니다. 서로 다른 두 정상 지급은 모두 통과하므로 포인트 갱신도 balance = balance + amount 같은 원자적 변경이나 적절한 동시성 제어가 필요합니다. 지급과 회수의 순서에 의미가 있다면 버전 검사와 보류 정책이 추가로 필요합니다. 중복 제거를 잘했는데 최종 잔액이 틀릴 수 있는 이유입니다. 테스트에서는 동시에 같은 ID를 처리하는 경우, 서로 다른 ID가 같은 계정을 갱신하는 경우, DB 커밋 직후 종료와 ACK 유실을 각각 재현해 지급 원장과 잔액을 함께 확인하겠습니다.

02

득점 포인트

이해가 드러나는 지점을 확인해 보세요.

  • ACK와 DB 변경의 커밋 사이의 중복 발생 순서를 설명한다.
  • unique inbox와 실제 데이터 변경을 같은 transaction으로 묶는다.
  • 외부 API·payload 변경·보관 기간의 한계를 명시한다.
03

감점 포인트

놓치기 쉬운 조건과 오해를 짚어 보세요.

  • ACK를 먼저 보내면 안전하게 중복이 사라진다고 말한다.
  • 이벤트 ID를 메모리에만 저장해 재시작 뒤 방지 기능이 사라진다.
  • 처리 기록을 재전달 기간과 무관하게 임의로 삭제한다.
04

더 파고들 거리

하나 더 질문하며 이해의 범위를 넓혀 보세요.

득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.

원리부터 공부하고 싶다면

답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.

← 전체 질문 보기