메시지 전달 보장 #At-most-once, At-least-once, Exactly-once와 순서 보장

메시지 전달 보장이란?

메시지 전달 보장(Message Delivery Guarantees)은 분산 시스템에서 메시지가 생산자(Producer)로부터 소비자(Consumer)에게 전달되고 처리되는 과정에서 발생할 수 있는 유실, 중복, 순서 변경 등의 문제를 어떻게 다룰 것인지 정의하는 개념이다.

메시지 브로커를 사용하는 시스템에서는 메시지를 전송했다고 해서 반드시 한 번만 처리되는 것은 아니다. 네트워크 단절, 서버 장애, 타임아웃, 소비자 재시작, ACK(Acknowledgment) 유실 등의 상황으로 인해 메시지 처리 여부를 판단하기 어려울 수 있다.

예를 들어 게임 서버에서 플레이어에게 포인트를 지급하는 메시지를 처리한다고 가정하자.

  • 소비자가 메시지를 수신한다.
  • 데이터베이스에 포인트를 지급한다.
  • 처리 완료 ACK를 메시지 브로커에 전달한다.
  • ACK가 유실된다.
  • 브로커는 메시지가 처리되지 않았다고 판단하고 다시 전달한다.

소비자가 중복 처리를 방지하지 않으면 플레이어에게 동일한 보상이 여러 번 지급될 수 있다.

따라서 메시지 전달 보장은 단순히 메시지를 전송하는 기술이 아니다. 장애가 발생하더라도 데이터의 정합성과 비즈니스 규칙을 유지하기 위한 설계 문제다.

대표적인 전달 보장 방식은 다음과 같다.

  • At-most-once: 최대 한 번 전달한다.
  • At-least-once: 최소 한 번 전달을 시도하며, 중복될 수 있다.
  • Exactly-once: 정의된 처리 범위에서 효과가 정확히 한 번 발생하도록 보장한다.

At-most-once

At-most-once는 메시지를 최대 한 번만 전달하거나 처리하도록 하는 방식이다.

메시지가 유실되더라도 재전송하지 않는다. 중복 가능성을 줄이는 대신 전달 실패 가능성을 허용한다.

동작 방식

메시지를 재전송하지 않거나, 처리 성공 여부를 확인하기 전에 메시지를 더 이상 복구할 수 없도록 소비 처리하는 방식으로 구현할 수 있다.

소비자가 메시지를 받기 전에 메시지가 유실되거나, 소비자가 처리 도중 종료되더라도 메시지를 다시 전달하지 않는 구조다.

장점

  • 중복 처리에 따른 비용이 적다.
  • 재전송과 복구 로직이 단순하다.
  • 지연 시간을 낮추는 데 유리할 수 있다.
  • 일부 이벤트의 유실을 허용할 수 있는 시스템에 적합하다.

단점

  • 메시지 유실을 복구하기 어렵다.
  • 장애 발생 시 일부 작업이 영구적으로 누락될 수 있다.
  • 중요한 비즈니스 데이터의 정합성을 유지하기 어렵다.

활용 사례

  • 게임 내 위치 및 회전 상태의 빈번한 갱신
  • 실시간 센서 데이터
  • 일부 텔레메트리 및 디버깅 로그
  • 이전 값이 최신 값으로 대체되는 상태 업데이트

플레이어의 위치가 지속적으로 전송되는 상황에서는 과거 위치 메시지가 유실되더라도 다음 위치 메시지로 상태를 갱신할 수 있다.

반면 아이템 구매나 포인트 지급처럼 반드시 처리되어야 하는 작업에는 적합하지 않다.

At-most-once는 시스템 전체에서 메시지 유실이 없다는 의미가 아니다. 중복 전달을 피하는 대신 유실을 허용하는 전달 정책이다.

At-least-once

At-least-once는 메시지가 최소 한 번 처리되도록 재전송과 복구를 활용하는 방식이다.

