본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 16. 17:28

GoAgentX 아키텍처 심층 분석 (파트 2): AHP — 멀티 에이전트를 위한 통신 백본 (Communication Backbone)

요약

GoAgentX 프레임워크의 핵심 통신 프로토콜인 AHP(Agent Hierarchical Protocol)를 소개합니다. 네트워크 오버헤드를 줄이기 위해 Go 채널을 활용하여 단일 프로세스 내에서 멀티 에이전트 간의 빠르고 가벼운 통신을 구현하는 방법을 다룹니다.

핵심 포인트

  • Go 채널 기반의 네트워크가 필요 없는 초고속 통신 프로토콜 구현
  • 비동기 작업 디스패칭, 하트비트, 결함 허용 기능 제공
  • 직렬화 오버헤드 제거를 통한 단일 프로세스 모드 최적화
  • TASK, RESULT, PROGRESS 등 5가지 메시지 모델 정의

이 글은 GoAgentX 시리즈의 두 번째 기사입니다. 파트 1은 여기에서 읽을 수 있습니다.

멀티 에이전트 시스템 (multi-agent systems)에 대해 이야기할 때 흔히 나오는 질문은 다음과 같습니다: 에이전트들은 서로 어떻게 대화하는가? HTTP? WebSocket? 메시지 큐 (Message queues)?

저의 직설적인 답변은 이랬습니다:
그들은 동일한 프로세스 (process) 내에서 실행되고 있습니다. 채팅 한 번 하려고 왜 굳이 네트워크를 거쳐야 합니까?
그렇게 해서 AHP (Agent Hierarchical Protocol)가 탄생했습니다. 이는 순수하게 Go 채널 (Go channels)로 구축된 가볍고 네트워크가 필요 없는 통신 프로토콜입니다.

왜 직접 만들었는가?

GoAgentX에는 두 가지 주요 역할이 있습니다: 리더 에이전트 (Leader Agent, 작업을 위임하는 보스)와 서브 에이전트 (Sub Agents, 작업자). 이들이 해결해야 하는 골칫거리에는 다음이 포함됩니다:

  • 비동기 작업 디스패칭 (Asynchronous task dispatching)
  • 진행 상황 보고 (Progress reporting)
  • 하트비트 (Heartbeat) 및 생존 확인 (liveness detection)
  • 결함 허용 (Fault tolerance) 및 데드 레터 처리 (dead letter handling)

Redis를 시도해 보았고, RabbitMQ도 살펴보았습니다... 그것들은 너무 무겁거나, 너무 느리거나, 혹은 프로세스 내 시스템 (in-process system)에 적합하지 않다는 느낌을 주었습니다. 그래서 저는 단순하고 빠르며 Go 네이티브 (Go-native)한 무언가를 만들기로 결정했습니다.
핵심 장점:

  • 매우 빠름 (채널 vs 네트워크 RTT)
  • 단일 프로세스 모드에서 직렬화 오버헤드 (serialization overhead) 없음
  • 진화가 용이함 — 비즈니스 로직을 변경하지 않고 나중에 백엔드를 gRPC로 교체 가능

AHP의 핵심 구성 요소

  • Protocol: 모든 것을 하나로 묶는 파사드 (Facade)
  • MessageQueue: 버퍼드 채널 (Buffered channel) + 백업 + 원자적 플래그 (atomic flags)
  • HeartbeatMonitor: 스마트 타임아웃 로직으로 죽은 에이전트를 감지
  • DLQ (Dead Letter Queue): 재시도 지원과 함께 실패한 메시지 처리
  • QueueRegistry: 이중 확인 잠금 (double-checked locking)을 이용한 지연 로딩 (Lazy loading)

메시지 모델

AHP는 5가지 메시지 유형을 정의합니다: TASK, RESULT, PROGRESS, ACK, HEARTBEAT.

