AgentZero 실사례로 풀어보는 Akka.NET 메시지 패턴. 작성일: 2026-04-14. 대상: Akka를 처음 보는 .NET / 서버 개발자. 이번 편은 여러 AI 터미널이 왜 질서 있는 메시지 구조를 필요로 하는지부터 설명한다.
![DevBegin > AI 터미널들은 어떻게 질서 있게 대화할까 — Akka 액터 모델 입문편 [PART 1] > 2026-04-14-akka-part1-hero-control-room.png](/download/attachments/125731527/2026-04-14-akka-part1-hero-control-room.png?version=1&modificationDate=1776119065997&api=v2)
최근 재밌는 실험을 하나 진행했다. 어느 순간 내 개발 환경은 코드 편집기라기보다, 여러 AI CLI를 제어하는 조종석이 되어 가고 있었다.
똑똑한 AI CLI만 해도 Claude Code, Codex, Gemini, 온디바이스 LLM까지 제각각이고, 이들을 쓰는 환경도 제각각이다. 어떤 건 WSL 위에서 돌고, 어떤 건 Windows Terminal 위에서 돈다. IDE도 하나로 통일되지 않는다. 인텔리J, 라이더, 웹스톰, 토이 프로젝트에선 VS Code, 조금 더 네이티브한 OS 프로그래밍으로 가면 Visual Studio까지 섞여 들어온다.
문제는 여기서 끝나지 않는다. 왼쪽 IDE에서는 코드를 보고, 아래 터미널에서는 여러 AI CLI를 돌리고, 오른쪽 보조 AI로는 Copilot이나 JetBrains AI를 붙이게 된다. 어느 순간 IDE는 디버깅 도구만이 아니라, CLI를 다중으로 제어하기 위한 거대한 리모컨이 된다.
그렇다고 이 툴들을 버릴 수도 없다. 집중 모드로 들어가면 도커도 관리해야 하고, 메모리 릭 도구도 써야 하고, 코드 퀄리티 검사도 로컬에서 직접 돌려봐야 하기 때문이다. 즉 우리는 “IDE를 버리고 AI만 쓰는 시대”로 간 게 아니라, “IDE 안에서 AI와 기존 도구를 동시에 더 많이 다뤄야 하는 시대”로 들어온 셈이다.
그러다 문득 이런 상상을 하게 됐다. “이 많은 CLI를 TUI 멀티뷰로 모아두고, 명령은 만능 리모컨 하나로 제어하면 어떨까?” 그 발상에서 시작한 프로젝트가 AgentZero다.
처음엔 단순했다. 여러 AI CLI를 한 화면에서 보고, 필요한 쪽에 명령을 보내는 정도였다. 그런데 일을 벌려 놓고 보니 리모컨과 CLI들이 조금씩 더 많은 말을 하기 시작했다. 간단한 명령 전달을 넘어서, AI CLI끼리 티키타카까지 가능해졌다. 바로 그 지점부터 통신체계가 눈에 띄게 복잡해졌고, 사람이 제어하기 어려운 수준에 도달했다.
여기서 문제가 더 재밌고 더 무서워졌다. 통합 리모컨으로 TV를 켰는데 에어컨이 켜지고, 냉장고에게 “식혀줘”라고 했더니 전자레인지가 데우는 모드로 들어가는 식이다. 더 골치 아픈 건 이게 항상 틀리는 것도 아니라는 점이다. 어떤 날은 맞고, 어떤 날은 엉뚱하게 반응한다. 그래서 더 헷갈린다. 리모콘이 지능을 달았나? 장치들이 서로의 신호를 잘못 듣고 있나? 아니면 주소 체계 자체가 꼬인 건가?
![DevBegin > AI 터미널들은 어떻게 질서 있게 대화할까 — Akka 액터 모델 입문편 [PART 1] > 2026-04-14-akka-part1-schrodinger-remote.png](/download/attachments/125731527/2026-04-14-akka-part1-schrodinger-remote.png?version=1&modificationDate=1776120632224&api=v2)
이쯤 되면 이건 그냥 만능 리모컨이 아니라 슈뢰딩거의 리모콘에 가깝다. 버튼을 누르기 전까지는 어떤 장치가 반응할지 확정되지 않고, 버튼을 누른 뒤에도 확률적으로만 맞는 것 같은 이상한 시스템. 개발자 입장에서는 “가끔 되니까 더 위험한” 종류의 버그다.
결국 전체 문제를 해결하려면 버튼을 더 영리하게 누르는 것이 아니라, 누가 어떤 메시지를 받고, 어디까지 전달하고, 누가 책임지고 복구해야 하는지 설계를 다시 해야 했다.
여기서부터는 단순 채팅 앱이 아니라 작은 애니메이션 관제실이 된다. 성격 다른 조연들이 한 무대에 올라와 서로 대사를 주고받기 시작했는데, 감독이 큐 사인을 놓치면 순식간에 장면이 무너지는 구조다. 누가 누구에게 말을 걸었는지, 누가 대기 중인지, 어느 터미널이 죽었는지, 지금 전체 방이 어떤 상태인지 한 번에 알아야 한다.
AgentZero에서 이 장면을 구현하다 보니 기존 WPF 콜백 구조는 바로 한계에 부딪혔다. 그래서 선택한 것이 Akka.NET 액터 모델이다.
이번 편의 핵심
|
![DevBegin > AI 터미널들은 어떻게 질서 있게 대화할까 — Akka 액터 모델 입문편 [PART 1] > 2026-04-14-akka-part1-mailbox-city.png](/download/attachments/125731527/2026-04-14-akka-part1-mailbox-city.png?version=1&modificationDate=1776119071287&api=v2)
토이 스토리의 장난감들을 떠올리면 이해가 쉽다. 장난감 하나하나는 자기 자리와 자기 역할이 있다. 누군가의 머릿속 변수를 몰래 건드리지 않고, 부르면 자기 차례에만 움직인다. Akka의 액터도 비슷하다.
Akka는 “자기 우편함을 가진 작은 일꾼들”이 메시지로만 대화하는 동시성 모델이다.
핵심 약속은 네 가지다.
약속 | 쉬운 설명 | 개발 관점의 의미 |
|---|---|---|
메일박스 + FIFO | 액터는 자기 편지함에 온 메시지를 순서대로 본다 | 내부 상태를 한 번에 하나씩 만져서 thread-safe |
공유 메모리 금지 | 남의 주머니에 손 넣지 않고 말로만 부탁한다 | lock 지옥이 줄어든다 |
위치 투명성 | 옆방 액터든 다른 머신 액터든 주소로 부른다 | 로컬/분산 코드가 비슷해진다 |
감독자 트리 | 부모가 자식 사고를 책임진다 | 장애 복구 전략을 구조로 설계한다 |
여기서 중요한 포인트는 “메시지 큐가 있다”가 아니다. Akka는 상태, 동시성, 라우팅, 장애 격리를 한 모델로 묶는다. 그래서 동시에 여러 AI가 떠드는 장면처럼 상태가 자주 바뀌고, 순서와 복구가 중요한 시스템에서 특히 강하다.
AgentZero의 초기 구조는 전형적인 WPF 코드비하인드였다. MainWindow와 AgentBotWindow가 콜백 4개로 연결되어 있었다.
MainWindow ──(콜백 4개)──→ AgentBotWindow _getActiveSession() _getSessionName() _getActiveDirectory() _getGroups() |
한두 개 창만 다룰 때는 이 방식이 편하다. 그런데 AI 터미널이 여러 개로 늘어나면 바로 어벤져스 작전실 문제가 생긴다. 닉 퓨리가 히어로 한 명씩 직접 전화 돌리는 방식으로는 중간 상태도, 장애도, 전체 상황판도 감당이 안 된다.
문제 | 콜백 구조에서 왜 막히는가 |
|---|---|
터미널 AI -> 봇 통신 | 단방향 콜백이라 터미널 안 AI가 먼저 말을 걸 통로가 없다 |
모드 전환 | 일반 셸인지 AI 점유 상태인지 표현할 모델이 없다 |
워크스페이스 격리 | 모든 터미널이 평평한 리스트에 있어 그룹 제어가 어렵다 |
장애 전파 | UI 스레드에 너무 많은 책임이 몰린다 |
전체 조회 | “지금 전부 몇 개 살아 있어?”를 조합식으로 계산해야 한다 |
즉 기존 구조는 “창 두 개가 서로 도와주는 앱”까지는 괜찮았지만, “여러 AI가 동시에 움직이는 관제실”을 감당하기에는 모델이 너무 납작했다.
![DevBegin > AI 터미널들은 어떻게 질서 있게 대화할까 — Akka 액터 모델 입문편 [PART 1] > 2026-04-14-akka-part1-actor-tree.png](/download/attachments/125731527/2026-04-14-akka-part1-actor-tree.png?version=1&modificationDate=1776119074363&api=v2)
Akka를 도입하면서 AgentZero는 화면 중심 구조에서 도시 구조로 바뀌었다.
ActorSystem("AgentZero")
└── /user/stage (StageActor)
├── /bot (AgentBotActor)
├── /ws-proj1 (WorkspaceActor)
│ ├── /term-0 (TerminalActor)
│ └── /term-1 (TerminalActor)
└── /ws-proj2 (WorkspaceActor)
└── /term-0 (TerminalActor) |
이걸 주토피아 교통관제실처럼 보면 쉽다.
액터 | 비유 | 실제 책임 |
|---|---|---|
| 중앙 관제실 | 전체 자식 생명주기 관리, 메시지 브로커 |
| 사회자 | 사용자와 대화하고 요청을 정리 |
| 구역 관리자 | 워크스페이스별 터미널 묶음 관리 |
| 현장 요원 | 실제 ConPTY 세션 1개를 감싼 실행 단위 |
이 구조가 좋은 이유는 “사람이 이해하는 경계”와 “코드가 실행되는 경계”가 거의 같아지기 때문이다. 워크스페이스는 정말 워크스페이스 액터가 되고, 터미널 하나는 정말 액터 하나가 되고, 사회자는 정말 별도 액터가 된다. 즉 설계도가 런타임 구조로 그대로 살아난다.
Become()이 왜 중요한가![DevBegin > AI 터미널들은 어떻게 질서 있게 대화할까 — Akka 액터 모델 입문편 [PART 1] > 2026-04-14-akka-part1-become-switch.png](/download/attachments/125731527/2026-04-14-akka-part1-become-switch.png?version=1&modificationDate=1776119078820&api=v2)
터미널은 항상 같은 존재가 아니다. 평범한 셸일 때도 있고, AI가 점유한 에이전트 모드일 때도 있다. 여기서 Become()이 등장한다.
[PlainCli] -- AI 프롬프트 감지 --> [AiAgent]
^ |
└-------- 모드 전환 ------------┘ |
같은 TerminalActor라도 상태가 바뀌면 같은 메시지를 다르게 처리한다.
메시지 | PlainCli | AiAgent |
|---|---|---|
| 텍스트 전달 | 텍스트 전달 |
| 무시 | 처리 |
| 로그 출력 | AI 패턴 분석 + 봇 전달 |
일반적인 if-else 구조에서는 “지금 어떤 상태인지”를 매 메시지마다 확인해야 한다. 상태가 늘수록 코드가 길게 퍼진다. 반면 Become()은 아예 핸들러 묶음을 갈아끼운다. 스파이더버스에서 같은 인물이 세계를 넘어갈 때 규칙이 달라지는 것처럼, 액터도 상태에 따라 다른 세계의 규칙으로 움직인다.
Forward가 라우팅을 살린다![DevBegin > AI 터미널들은 어떻게 질서 있게 대화할까 — Akka 액터 모델 입문편 [PART 1] > 2026-04-14-akka-part1-routing-portals.png](/download/attachments/125731527/2026-04-14-akka-part1-routing-portals.png?version=1&modificationDate=1776119081781&api=v2)
여러 단계의 액터를 지나 메시지가 흘러갈 때 가장 무서운 일은 “누가 원래 보낸 말이었는지”를 잃는 것이다. 그래서 핵심 라우팅에는 Forward를 쓴다.
TerminalActor(AiAgent) -> WorkspaceActor -> StageActor -> AgentBotActor |
AgentBotActor -> StageActor -> WorkspaceActor -> TerminalActor(AiAgent) |
Tell이 새 택배를 다시 포장해서 보내는 느낌이라면, Forward는 송장까지 그대로 살려서 다음 허브로 넘기는 느낌이다. 이 차이가 중요하다. 라우팅이 깊어질수록 응답 경로가 꼬이기 쉽기 때문이다.
액터 모델의 또 다른 핵심은 장애 처리다. 처음엔 모든 예외를 Restart로 몰아도 될 것처럼 보인다.
localOnlyDecider: ex => Directive.Restart |
하지만 현실은 그렇지 않았다. ConPTY 파이프가 이미 닫힌 상태라면 재시작해도 다시 같은 예외만 난다. 이럴 때는 Restart가 아니라 Stop이 맞다.
localOnlyDecider: ex => ex switch
{
ObjectDisposedException => Directive.Stop,
IOException => Directive.Stop,
_ => Directive.Restart
} |
중요한 건 무조건 복구가 아니다. 무엇을 복구하고, 무엇은 끊어내야 하는지 경계를 그리는 것이다. 액터 시스템은 이 판단을 코드 한복판이 아니라 감독 전략이라는 구조적 자리에서 하게 해 준다.
새 구조가 좋다고 기존 코드를 하루아침에 다 갈아엎을 수는 없다. AgentZero는 이 이행을 7단계로 쪼갰다.
단계 | 한 일 | 테스트 |
|---|---|---|
2-1 |
| 47 |
2-2 | 터미널 생성/종료 이벤트 메시지화 | 47 |
2-3 |
| 47 |
2-4 | AI 프롬프트 감지 후 모드 전환 | 50 |
2-5 |
| 50 |
2-6 |
| 50 |
2-7 | 터미널 AI <-> 봇 양방향 E2E | 53 |
운영 원칙은 두 가지였다. 새 경로가 완성되기 전까지 기존 콜백을 완전히 끊지 않고, 각 단계마다 dotnet build, dotnet test를 통과시킨다. 가디언즈 멤버를 한 번에 다 태우지 않고 한 명씩 합류시키는 식이다.
![DevBegin > AI 터미널들은 어떻게 질서 있게 대화할까 — Akka 액터 모델 입문편 [PART 1] > 2026-04-14-akka-part1-memory-guards.png](/download/attachments/125731527/2026-04-14-akka-part1-memory-guards.png?version=1&modificationDate=1776119085813&api=v2)
여기서부터가 진짜 실전이다. 액터 구조를 세웠다고 끝나지 않았다. 그 위에서 LLM이 시스템을 어떻게 오용하는지 계속 드러났다.
[AI-REQ], [AI-FnCall], [AI-TOOL], [AI-RESP] 로그를 심고 나서야 거짓 성공, 잘못된 함수 호출 복사, 같은 도구 반복 호출, 무한 폴링 같은 이상 패턴이 보였다. 로그는 단순 기록이 아니라 액터와 LLM 사이에서 무슨 일이 벌어졌는지 보여주는 블랙박스였다.
온디바이스 LLM은 긴 히스토리에서 “지금 어디까지 했는지”를 자주 놓친다. 그래서 AgentBotActor 안에 최근 30개 작업을 보관하는 세션 메모리를 두었다.
private const int MaxMemoryEntries = 30; private readonly List<string> _sessionMemory = new(); |
이 메모리는 사용자 입력, 도구 호출과 결과, 다음에 해야 할 일을 매번 시스템 메시지로 다시 정리해 준다. 인사이드 아웃의 기억구슬처럼 지금까지의 맥락을 잃지 않게 붙잡아 주는 셈이다.
실제로는 이런 일도 있었다.
stage_send는 성공했다고 말하지만 실제로는 터미널에 안 감meeting_say(...) 같은 함수 호출 문법을 그대로 터미널에 타이핑함term_read를 수십 번 반복 호출함그래서 메시지 전달, 에러 문구, 동일 호출 횟수, 라운드당 호출 상한 같은 다층 가드를 넣었다.
좋은 에이전트 시스템은 도구를 많이 주는 시스템이 아니라, 도구를 어디까지 쓰게 할지 경계를 잘 친 시스템이다.
모든 구조와 가드를 넣고 나서야 아래 시나리오가 깨끗하게 돌아갔다.
사용자: “AI를 이용한 개발생산성 3인 미팅 시작해”
단계 | 무슨 일이 일어났나 |
|---|---|
미팅 생성 | 사회자 봇이 회의록 파일을 만든다 |
초대 전송 |
|
의견 수집 |
|
회의록 기록 |
|
최종 응답 | 사용자가 읽을 수 있는 요약을 돌려준다 |
지표도 눈에 띄게 좋아졌다.
지표 | 이전 | 이후 |
|---|---|---|
라운드당 도구 호출 평균 | 50~80 | 1~4 |
멤돔 루프 | 빈번 | 0건 |
| 0%에 가까움 | 100% |
3인 미팅 완결성 | 불가 | 가능 |
즉 Akka는 단지 동시성 프레임워크를 써 봤다에서 끝난 것이 아니라, 여러 AI가 함께 일하는 현장을 실제로 굴리는 기반이 됐다.
이번 1편은 왜 여러 AI 터미널 협업이 콜백 구조로는 쉽게 무너지는지, 왜 Akka의 액터 트리가 이 문제를 잘 정리하는지, 실제 AgentZero가 어떤 관제 구조를 세웠는지를 먼저 보여줬다.
하지만 아직 남은 질문이 있다.
여러 AI가 질서 있게 말할 수 있게 만든 것과, 일반 LLM이 스스로 “일하는 에이전트”가 되는 것은 같은 문제일까?
아니다. 그건 다음 회차의 문제다. PART 2에서는 왜 일반 LLM은 기다리지 못하는지, 왜 ReAct가 필요해졌는지, 왜 Akka Become()이 단순한 모드 전환을 넘어 상태 머신의 뼈대가 되는지를 다룬다.
NEXT - PART 2 1편이 “대화가 엉키지 않게 도시를 세우는 편”이었다면, 2편은 “그 도시 안에서 사회자 LLM에게 기다림과 판단을 가르치는 편”이다. |