소비자의 처리 완료가 확인되지 않으면 메시지를 다시 전달한다. 메시지 유실 가능성을 줄일 수 있지만, 이미 처리한 메시지가 다시 전달될 수 있다.

동작 방식

일반적인 처리 흐름은 다음과 같다.

  • 생산자가 메시지를 브로커에 발행한다.
  • 브로커가 메시지를 저장하거나 전달한다.
  • 소비자가 메시지를 수신한다.
  • 소비자가 작업을 처리한다.
  • 처리가 성공하면 ACK를 전송한다.
  • ACK가 확인되지 않으면 브로커가 메시지를 재전달한다.

핵심은 메시지 처리 완료를 확인할 수 없을 때 재시도한다는 것이다.

ACK가 유실되면 실제 작업이 성공했더라도 브로커는 이를 알 수 없다. 따라서 동일한 메시지가 다시 전달될 수 있다.

장점

  • 일시적인 장애에서 메시지를 복구할 수 있다.
  • 중요한 작업의 누락 가능성을 줄일 수 있다.
  • 소비자가 재시작되더라도 미완료 작업을 다시 처리할 수 있다.

단점

  • 동일한 메시지가 여러 번 전달될 수 있다.
  • 중복 처리 방지 로직이 필요하다.
  • 재시도 폭주나 장애 복구 과정에서 부하가 증가할 수 있다.
  • 외부 시스템까지 포함하면 전체 작업의 성공 여부를 판단하기 어려울 수 있다.

활용 사례

  • 게임 보상 지급
  • 결제 및 주문 처리
  • 이메일과 알림 발송
  • 비동기 데이터 처리
  • 데이터베이스 동기화 작업

이러한 작업에서는 메시지의 재전달을 허용하되, 동일한 비즈니스 작업이 여러 번 실행되지 않도록 설계하는 것이 중요하다.

Exactly-once 처리의 한계

Exactly-once는 메시지 또는 작업이 정확히 한 번 처리되도록 보장하는 개념이다. 그러나 이를 시스템 전체에 적용하는 것은 어렵다.

가장 먼저 구분해야 할 것은 메시지가 정확히 한 번 전달되는 것과 비즈니스 효과가 정확히 한 번 발생하는 것은 서로 다른 문제라는 점이다.

메시지 전달과 실제 작업의 차이

메시지 브로커가 동일한 메시지를 한 번만 전달하도록 구성했다고 가정하자.

소비자가 메시지를 처리하는 도중 데이터베이스에 포인트를 지급했지만, 처리 결과를 기록하기 전에 서버가 종료되었다면 어떻게 될까?

재시작 후 브로커가 메시지를 재전달하면 포인트가 중복 지급될 수 있다. 반대로 재전달을 막으면 포인트 지급이 완료되지 않았을 가능성이 있다.

이 문제는 메시지 전달 횟수만 제어해서 해결할 수 없다. 메시지 브로커, 소비자, 데이터베이스 사이에서 작업의 처리 여부와 실제 효과를 일관되게 관리해야 한다.

Exactly-once의 적용 범위

Exactly-once는 특정 시스템과 처리 범위 안에서 구현할 수 있다.

일부 메시지 브로커는 자체 트랜잭션과 메시지 처리 기능을 결합하여 중복 처리나 재처리 문제를 제한한다. 그러나 그 보장이 외부 데이터베이스의 변경이나 결제 API 호출까지 자동으로 확장되는 것은 아니다.

따라서 Exactly-once를 검토할 때는 다음 사항을 명확하게 정의해야 한다.

  • 어느 구간에서 중복을 방지하는가?
  • 메시지 소비와 결과 저장이 하나의 트랜잭션으로 묶이는가?
  • 서버 장애 이후에도 처리 결과를 복구할 수 있는가?
  • 외부 API 호출까지 중복 방지가 가능한가?
  • 보장되지 않는 장애 상황은 무엇인가?

실무적인 접근