const (
AHPMethodTask AHPMethod = "TASK" // 작업 할당 (Task allocation)
AHPMethodResult AHPMethod = "RESULT" // 작업 결과 (Task Result)
...

메시지 구조체 (message struct)

type AHPMessage struct {
    MessageID   string         `json:"message_id"`
    Method      AHPMethod      `json:"method"`
...

MessageID 생성

MessageID는 세 부분으로 구성된 ID입니다:

func generateMessageID() string {
    id := atomic.AddUint64(&messageIDCounter, 1)
    randSuffix := getRandomSuffix()
...
  • 타임스탬프 접두사 (Timestamp prefix): 가독성이 높고 문제 해결에 용이합니다.
  • 원자 카운터 (Atomic counter): 동일한 나노초 증가 내에서 여러 메시지의 순서 번호를 제공합니다.
  • 랜덤 접미사 (Random suffix): 다중 프로세스 시나리오에서 충돌을 방지합니다.

이 방식은 전역 코디네이터에 의존하지 않으며, 프로세스 내에서 고유성을 보장합니다.

완전히 이해가 되지 않아도 걱정하지 마세요. 저는 단순히 블록체인(blockchain)의 일부 설계 요소를 차용했을 뿐입니다. 이전에 공개 체인의 설계에 참여한 적이 있으며, 메시지들이 참조되었습니다. 다른 의도는 없습니다. 주된 목적은 **상태 동기화 (state synchronization)**입니다.

HeartbeatMonitor

핵심 프로세스:

  • 각 에이전트(Agent)는 고정 간격(기본값 5초)으로 하트비트(heartbeat)를 전송합니다.
  • HeartbeatMonitor는 가장 최근의 하트비트 시간을 기록합니다.
  • 만약 시간 초과 기간(default 30초)이 초과되고 연속적으로 놓친 하트비트 수가 임계값(default 3회)에 도달하면, 해당 에이전트는 오프라인으로 표시됩니다.

시간 초과 감지 알고리즘 (Timeout detection algorithm)

func (m *HeartbeatMonitor) CheckTimeouts() []string {
    timedOut := m.checkAndMarkOffline()  // 쓰기 잠금(写锁) 하에 검사
    for _, agentID := range timedOut {
...

주요 경계 조건 처리 (Key Boundary Condition Handling):

  1. 점진적 타임아웃 (Progressive Timeout): 간헐적인 네트워크 지연 (Network Latency)으로 인한 오탐 (False Positives)을 방지하기 위해, 3번의 하트비트 (Heartbeat) 미발생 후에만 오프라인 (Offline) 상태로 판단합니다. 2. 중복 콜백 방지 (Avoid Duplicate Callbacks): 오프라인 상태가 된 에이전트는 콜백을 다시 트리거하지 않습니다. 3. 락 외부에서 콜백 실행 (Callbacks Executed Outside Lock): notifyCallbacks 리스트를 복사하고 락 (Lock)을 해제한 후 실행을 시작합니다. 이는 데드락 (Deadlock)을 방지하는 데 매우 중요합니다.

HeartbeatSender에는 두 가지 유형이 있습니다:

  • ahp.HeartbeatSender: 대상의 메시지 큐 (MessageQueue)로 내부 하트비트 (Internal Heartbeat)를 포함한 AHPMethodHeartbeat 메시지를 전송합니다.
  • heartbeatSender (internal/agents/sub/ 내 위치): 외부 하트비트 (External Heartbeat)를 포함하여 HeartbeatMonitor.RecordHeartbeat를 직접 호출합니다.

현재 서브 에이전트 (Sub Agent)는 모놀리식 배포 (Monolithic Deployments) 환경에서 더 효율적인 두 번째 방식을 사용합니다.

데드 레터 큐 (Dead-Letter Queues): DLQ

Enqueue가 에러를 반환하면, Protocol.SendMessage는 실패한 메시지를 DLQ로 라우팅합니다:

func classifyEnqueueError(err error) string {
    switch {
    case errors.Is(err, apperrors.ErrQueueClosed):  return "queue_closed"
...

DLQProcessor는 에러 유형에 따라 커스텀 프로세서 (Custom Processors)를 등록하는 것을 지원하며 자동 재시도 (Automatic Retries)를 지원합니다:

  • MaxRetries = 0: 무한 재시도
  • MaxRetries > 0: 지정된 재시도 횟수에 도달하면 소진 (Exhausted)된 것으로 표시됩니다.
  • 현재는 지수 백오프 (Exponential Backoff)가 구현되어 있지 않으며, 이는 개선이 필요한 영역입니다.

주요 설계 결정 (Key Design Decisions)

왜 비차단 인큐 (Non-Blocking Enqueues)인가?

  • 에이전트 (Agent)는 멀티스레드 (Multi-threaded) 환경에서 동작하므로, 차단 (Blocking)은 연쇄적인 대기 (Cascading Waits)를 초래할 수 있습니다.
  • DLQ는 더 나은 결함 허용 (Fault-tolerance) 시맨틱을 제공합니다. 실패한 메시지를 재시도할 수 있습니다.
  • 호출자 (Caller)가 더 큰 제어권을 갖습니다: 즉시 재시도, 나중에 재시도, 또는 폐기.

TOCTOU 방지

SendMessage에는 핵심적인 설계 결함이 있습니다. 즉, 메시지를 인큐잉(enqueuing)하기 전에 IsFull 여부를 확인하지 않는다는 점입니다. 만약 인큐잉 전에 IsFull을 확인한다면, 확인 시점과 인큐잉 시점 사이에 큐가 '가득 차지 않음'에서 '가득 참' 상태로 변할 수 있으며(TOCTOU race condition), 이는 메시지 손실로 이어집니다. 작업을 직접 실행하고 에러를 처리하는 방식이 더 견고합니다.

확장을 위한 직렬화(Serialization) 예약

현재 AHP는 순수하게 프로세스 내(intra-process) 통신이며, JSON만으로도 충분합니다. 하지만 Codec 인터페이스는 두 가지 방향으로 확장을 예약해 두었습니다.

  • 프로세스 간 통신 (Inter-process communication): protobuf/msgpack를 통해 더 작은 페이로드(payload)를 제공할 수 있습니다.
  • 영속성 (Persistence): DLQ 메시지가 디스크에 기록될 경우, 바이너리 형식이 더 유리합니다.

이제 많은 분이 기다리셨던 폭로(exposé)를 시작하겠습니다:

솔직히 말해서, AHP는 완벽하지 않습니다. 저 또한 몇 가지 함정(pitfalls)을 경험했습니다:

  • 순수 프로세스 내 통신: 프로세스를 넘나들 수 없습니다. 분산 시스템(distributed systems)을 위해서는 MessageQueue 구현체로 전환해야 합니다.
  • 브로드캐스트(broadcast) 불가: 여러 구독자(Subs)에게 메시지를 보내야 하나요? 하나씩 개별적으로 보내야 합니다.
  • 불충분한 재시도 전략: DLQ 재시도 간격이 고정되어 있으며, 지수 백오프(exponential backoff)가 적용되지 않습니다. 이는 지속적인 실패가 발생할 경우 재시도 폭풍(retry storm)을 유발할 수 있습니다.
  • 유연하지 않은 라우팅: 콘텐츠 기반의 동적 라우팅(content-based dynamic routing)이나 토픽(Topic) 구독을 지원하지 않습니다.

하지만 이러한 한계점들은 필요에 따라 선택한 트레이드오프(trade-off)입니다. 모놀리식(monolithic) 단계에서는 채널(channel) 솔루션이 분산 복잡성을 90%까지 줄여줍니다. 만약 향후에 마이크로서비스(microservices)를 실제로 구현하고 싶다면, 하위 구현체만 교체하면 됩니다. 상위의 비즈니스 로직 코드는 변경할 필요가 없습니다. 이것이 추상화 계층(abstraction layer)의 이점입니다. 단일 머신에서 사용할 때는 확실히 매우 매끄럽습니다. 하지만 만약 이 프로젝트를 면접용으로 사용하려 한다면, 분산 환경(distributed environment)에서 이를 어떻게 처리할지에 대해 준비할 것을 권장합니다. 이 글을 쓰는 목적은 시작점(starting point)을 제공하기 위함입니다. AI 시대에는 기본적인 프로그래밍 기술 외에도, 개인적으로 소프트웨어 엔지니어링(software engineering) 기술을 연마할 것을 제안합니다. 결국, 단순히 LLM API 호출자(caller)가 되는 것은 우리의 목표가 아니기 때문입니다.

요약하자면, AHP는 제가 GoAgentX를 위해 개발한 통신 프레임워크입니다. 채널 메시지 전달(Channel message passing), DLQ 폴백(DLQ fallback), 그리고 HeartbeatMonitor 모니터링 — 이 세 가지 구성 요소가 결합되어 멀티 에이전트 통신을 위한 완전한 인프라를 제공합니다.

코드에 남겨둔 인터페이스들(Codec, DLQ handler, MessageSender)은 본질적으로 백업 플랜입니다. 나중에 gRPC나 RabbitMQ로 전환해야 할 경우, 구현 계층(implementation layer)만 간단히 변경하면 됩니다. 하위의 비즈니스 로직은 변하지 않은 채로 유지됩니다. 이러한 설계는 스타트업 프로젝트에서 특히 중요합니다. 내일의 아키텍처가 어떤 모습일지 알 수 없기 때문입니다.

다음으로는 메모리 증류(memory distillation)에 대해 이야기해 보겠습니다. 에이전트가 히스토리에 있는 수백 개의 대화로부터 어떻게 유용한 경험을 추출하는지에 대한 내용입니다. 이는 에이전트 관련 면접에서 자주 질문을 받는 핵심 기술이기도 합니다. 즉, 긴 컨텍스트(long-context) 시나리오에서 모델의 정보가 왜곡되지 않도록 어떻게 보장할 것인가 하는 문제입니다.

GitHub: https://github.com/Timwood0x10/GoAgentX
어떻게 생각하시나요? Star를 누르거나, 직접 사용해 보거나, 피드백을 남겨주세요 — 매우 환영합니다!

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0