Joe Armstrong 저서에서 인용
"메시지는 유실될 수 있다. Erlang은 실패 가능성을 숨기지 않는다.
그러나 그것은 신뢰할 수 있는 실패이다. 왜냐하면 숨기지 않기 때문이다."
— Joe Armstrong, Programming Erlang
💡 "거짓없는(Unreliable but Honest)" 철학
"Erlang은 메시지를 신중하게 보내며, 실패할 가능성을 숨기지 않는다.
메시지가 유실될 수 있음을 정직하게 인정하고,
시스템 설계자가 그에 맞게 복원력을 고려하도록 한다."
언랭에서 출발한 위와같은 개념은 오늘날의 메시지 전송 수준 개념에 영향을 주며 확장 되었습니다.
언랭의 거짓없는 메시지 전송을 살펴보고 메시지 전송 수준에 대한 개념과 함께
Kafka/Akka에서 이용할수 있는 장치도 함께 살펴보겠습니다.
🧩 Erlang의 메시지 전송: "Unreliable but Honest"
🟡 핵심 정의
Erlang은 메시지를 “거짓없이(honestly)” 보낸다고 보장하되, “항상 도착하는 것”은 보장하지 않음.
이는 즉, "보낸다고 했으면 진짜 보냈다"는 것을 보장하지만, 네트워크나 프로세스 문제로 인해 수신자가 받지 못할 수 있음.
✅ 의미 정리
| 요소 | 설명 |
|---|---|
| 보낸다는 약속 | Sender ! Msg를 실행하면 Erlang은 메시지를 보내려고 시도했음을 보장 |
| 수신자 보장 없음 | 수신자가 다운되었거나, 네트워크 분리, 큐가 손상된 경우 메시지는 유실될 수 있음 |
| 확인(ACK) | 기본적으로 ACK가 없음. 메시지 수신 여부는 확인할 방법이 없음 |
| 신뢰성 책임 | 개발자가 직접 retry, ACK, 중복제거를 구현해야 함 |
| 기본 전송 모델 | At-Most-Once에 가까운 구조 (한 번 보내고 끝) |
🧠 Erlang 프로그래머의 현실 대응법
| 상황 | 대응 방식 |
|---|---|
| 메시지 유실 방지 | gen_server:call 처럼 동기 호출 사용 (내부 ACK) |
| 중복 방지 | 메시지에 UUID 붙이고 중복 수신 필터링 |
| 상태 복구 | supervisor + process monitoring 활용 |
| 메시지 순서 보장 | 기본적으로 없음 → 순서를 명시적으로 관리해야 함 |
예시요약
% 메시지 보내기 (ACK 없음, 실패 감지 불가)
ReceiverPid ! {from, self(), hello}.
% 신뢰 보장 원한다면 call 사용
Response = gen_server:call(MyServer, {do_work, Payload}).
🎯 요약 비교
| 항목 | Erlang 기본 메시징 | Kafka / Akka 등 현대 메시지 플랫폼 |
|---|---|---|
| 전달 보장 | ❌ (at-most-once 유사) | ✅ (옵션에 따라 보장) |
| 중복 방지 | ❌ 직접 구현 | ✅ 트랜잭션 / idempotency 지원 |
| 유실 허용 | ✅ (허용됨, 정직함) | ❌ 최소 at-least-once 보장됨 |
| ACK / Retry | ❌ 기본 미제공 | ✅ 명시적 제공 |
🧠 핵심 요약
단계 | 시스템 | 메시지 전달 수준 | 특징 |
|---|---|---|---|
① | Erlang (1986) | 기본: | 메시지 유실 가능, ack 없음, 재전송은 직접 구현 |
② | Akka (2010~) | 기본: |
|
③ | Akka Persistence |
| 이벤트 소싱, 재처리 시 동일 효과 보장 |
④ | Kafka (v0.11~) | 명시적 지원: | 트랜잭션 API + Idempotent Producer 제공 |
Message Delivery Semantics
메시지 보장수준은 전략에따라 채택할수 있으며 모든기능에 보장수준을 최고수준으로 하는 오버엔지니어링을 하기보다는
인프라및 설계비용을 고려 전략적 채택이 중요할수 있습니다.
✅ 1. At Most Once (최대 1번 전송)
의미: 메시지가 0번 또는 1번 전송됨.
메시지 손실이 발생할 수 있지만 중복은 없음.보장 수준: 전송 실패 시 재시도 없음
사용 예시: 로그 데이터, 비중요 알림
장점: 빠르고 가볍다. 인프라 부담 거의 없음
비용 관점:
최저 비용
메시지 저장 없음, 재전송 로직 없음
브로커도 상태를 유지할 필요 거의 없음
✅ 2. At Least Once (적어도 1번 전송)
의미: 메시지가 1번 이상 전송될 수 있음.
중복 가능성이 있으나 손실은 없음보장 수준: 수신 확인이 없으면 재전송
사용 예시: 결제 요청, 주문 이벤트
장점: 신뢰성 확보 (중복 제거는 소비자가 책임짐)
비용 관점:
중간 수준 비용
메시지 재시도를 위한 큐 유지, 상태 저장 필요
클라이언트 or 브로커의 중복 제어 로직 필요
디스크 I/O 비용, 네트워크 재전송 비용 발생
✅ 3. Exactly Once (정확히 1번 전송)
의미: 메시지가 정확히 1번만 전달됨.
중복도 손실도 없음보장 수준: 중복 제거 + 재전송 보장
사용 예시: 금융거래, 은행 이체, 재고 차감
장점: 최상의 데이터 무결성
비용 관점:
가장 높은 비용
트랜잭션, idempotent 처리를 위한 상태 관리,
브로커 + 소비자 모두 상태 추적Kafka의 경우, idempotent producer + transactional consumer 필요
성능 하락, 지연 발생 가능성 증가
💡 선택 기준 요약표
전송 수준 | 중복 | 손실 | 처리 비용 | 사용 예시 |
|---|---|---|---|---|
At Most Once | ❌ | ✅ | 💲 (낮음) | 로그, 알림 |
At Least Once | ✅ | ❌ | 💲💲 (중간) | 주문, 결제 |
Exactly Once | ❌ | ❌ | 💲💲💲 (높음) | 금융, 정산 |
🔍 인프라 비용 고려 팁
로그나 비동기 알림은
At Most Once로 충분 → 비용 절감결제, 주문처리는
At Least Once+ 중복제거 로직 추가로 타협 가능회계, 정산은
Exactly Once필수지만, 서비스 경계를 좁히면 필요한 범위를 최소화할 수 있음
Kafka를 활용한 Exactly Once전략
Kafka를 단순하게 이용한다고 ExactlyOnce의 목표가 달성되는것이 아니며 성능을 위해 At MostOnce전략을 채택할수도 있으며
Exactly달성을 위해서는 Kafka의 특성을 이해하고 설계자가 활용해야하며 Kafka는 메시지 보장수준에따른 다양한 전략적 기능을 제공해줍니다.
Kafka에서 **Exactly Once Semantics (EOS)**를 구현하기 위해서는 일반적인 at-least-once 또는 at-most-once보다 복잡한 설정과 추가 기술이 필요합니다. 다음은 Kafka에서 Exactly Once 메시지 전송을 보장하기 위해 반드시 이해하고 활용해야 하는 기술 요소들을 요약한 것입니다:
✅ 1. Transactional Producer
기능: 하나 이상의 메시지를 하나의 트랜잭션으로 묶어서 처리.
설정:
enable.idempotence=true(필수)transactional.id지정 (트랜잭션을 식별하는 고유 ID)
역할:
중복 전송 방지 (idempotent producer)
브로커에 정확히 한 번만 기록되도록 보장
✅ 2. Idempotent Producer
기능: 동일한 메시지를 여러 번 전송해도 중복 저장되지 않도록 보장
설정:
Kafka 0.11+ 이상 기본 지원
enable.idempotence=true설정 시 활성화됨
✅ 3. Transaction Coordinator
Kafka 내부에서 트랜잭션을 관리하는 역할
트랜잭션 범위 내의 쓰기 작업을 commit 또는 abort로 원자적 처리
✅ 4. Transactional Consumer (정확히 Once 소비 보장)
소비자 측에서도 정확히 한 번 처리를 위해 read-process-write 패턴을 트랜잭션과 묶어야 함
일반적으로
consume → process → produce전 과정을 하나의 Kafka 트랜잭션 내에서 실행
트랜잭션 시작 → 메시지 소비 → 처리 → 결과 produce → offset 커밋 → 트랜잭션 commit
- Kafka에서는 offset 커밋도 트랜잭션에 포함할 수 있도록
KafkaConsumer를 트랜잭션과 연동함
✅ 5. Exactly Once 설정 요약
| 설정 항목 | 값 |
|---|---|
enable.idempotence | true (Producer 필수) |
acks | all |
transactional.id | 고유한 문자열 지정 |
isolation.level | read_committed (Consumer 측) |
| offset 커밋 방식 | 트랜잭션 내부에서 처리 필요 |
✅ 6. 주의점 / 제약사항
성능 저하: 트랜잭션 overhead로 인한 처리 속도 감소
브로커 최소 요구사항: Kafka 0.11 이상, 3개 이상의 브로커 권장 (quorum을 통한 안정성)
트랜잭션 시간 제한: 기본
transaction.timeout.ms는 15분상호작용하는 시스템도 정확히 한번 처리 보장해야 전체 E2E가 Exactly Once가 됨
💡 실무에서 함께 고려해야 할 것
Kafka 외부 시스템과 연동 시 (DB, API 등) 정확히 한 번 처리 보장 로직은 별도로 구현 필요
EOS는 전체 처리 파이프라인이 트랜잭션을 지원할 때만 실질적인 의미를 가짐
AKKA에서의 Exactly Once 전략
✅ 1. Persistence (Event Sourcing) + At-Least-Once + Idempotency 조합
Akka의 기본 메시지 전송은 at-most-once입니다.
하지만
EventSourcedBehavior또는DurableStateBehavior를 이용해 재시도 및 중복 방지(Idempotency key logic)를 구현하면 사실상 Exactly Once에 근접할 수 있습니다.
✅ 2. Deduplication (중복 제거 로직)
메시지마다 고유 ID (UUID or domain sequence) 부여
처리 완료된 ID는 캐시 (예: Redis, LRU Cache)나 저장소에 기록
동일 ID 수신 시 중복 처리 방지
✅ 3. Transactional Outbox 패턴
메시지 처리와 외부 시스템 전송을 단일 트랜잭션으로 묶음
DB에 Outbox 테이블을 만들고, 메시지를 먼저 저장
별도 프로세스가 Outbox 테이블을 읽고 메시지 전송 후 처리 상태 업데이트
Kafka 등 외부 시스템과 연계 시 유용
✅ 4. Exactly Once Kafka Sink (Kafka + Akka Streams)
Kafka의 Transactional Producer + Akka Streams Alpakka Kafka Connector
Transactional.sink()사용오직 한 번만 메시지가 Kafka에 기록되도록 보장
Transactional
.source(...)
.map { msg -> ProducerMessage.single(..., msg.partitionOffset) }
.via(Transactional.flow(producerSettings, transactionSettings))
.runWith(Sink.ignore(), materializer)
✅ 5. Pekko DurableStateBehavior with External Store
Durable State로 상태를 최신값만 유지
DB와의 upsert 트랜잭션으로 메시지 처리 중복 방지
메시지를 처리한 후에만 상태 반영 → 중복 처리 회피
✅ 6. Reliable Delivery (Akka Typed) (Experimental / Pekko 개선 중)
Akka Typed의
ReliableDelivery모듈은 ack 기반의 메시지 재전송 및 순서 보장을 제공정확히 한 번은 아니지만 적어도 한 번에 기반한 구현을 쉽게 구성 가능
🔐 보조 도구
Akka Cluster Sharding + Persistence: 상태 기반 메시지 라우팅과 저장을 결합
Timers / Schedulers: 메시지 처리 timeout & 재시도 트리거
결론
Akka 자체는 정확히 "Exactly Once"를 자동으로 보장하지 않습니다. 하지만 다음의 조합을 통해 이를 현실적으로 달성할 수 있습니다:
At-Least-Once delivery + Idempotent business logic + Persistence or transactional write