대부분의 비즈니스 시스템에서는 At-least-once 전달을 사용하고, 소비자 측에서 멱등성(Idempotency)을 보장하는 방식이 실용적이다.

동일한 보상 메시지가 여러 번 전달되더라도 하나의 보상 ID에 대해 지급이 한 번만 발생하도록 설계하는 것이다.

이를 위해 다음 기술을 활용할 수 있다.

  • 메시지 고유 ID
  • 멱등성 키(Idempotency Key)
  • 데이터베이스 유니크 제약 조건
  • 트랜잭션을 통한 처리 기록과 비즈니스 데이터의 원자적 변경
  • Outbox 및 Inbox 패턴
  • 재시도 및 실패 메시지 관리

Exactly-once는 모든 시스템에서 달성해야 하는 절대적인 목표가 아니다. 어떤 범위에서 어떤 효과를 정확히 한 번만 발생시켜야 하는지 정의한 뒤 구현해야 하는 보장 수준이다.

메시지 순서 보장

메시지 전달 보장과 메시지 순서 보장은 별개의 문제다.

모든 메시지가 유실 없이 전달되더라도 소비자가 처리하는 순서는 생산자가 발행한 순서와 다를 수 있다.

순서가 변경되는 원인

다음과 같은 메시지가 순서대로 발행되었다고 가정하자.

  • 메시지 A: 플레이어 레벨을 변경한다.
  • 메시지 B: 플레이어 레벨을 다시 변경한다.
  • 메시지 C: 플레이어 레벨을 최종 변경한다.

여러 소비자가 메시지를 병렬로 처리하거나 재시도 과정에서 메시지가 다시 전달되면 C가 먼저 완료되고 A가 나중에 완료될 수 있다.

그 결과 최신 상태가 과거 상태로 덮어써질 수 있다.

순서 보장 방법

단일 소비자 사용

하나의 소비자가 메시지를 순차적으로 처리하면 동시 처리로 인한 순서 변경을 줄일 수 있다.

다만 처리량이 제한되고, 재시도나 여러 생산자가 관여하는 상황에서는 별도의 순서 관리가 필요할 수 있다.

파티션 키 사용

Kafka와 같은 파티션 기반 브로커에서는 동일한 키를 가진 메시지를 같은 파티션으로 보내고, 파티션 내부의 순서를 활용할 수 있다.

예를 들어 playerId를 키로 사용하면 동일 플레이어의 이벤트를 같은 파티션에 배치하여 순서를 관리할 수 있다.

단, 여러 파티션에 걸친 전체 순서가 자동으로 보장되는 것은 아니다.

시퀀스 번호 사용

각 메시지에 증가하는 시퀀스 번호를 부여한다.

{
  "playerId": "player-abc",
  "sequence": "seq-current",
  "type": "PLAYER_LEVEL_CHANGED",
  "level": "level-current"
}

실제 구현에서는 시퀀스 번호와 레벨을 정수로 관리한다. 소비자는 현재 처리한 시퀀스 번호와 비교하여 오래된 메시지를 무시하거나 누락된 메시지를 확인할 수 있다.

버전 기반 상태 갱신

데이터베이스에 상태 버전을 기록하고 더 최신 버전의 메시지만 반영한다.

UPDATE player_state
SET level = :new_level,
    version = :new_version
WHERE player_id = :player_id
  AND version < :new_version;

이 방식은 오래된 상태가 최신 상태를 덮어쓰는 문제를 방지하는 데 유용하다. 실제 구현은 데이터베이스 종류와 동시성 제어 방식에 맞게 설계해야 한다.

순서 보장의 비용

순서를 엄격하게 보장할수록 병렬 처리의 자유도가 감소한다. 특정 메시지가 실패하거나 지연되면 후속 메시지도 기다려야 할 수 있다.

따라서 모든 메시지에 전역 순서를 강제하기보다는 순서가 중요한 범위를 식별하고 해당 범위 안에서만 순서를 보장하는 방식이 일반적으로 효율적이다.

