메시지가 최소 한 번은 전달된다는 보장. 유실은 없지만 중복이 생길 수 있다. 대부분의 브로커가 기본으로 제공하는 수준이다.
왜 중복이 생기는가 — 확인 시점의 틈
- ① 브로커 → 소비자 : 메시지 전달
- ② 소비자 : 처리 완료 (DB에 저장됨)
- ③ 소비자 → 브로커 : ack 전송
②와 ③ 사이에 소비자가 죽으면 브로커는 ack를 못 받았으니 "아직 처리 안 됐다"고 판단해 다시 보낸다. 그런데 실제로는 이미 처리됐다 → 중복.
순서를 바꾸면 어떻게 될까?
- ① 전달 → ② ack 먼저 → ③ 처리 이러면 ②와 ③ 사이에 죽었을 때 메시지가 사라진다(At-Most-Once). 유실과 중복 중 하나는 반드시 감수해야 한다 — 이것이 분산 시스템의 근본 제약이다.
세 가지 시맨틱
| 유실 | 중복 | 언제 | |
|---|---|---|---|
| At-Most-Once | 있음 | 없음 | 유실해도 되는 로그·메트릭 |
| At-Least-Once | 없음 | 있음 | 대부분의 업무 처리 |
| Exactly-Once | 없음 | 없음 | 조건부로만 성립 |
그래서 소비자를 멱등하게 만든다
방법 ① 처리 이력 테이블
INSERT INTO processed_message (message_id) VALUES (?);
-- 유니크 제약 위반 → 이미 처리한 것 → 건너뛴다
업무 처리와 같은 트랜잭션에 넣어야 한다. 따로 하면 그 사이에 죽었을 때 또 어긋난다.
방법 ② 연산 자체를 멱등하게
- 잔액 = 1000 — ← 멱등 (몇 번 해도 같음)
- 잔액 = 잔액 + 100 — ← 비멱등 (할 때마다 늘어남) 가능하면 절대값 설정으로 설계한다.
방법 ③ 조건부 갱신
UPDATE orders SET status = 'PAID'
WHERE id = ? AND status = 'PENDING'; -- 이미 PAID면 0행 → 무시
상태 전이를 조건으로 걸면 두 번째 실행이 자동으로 무효가 된다.
면접 답변 골격
"브로커는 at-least-once를 주므로 중복은 전제하고 설계합니다. 메시지에 고유 ID를 두고 처리 이력을 업무 트랜잭션과 함께 기록하거나, 상태 전이를 조건부 UPDATE로 만들어 두 번째 처리가 무해하게 합니다."