2026년 08월 26일

서버에서 정확히 한 번만 실행하기가 어려운 이유: Exactly Once의 현실

분산 시스템을 다루다 보면 “이 작업이 딱 한 번만 실행되면 되는데”라는 생각을 자주 하게 됩니다. 결제 처리, 이메일 발송, 재고 차감처럼 중복 실행이 곧 장애로 이어지는 작업들이 있기 때문입니다. 그런데 막상 이를 구현하려고 하면 생각보다 훨씬 복잡한 문제와 마주치게 됩니다.

이 글에서는 Exactly Once(정확히 한 번 실행)가 무엇이고, 왜 이것이 분산 환경에서 본질적으로 어려운지, 그리고 실무에서 어떤 방식으로 이 문제에 접근하는지를 정리합니다.

Exactly Once란 무엇인가

메시지 전달이나 작업 실행의 의미론(semantics)은 보통 세 가지로 분류됩니다.

  • At Most Once: 작업이 최대 한 번 실행됩니다. 실패하면 재시도하지 않으므로 누락이 발생할 수 있습니다.
  • At Least Once: 작업이 최소 한 번 실행됩니다. 실패 시 재시도하지만 중복 실행이 발생할 수 있습니다.
  • Exactly Once: 작업이 정확히 한 번만 실행됩니다. 누락도 없고 중복도 없습니다.

세 번째가 가장 이상적으로 보입니다. 그런데 현실에서 Exactly Once를 달성하는 것은 단순한 코드 몇 줄로 해결되지 않습니다.

문제의 본질: 네트워크는 신뢰할 수 없다

분산 시스템의 근본적인 어려움은 네트워크가 완전히 신뢰할 수 없다는 점에서 출발합니다. 요청을 보냈을 때 일어날 수 있는 일은 크게 세 가지입니다.

  1. 요청이 서버에 도달하지 못한 경우
  2. 요청이 서버에 도달해서 처리됐지만 응답이 클라이언트에 돌아오지 못한 경우
  3. 요청이 정상적으로 처리되고 응답도 정상적으로 돌아온 경우

클라이언트 입장에서는 응답을 받지 못한 경우, 1번인지 2번인지 알 방법이 없습니다. 단순히 타임아웃이 발생했다는 사실만 알 뿐입니다. 이때 재시도를 하면 작업이 중복 실행될 수 있고, 재시도를 하지 않으면 작업이 누락될 수 있습니다.

이것이 Exactly Once를 어렵게 만드는 핵심입니다. 요청의 결과를 확신할 수 없는 상황에서 중복도 누락도 없이 처리하는 것은 단일 서버 환경에서도 쉽지 않고, 여러 서버와 네트워크가 얽힌 분산 환경에서는 더욱 복잡해집니다.

재시도와 중복 실행의 딜레마

대부분의 시스템은 신뢰성을 높이기 위해 실패 시 재시도를 구현합니다. 재시도는 At Least Once를 보장하는 방법이지만, 동시에 중복 실행의 가능성을 열어둡니다.

예를 들어 결제 요청을 서버로 보냈는데 응답이 없다고 가정해 봅시다. 클라이언트가 재시도를 하면 결제가 두 번 청구될 수 있습니다. 재시도를 하지 않으면 결제가 실제로 이루어졌는데도 실패로 처리될 수 있습니다.

이 딜레마를 해결하기 위해 자주 사용되는 개념이 멱등성(Idempotency) 입니다.

멱등성으로 중복 실행 방어하기

멱등성이란 같은 작업을 여러 번 실행해도 결과가 달라지지 않는 성질을 말합니다. 예를 들어 특정 값으로 상태를 설정하는 작업은 멱등적입니다. 반면 값을 증가시키는 작업은 기본적으로 멱등적이지 않습니다.

실무에서는 멱등성 키(Idempotency Key) 를 사용해 중복 요청을 방어합니다. 클라이언트가 요청을 보낼 때 고유한 키를 함께 전송하고, 서버는 해당 키를 기준으로 이미 처리된 요청인지 확인합니다. 같은 키로 다시 요청이 들어오면 이전 결과를 그대로 반환하고 실제 작업은 다시 수행하지 않습니다.

이 방식은 중복 실행 방어에 효과적이지만, 그 자체로 Exactly Once를 완전히 보장하지는 않습니다. 멱등성 키를 저장하는 과정 자체에서 장애가 발생하거나, 여러 서버 인스턴스가 동시에 같은 키로 들어온 요청을 처리하려 할 때 경쟁 조건이 생길 수 있기 때문입니다.

분산 트랜잭션의 복잡함

단일 데이터베이스 내에서는 트랜잭션을 통해 원자성을 보장할 수 있습니다. 그런데 마이크로서비스나 이벤트 기반 아키텍처에서는 여러 서비스와 데이터 저장소에 걸쳐 작업이 이루어지기 때문에 단순한 트랜잭션으로 Exactly Once를 달성하기 어렵습니다.

2단계 커밋(2PC)의 한계

분산 트랜잭션을 구현하기 위한 고전적인 방법인 2단계 커밋(Two-Phase Commit, 2PC)은 여러 참여자가 트랜잭션에 동의한 뒤 일괄 커밋하는 방식입니다. 그러나 2PC는 코디네이터 노드가 단일 장애 지점이 될 수 있고, 커밋 직전에 코디네이터가 장애를 겪으면 참여자들이 대기 상태에 빠지는 문제가 있습니다. 또한 성능 오버헤드도 적지 않습니다.