플레이어별 인벤토리 변경은 플레이어 단위의 순서가 중요하지만, 서로 다른 플레이어의 채팅 메시지까지 전역 순서로 처리할 필요는 없을 수 있다.

중복 메시지 처리

중복 메시지는 At-least-once 전달을 사용하는 시스템에서 반드시 고려해야 하는 문제다.

중복 메시지 자체를 완전히 제거하려 하기보다는 중복 메시지가 도착해도 비즈니스 결과가 달라지지 않도록 설계하는 것이 중요하다.

메시지 고유 ID

생산자는 각 논리적 작업에 고유한 ID를 부여한다.

{
  "messageId": "reward-event-abc",
  "playerId": "player-abc",
  "rewardType": "DAILY_LOGIN",
  "amount": "reward-amount"
}

실제 구현에서는 amount를 정수형 값으로 저장한다. 소비자는 messageId를 기준으로 이미 처리한 메시지인지 확인할 수 있다.

메시지 ID를 재시도할 때마다 새로 생성하면 동일한 작업을 구분할 수 없다. 따라서 같은 논리적 작업의 재시도에는 같은 식별자를 사용해야 한다.

처리 기록과 비즈니스 변경의 원자성

다음과 같은 구현은 안전하지 않다.

  • 메시지가 처리되었는지 확인한다.
  • 포인트를 지급한다.
  • 처리 완료 기록을 저장한다.

포인트를 지급한 뒤 처리 완료 기록을 저장하기 전에 서버가 종료되면 재전달된 메시지가 포인트를 다시 지급할 수 있다.

반대로 처리 완료 기록을 먼저 저장하고 포인트 지급 전에 서버가 종료되면 실제 보상은 지급되지 않았는데 재처리가 차단될 수 있다.

관계형 데이터베이스에서는 처리 기록과 비즈니스 변경을 동일한 트랜잭션으로 묶을 수 있다.

BEGIN;

INSERT INTO processed_messages (message_id)
VALUES (:message_id);

UPDATE player_wallet
SET points = points + :reward_amount
WHERE player_id = :player_id;

COMMIT;

processed_messages.message_id에는 유니크 제약 조건이 필요하다. 이미 처리한 메시지가 재전달되면 INSERT가 유니크 제약 조건 위반으로 실패하므로 소비자는 이를 중복으로 판단하고 포인트 지급을 건너뛸 수 있다.

실제 구현에서는 유니크 제약 조건 위반과 다른 데이터베이스 오류를 구분해야 한다. 또한 트랜잭션이 실패하면 처리 기록과 포인트 변경이 함께 롤백되어야 한다.

멱등성(Idempotency)

멱등성이란 동일한 작업을 여러 번 실행하더라도 최종 결과가 한 번 실행한 것과 같도록 만드는 성질이다.

예를 들어 다음 작업은 멱등적이다.

UPDATE player_state
SET level = :target_level
WHERE player_id = :player_id;

같은 명령을 반복 실행해도 지정된 레벨이 유지된다.

반면 다음 작업은 멱등적이지 않다.

UPDATE player_wallet
SET points = points + :reward_amount
WHERE player_id = :player_id;

반복 실행할 때마다 포인트가 증가한다.

따라서 증가, 차감, 보상 지급처럼 반복 실행이 결과를 변경하는 작업에는 멱등성 키와 처리 기록을 활용해야 한다.

단순히 처리 기록을 확인하는 것만으로는 충분하지 않다. 동일한 메시지가 동시에 도착할 수 있으므로 데이터베이스 제약 조건과 트랜잭션을 사용해 경쟁 조건까지 방지해야 한다.

외부 시스템과의 중복 처리

메시지 소비자가 외부 결제 API나 알림 서비스를 호출한다면 데이터베이스 트랜잭션만으로 전체 작업의 원자성을 보장할 수 없다.

