🖼️ 그림 0. (히어로) 미션 크리티컬 음성 서비스를 떠받치는 액터들 — 카툰 히어로 이미지
🇺🇸 English version — Read this article in English: Never Drop a Single Call — How Luware Protects Mission-Critical Voice Services with Akka.NET Actors
🎯 3줄 요약
- Luware는 Microsoft Teams 기반 컨택센터 Nimbus를 운영하며 월 수백만 분의 통화를 무중단으로 처리한다.
- 통화 상태를 마이크로서비스에 흩뿌리니 분산 락 지옥과 레이스 컨디션이 터졌고, 이를 Akka.NET 액터 + 클러스터 샤딩 + 이벤트 소싱으로 풀었다.
- 핵심은 "상태를 가진 객체(액터) 하나가 통화 하나를 통째로 소유"하는 발상의 전환이다. 락이 사라지고 시스템은 "그냥 잘 돌아간다".
들어가며 — 전화 한 통이 끊기면 안 되는 세계
여러분이 콜센터에 전화를 걸었다고 해보자. 상담사 연결, 대기열, 전환(transfer), 녹취, 통화 분석이 밀리초 단위로 맞물려 돌아간다. 여기서 타이밍이 한 번 어긋나면? 통화가 끊긴다. 고객은 화를 내고, 기업은 신뢰를 잃는다.
스위스 회사 Luware가 만드는 Nimbus가 바로 이 세계에 산다. Nimbus는 Microsoft Teams에 통합되는 컨택센터 소프트웨어로,
- 월 수백만 분의 통화를 처리하고
- 전 세계 1,000개 이상의 고객사를 두고 있으며
- 24/7 무중단, 다운타임 제로를 요구받는다.
이 글은 Petabridge가 공개한 Luware 사례 연구를 바탕으로, Luware가 어떤 벽에 부딪혔고 Akka.NET 액터 모델로 어떻게 그 벽을 넘었는지를 개발자 눈높이에서 풀어낸다. 액터가 처음인 분도 따라올 수 있게 개념부터 차근차근 짚는다.
1. 문제 — "분산 락 지옥에 빠진다"
처음에 Luware는 통화 상태(state)를 여러 stateless 마이크로서비스에 나눠서 관리했다. 통화 한 건의 정보가 서비스 A에도, B에도, 캐시에도 흩어져 있는 구조다. 문제는 여기서 시작된다.
🖼️ 그림 1. 분산 락 지옥 vs 액터 — 같은 통화 상태를 여러 서비스가 동시에 만지는 혼돈 / 액터 하나가 통화를 통째로 소유하는 질서
Luware의 엔지니어 Jason Shave의 말이 이 상황을 정확히 요약한다.
"결국 분산 락 지옥에 빠진다. 사방이 레이스 컨디션 천지다 (You end up in distributed locking hell. Race conditions everywhere)."
구체적으로 두 가지 난제가 있었다.
① 공유 가변 상태 + 분산 락
여러 서비스가 같은 통화 상태를 동시에 읽고 쓰면, 일관성을 지키려고 분산 락을 건다. 락은 느리고, 잘못 풀리면 교착(deadlock)이고, 밀리초 단위 경쟁에서 한 번 미끄러지면 통화가 끊긴다.
② 웹훅 라우팅 문제
전화망(telephony) API는 비동기다. "통화가 응답됨", "상대가 끊음" 같은 이벤트를 웹훅(콜백) 으로 뒤늦게 쏴준다. 그런데 이 콜백을 받는 서비스는 stateless이고, 클러스터는 부하에 따라 노드가 늘었다 줄었다 한다. "이 콜백이 도대체 어느 노드, 어느 메모리에 살아있는 통화 세션의 것이냐" 를 매번 정확히 찾아 보내야 하는, 전형적인 분산 시스템 난제다.
2. 액터 모델이란? — 락 대신 우편함
해법으로 들어가기 전에, 액터 모델(Actor Model) 부터 이해하자. 의외로 오래된 아이디어다.
- 1973년, MIT의 Carl Hewitt, Peter Bishop, Richard Steiger가 논문에서 처음 제안했다.
- 1980년대 후반, Ericsson이 통신 교환기를 만들려고 Erlang 언어로 상용화했다. (전화망에서 출발했다는 점이 의미심장하다 — Luware의 도메인과 똑같다!)
- 2009년, Jonas Bonér가 JVM용 Akka를 만들었고, 그 .NET 포팅이 바로 Akka.NET이다.
핵심 발상은 이렇다. "공유 메모리를 락으로 지키지 말고, 아예 상태를 한 명에게만 맡기고 나머지는 메시지로만 부탁하자."
회사 부서를 떠올리면 쉽다. 회계팀 책상 서랍(상태)을 아무나 열어 만지면 난장판이지만, "경비 처리 좀 해주세요"라고 요청(메시지) 을 보내면 회계팀이 알아서 순서대로 처리한다. 액터가 바로 이 부서다.
전통적 방식 (공유 상태 + 락) | 액터 모델 |
여러 스레드가 같은 메모리를 동시에 만짐 | 액터 하나가 자기 상태를 독점 소유 |
일관성 유지를 위해 락/세마포어 필요 | 락이 아예 필요 없음 (한 번에 메시지 하나씩 처리) |
레이스 컨디션·교착 위험 | 메시지 큐가 자연스럽게 순서를 보장 |
직접 메서드 호출 | 비동기 메시지 전달 |
액터의 3가지 특징
- 비동기 메시지 통신: 직접 함수를 호출하지 않고, 서로 메시지(우편)를 주고받는다.
- 독립된 상태: 각 액터는 자기만의 내부 상태를 가지며, 그 누구도 바깥에서 직접 건드릴 수 없다.
- 동적 행동: 메시지를 처리하면서 자식 액터를 만들거나, 다른 액터에 메시지를 보내거나, 스스로 종료할 수 있다.
3. Luware의 해법 — 클러스터 샤딩 + 이벤트 소싱 액터
Luware는 "클러스터 샤딩된 액터 시스템 + 이벤트 소싱" 으로 문제를 정면 돌파했다. 통화 세션 하나당 액터 하나를 띄우고, 그 액터가 해당 통화의 모든 상태를 독점한다. 여기에 Akka.NET의 여러 무기를 조합했다.
🖼️ 그림 2. 클러스터 샤딩 — 통화 세션 ID로 액터를 클러스터 노드에 자동 분배하고, 웹훅이 정확한 액터를 찾아가는 모습
① 클러스터 샤딩(Cluster Sharding) — "위치 투명성"으로 웹훅 라우팅 해결
클러스터 샤딩은 "논리적 ID로 액터를 부르되, 그 액터가 클러스터의 어느 노드에 사는지는 신경 쓰지 않게" 해주는 기능이다. 샤딩이 관리하는 액터를 엔티티(entity) 라 부르며,
- 엔티티는 클러스터 노드들에 자동 분배되고
- 한 엔티티 인스턴스는 항상 한 노드에만 존재하며
- 노드가 들어오고 나갈 때 알아서 재배치(re-balance) 된다.
Luware는 통화 세션 ID를 엔티티 ID로 썼다. 그러자 1장의 웹훅 라우팅 난제가 깔끔하게 풀린다. "이 콜백은 통화 X의 것" → 클러스터 샤딩이 통화 X 액터가 사는 노드로 결정론적으로 보내준다. 서비스 디스커버리도, 분산 락도 필요 없다. 이게 바로 위치 투명성(Location Transparency) 이다.
② 이벤트 소싱(Event Sourcing) — 죽어도 되살아나는 통화 상태
무중단 시스템에서 노드(Pod)는 언제든 죽거나 재시작될 수 있다. 그때 진행 중이던 통화 상태가 날아가면 끝장이다. Luware는 Akka.Persistence의 이벤트 소싱으로 이를 막았다.
🖼️ 그림 3. 이벤트 소싱 — 통화에 일어난 사건을 저널에 차곡차곡 기록하고, 재시작 시 사건을 재생(replay)해 상태를 복구
이벤트 소싱의 흐름은 이렇다.
- 액터가 커맨드(예: "상대를 통화에 추가")를 받는다.
- 현재 상태에 적용 가능한지 검증한다.
- 통과하면 그 효과를 나타내는 이벤트("OO이 추가됨")를 만들어 저널에 영구 저장한다.
- 저장 성공 후 그 이벤트로 상태를 바꾼다.
핵심은 상태가 아니라 "사건의 연속"을 저장한다는 것. 그래서 Pod가 죽어도 저널의 이벤트를 처음부터 재생(replay) 하면 통화 상태가 그대로 복구된다. 상태 손실 없는 복구 — 무중단의 핵심 장치다.
💬 참고로 클러스터 샤딩 + 이벤트 소싱은 Akka 생태계에서 가장 흔한 조합이다. 다만 영속 액터가 정확히 복구·저장되려면 각 엔티티에 전역 고유 PersistentId를 줘야 한다.
③ Become 패턴 — 상황 따라 변신하는 액터
통화는 맥락에 따라 행동이 달라진다. 같은 "사람 추가"라도 일반 통화에서와 상담원 협의 전환(consultative transfer) 중에서는 의미가 다르다. Akka.NET의
Become 패턴은 액터가 현재 행동(behavior)을 통째로 교체하게 해준다. 거대한 if-else 상태 머신 대신, "지금은 협의 전환 모드"처럼 행동을 갈아끼우는 식이다. 코드가 훨씬 읽기 쉬워진다.④ 죽은 통화 감지 — 스케줄 메시지 + 패시베이션
끊겼는데 정리되지 않은 "좀비 통화" 액터가 메모리를 갉아먹으면 안 된다. Luware는 스케줄 메시지(일정 시간 뒤 자신에게 "아직 살아있니?" 메시지 발송)와 패시베이션(passivation)(유휴 액터를 메모리에서 안전하게 내려놓기)으로 자동 청소했다.
⑤ 액터 템플릿 — 복잡함을 감추는 재사용 패턴
마지막 영리한 한 수. Luware는 Akka.NET의 복잡함을 추상화한 재사용 가능한 액터 템플릿을 만들었다. 덕분에 형제 팀들이 Akka.NET 전문가가 되지 않고도 같은 액터 아키텍처로 새 채널 연동을 붙일 수 있었다.
4. 왜 Orleans가 아니라 Akka.NET이었나
.NET 진영에는 Microsoft의 액터 프레임워크 Orleans 도 있다. (Halo 4의 클라우드 서비스를 떠받친 것으로 유명하다.) Luware는 왜 Akka.NET을 골랐을까? Jason Shave는 세 가지를 들었다.
🖼️ 그림 4. 관제실에서 한눈에 보이는 통화들 — Phobos/OpenTelemetry 관측성과 .NET Aspire 개발 경험
결정 요인 | 내용 |
활발한 개발 | 당시 Orleans는 정체된 듯 보였고, Akka.NET은 검증된 JVM Akka의 혈통을 잇고 있었다 |
커뮤니티 | Akka.NET Discord 커뮤니티가 따뜻하고 활발하게 느껴졌다 |
벤더 정렬 | 프레임워크를 직접 만든 Petabridge 엔지니어에게 바로 접근할 수 있고, 그들의 사업적 이해관계가 프레임워크 성공과 일치했다 |
마지막 포인트가 중요하다. Petabridge는 단순 컨설팅사가 아니라 Akka.NET을 직접 만든 팀이다. 모든 설계 결정을 속속들이 안다. 이들은 Boeing, Apple, JPMorgan Chase 같은 기업도 지원하며, 상용 관측성 도구 Phobos와 무료 학습 과정 Akka.NET Bootcamp(2만+ 수강)도 운영한다.
5. 운영 — 보이지 않으면 못 고친다
월 수백만 분의 통화를 디버깅하려면 관측성(observability) 이 생명이다.
- Phobos + OpenTelemetry: 액터 계층 전체를 추적(trace)할 수 있어, 어느 액터에서 무슨 메시지가 어떻게 흘렀는지 들여다본다. 수백만 분 규모의 통화 디버깅에 필수.
- .NET Aspire: 로컬 개발 환경에서 전체 아키텍처를 시뮬레이션하며 개발자 경험을 끌어올렸다.
그 결과를 Luware는 한 문장으로 표현한다 — 시스템이 "그냥 잘 돌아간다(just works)". 프로덕션 안정성에 대한 확신. 미션 크리티컬 음성 서비스에서 이보다 값진 칭찬은 없다.
6. 결과 정리
측면 | 효과 |
신뢰성 | 프로덕션 안정성에 대한 확신 — "그냥 잘 돌아간다" |
락 제거 | 액터가 상태를 독점 → 공유 가변 상태·분산 락 소멸 |
결정론적 라우팅 | 클러스터 샤딩으로 웹훅이 정확한 통화 액터에 도달 |
무중단 복구 | 이벤트 소싱 저널 재생으로 Pod 재시작에도 상태 보존 |
관측성 | Phobos/OpenTelemetry로 액터 계층 전체 추적 |
확장성 | 형제 팀이 전문가 없이도 같은 아키텍처로 새 채널 연동 |
7. 개발자를 위한 교훈 — 언제 액터를 떠올릴까
Luware 사례가 주는 신호는 분명하다. 다음 냄새가 나면 액터 모델을 검토할 때다.
- 상태를 가진 엔티티가 동시에 많이 살아있다 (통화, 주문, 게임 세션, IoT 디바이스, 채팅방…).
- 그 엔티티들이 분산 환경에서 락 때문에 고통받고 있다.
- 비동기 콜백/이벤트를 특정 인메모리 세션으로 정확히 라우팅해야 한다.
- 노드가 죽어도 상태를 잃으면 안 된다.
반대로, 단순 CRUD나 stateless 요청-응답이라면 액터는 과한 무기다. 액터의 진가는 "상태 + 동시성 + 분산" 세 가지가 동시에 터질 때 드러난다.
핵심 한 줄: 락으로 메모리를 지키지 말고, 상태를 액터 하나에게 통째로 맡겨라. Luware는 이 한 줄로 분산 락 지옥에서 빠져나왔다.
참고 자료
- 액터 모델 기원: Carl Hewitt 외, A Universal Modular Actor Formalism for Artificial Intelligence (1973)