이런 이유로 현대의 분산 시스템에서는 2PC 대신 Saga 패턴 이나 아웃박스 패턴(Transactional Outbox) 같은 대안적인 접근 방식을 선호하는 경향이 있습니다.

아웃박스 패턴

아웃박스 패턴은 데이터베이스 변경과 메시지 발행을 하나의 로컬 트랜잭션으로 묶는 방법입니다. 서비스는 비즈니스 데이터를 변경할 때 동시에 같은 트랜잭션 내에서 ‘발송 예정 메시지’ 테이블(아웃박스)에 메시지를 기록합니다. 이후 별도의 프로세스가 아웃박스를 폴링하거나 변경 데이터 캡처(CDC)를 통해 메시지를 실제로 발행합니다.

이 방식은 데이터베이스 변경과 메시지 발행의 원자성을 로컬 트랜잭션으로 보장할 수 있어 신뢰성이 높습니다. 다만 메시지 브로커 측에서 중복 메시지를 처리할 수 있도록 소비자 쪽에서도 멱등성 처리를 별도로 구현해야 합니다.

메시지 브로커와 Exactly Once

Kafka나 RabbitMQ 같은 메시지 브로커를 사용하는 환경에서도 Exactly Once는 어려운 문제입니다.

Kafka는 버전 0.11부터 프로듀서 레벨의 멱등성과 트랜잭션 API를 통해 Kafka 클러스터 내부에서 Exactly Once Semantics(EOS)를 지원합니다. 같은 Kafka 토픽 간의 읽기-처리-쓰기 파이프라인에서는 이 기능이 효과적으로 동작합니다.

그러나 주의해야 할 점이 있습니다. Kafka의 EOS는 Kafka 내부의 메시지 전달에 대한 보장이지, 그 메시지를 소비한 뒤 외부 데이터베이스나 API를 호출하는 작업 전체에 대한 보장이 아닙니다. 소비자가 메시지를 처리하고 외부 시스템에 작업을 기록하는 과정에서는 여전히 중복이나 누락이 발생할 수 있습니다.

결국 end-to-end Exactly Once는 메시지 브로커의 기능만으로는 완결되지 않으며, 소비자 측의 멱등성 처리가 함께 필요합니다.

실무에서의 현실적인 접근

이론적으로 완벽한 Exactly Once를 모든 상황에서 달성하는 것은 분산 시스템의 특성상 불가능에 가깝습니다. 그래서 실무에서는 다음과 같은 실용적인 전략을 조합해서 사용합니다.

  • 멱등성 설계: 작업 자체를 멱등적으로 설계하여 중복 실행이 발생해도 결과가 같도록 만듭니다.
  • 멱등성 키 활용: 요청마다 고유한 키를 부여하고 서버에서 중복 요청을 감지합니다.
  • 아웃박스 패턴: 데이터 변경과 메시지 발행을 하나의 로컬 트랜잭션으로 처리합니다.
  • 중복 처리 로직: At Least Once를 기본으로 하되, 소비자 측에서 이미 처리된 작업을 감지하고 건너뛰는 로직을 구현합니다.
  • 분산 락: 동시에 같은 작업이 여러 인스턴스에서 실행되지 않도록 Redis나 ZooKeeper 같은 도구로 분산 락을 사용합니다.

완벽한 Exactly Once보다 “중복이 발생하더라도 그 영향을 최소화하고 감지할 수 있는 구조” 를 만드는 것이 현실적인 목표가 되는 경우가 많습니다.

언제 어떤 보장을 선택할까

모든 작업에 Exactly Once가 필요한 것은 아닙니다. 작업의 성격에 따라 적절한 보장 수준을 선택하는 것이 효율적입니다.

작업 유형권장 보장 수준이유
로그 수집, 분석 이벤트At Least Once중복 허용 가능, 누락이 더 문제
알림, 이메일 발송At Least Once + 멱등성 처리중복 발송 방지 필요
결제, 재고 차감Exactly Once에 가까운 처리중복·누락 모두 치명적
캐시 갱신At Most Once실패해도 다음 갱신으로 해결 가능

중요한 금융 처리나 재고 관련 작업이라면 멱등성 키, 아웃박스 패턴, 분산 락을 함께 적용하여 최대한 Exactly Once에 근접한 처리를 목표로 합니다. 반면 로그 수집처럼 약간의 중복이 허용되는 영역에서는 복잡성을 낮추고 At Least Once로 충분합니다.

정리

Exactly Once는 분산 시스템에서 달성하기 어려운 보장입니다. 네트워크의 비신뢰성, 재시도로 인한 중복, 여러 서비스에 걸친 원자성 확보의 어려움이 복합적으로 작용하기 때문입니다.

실무에서는 완벽한 Exactly Once를 단일 메커니즘으로 해결하려 하기보다, 멱등성 설계, 중복 감지, 아웃박스 패턴 등 여러 기법을 조합해서 시스템 전체적으로 중복과 누락의 영향을 최소화하는 방향으로 접근하는 것이 현실적입니다. 어떤 수준의 보장이 필요한지를 작업의 특성에 맞게 판단하고 그에 맞는 설계를 선택하는 것이 핵심입니다.