외부 API에서 결제가 성공했지만 응답이 유실되면 소비자는 결제 성공 여부를 알 수 없다. 동일한 요청을 다시 실행하면 중복 결제가 발생할 수 있다.

가능하다면 외부 시스템이 지원하는 멱등성 키를 사용하고, 요청 결과 조회 및 재조정 절차를 마련해야 한다.

외부 시스템이 멱등성이나 결과 조회를 지원하지 않는다면 중복 효과를 완전히 방지하기 어려울 수 있다.

메시지 전달 보장 방식 비교

구분At-most-onceAt-least-onceExactly-once
전달 정책최대 한 번최소 한 번 전달을 시도정의된 범위에서 한 번
메시지 유실발생 가능적절한 저장 및 재시도 조건에서 줄일 수 있음적용 범위와 구현에 따라 다름
중복 전달일반적으로 방지발생 가능정의된 범위에서 방지
구현 복잡도낮은 편중간높은 편
핵심 고려 사항유실 허용 여부멱등성 및 재시도트랜잭션 경계와 보장 범위

Exactly-once는 시스템 전체의 모든 부작용이 자동으로 한 번만 발생한다는 뜻이 아니다. 브로커가 제공하는 보장과 데이터베이스 및 외부 API의 보장은 별도로 확인해야 한다.

실시간 게임 서버에서의 적용

게임 서버에서는 데이터의 성격에 따라 서로 다른 전달 정책을 적용해야 한다.

데이터 유형권장 접근 방식이유
플레이어 위치 갱신유실 허용 또는 최신 값 우선오래된 위치보다 최신 위치가 중요
채팅 메시지재전달 및 중복 표시 방지메시지 누락과 중복 표시를 줄여야 함
매칭 상태 변경멱등성 및 상태 전이 검증중복 참가와 잘못된 상태 변경 방지
아이템 구매트랜잭션 및 멱등성중복 구매와 재화 손실 방지
게임 보상 지급재전달 및 중복 방지재시도는 허용하되 지급 효과는 한 번만 발생
랭킹 갱신이벤트 순서 및 버전 관리오래된 점수가 최신 점수를 덮어쓰는 문제 방지

특히 게임 경제 시스템에서는 메시지 전달 자체보다 실제 보상 지급의 정합성이 중요하다.

메시지 브로커가 재전달을 지원하더라도 포인트 차감, 아이템 지급, 거래 기록 저장이 서로 다른 트랜잭션으로 분리되어 있다면 일부만 반영되는 문제가 발생할 수 있다.

이러한 경우에는 데이터베이스 트랜잭션, 유니크 제약 조건, 작업 상태 관리 및 복구 절차를 함께 설계해야 한다.

실무 설계 원칙

메시지 전달 보장을 설계할 때는 다음 원칙을 적용할 수 있다.

  • 메시지 유실 허용 여부를 먼저 판단한다. 모든 이벤트에 동일한 전달 정책을 적용할 필요는 없다.
  • 재시도 가능한 작업은 멱등적으로 설계한다. 중복 효과를 방지하지 않으면 재시도는 데이터 오류의 원인이 된다.
  • 처리 기록과 비즈니스 변경을 원자적으로 관리한다. 데이터베이스 트랜잭션과 유니크 제약 조건을 활용한다.
  • 순서 보장이 필요한 범위를 제한한다. 사용자, 게임 세션, 주문 등 도메인별 키를 기준으로 순서를 관리한다.
  • 실패 메시지의 복구 경로를 마련한다. 재시도 횟수 제한, 지수 백오프, Dead Letter Queue(DLQ), 운영자 재처리 기능 등을 고려한다.
  • 보장 범위를 문서화한다. 브로커의 전달 보장, 소비자 처리 보장, 데이터베이스 변경 및 외부 API 효과를 구분한다.
  • 장애 상황을 테스트한다. 메시지 수신 직후, 데이터베이스 커밋 직후, ACK 전송 직전 등 여러 시점에서 프로세스가 종료되는 상황을 검증한다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다