- Synthesize collaborative multi-agent architectural analysis in NATS_REPORT.md - Establish Option C: retain MQTT client protocol while adopting nats-server as dedicated broker - Add private server deployment and configuration guide in PRIVATE_SERVER.md - Update IMPROVEMENTS.md with latent defect findings (B-14, B-15, B-16, O-5) and 4-track priority roadmap - Archive durable loop planning and review reports in .agents/reports/
13 KiB
13 KiB
📊 MAM 메시징 백플레인 아키텍처 심층 분석 보고서: MQTT vs NATS
- 문서 버전: Rev.2 Final Synthesis (
f1956d2e/5ac88ca0) - 작성/검토 주체: MAM Multi-Agent Orchestration Team (
claude,agy) - 기준 커밋:
ac82f9b(refactor, 276/276 tests passing) - 문서 목적: MAM 프레임워크의 메시징 인프라(MQTT)를 NATS로 전면 전환할 것인지 여부에 대한 종합적인 기술·운영·보안 타당성 분석 및 실행 로드맵 확정.
0. 최종 판정 (Executive Verdict)
🎯 [VERDICT: DO NOT MIGRATE CLIENT PROTOCOL — ADOPT
nats-serverAS DEDICATED BROKER]클라이언트 전송 프로토콜(MQTT)은 유지하고, 전용 브로커로서
nats-server의 내장 MQTT 3.1.1 어댑터를 채택합니다.
0.1 3대 핵심 근거 요약
[MAM Control Plane] ────> run_loop.sh (wait_for_job: 3s Local Disk Polling) ──> 100% Broker-Independent
[Observability Plane] ────> publish_event.py ──(MQTT 3.1.1)──> nats-server (JetStream + nkeys)
- 제어 평면과 관측 평면의 분리:
MAM의 핵심 루프(
run_loop.sh)는 MQTT 메시지를 구독하지 않으며, 로컬 파일시스템(.mam/jobs/<id>.json)을 3초 주기로 폴링(wait_for_job)하여 작업 완료를 판정합니다. 브로커는 비동기 관측(observability) 사이드카이며 제어 평면을 차단하지 않습니다. - NATS의 실질적 이점은 '서버'에 존재:
NATS의 핵심 강점(단일 무의존 Go 바이너리, JetStream 영속성, nkeys/JWT 계정·Subject별 ACL)은 서버 계층의 속성입니다.
nats-server는 MQTT 3.1.1 프로토콜을 네이티브로 수용하므로, 클라이언트 코드를 한 줄도 바꾸지 않고 서버의 모든 운영·보안 이점을 100% 확보할 수 있습니다. - 네이티브 NATS(
nats-py) 전환의 비용 대비 무익함:nats-py는 asyncio 전용 라이브러리로, bash 기반의 단명(short-lived) 동기 CLI 도구들(publish_event.py등)과 심각한 구조적 마찰을 일으키며, 최소 46건의 테스트 재작성 및 276건 green 베이스라인 훼손 위험을 초래합니다. 반면 NATS 고유 기능(Req/Reply, 초당 수백만 처리량, 클러스터링)은 MAM 워크로드(단일 워크스페이스, 잡당 수 개 이벤트)에서 전혀 사용되지 않습니다.
1. 3대 전략적 선택지 비교 분석
| 평가 항목 | (A) 현행 유지 (공개 HiveMQ) | (B) 네이티브 NATS 전환 (nats-py) |
(C) nats-server + MQTT 프로토콜 유지 (채택안) |
|---|---|---|---|
| 클라이언트 코드 변경 | 0줄 | 4개 호출부 전면 비동기 재작성 | 0줄 (환경변수만 구성) |
| 테스트 코드 재작성 | 0건 | 최소 46건 재작성 (276건 베이스라인 위험) | 0건 (기존 276건 100% 보존) |
| A-2 보안 결함 해소 | ❌ 불가 (공개 브로커) | ✅ 완전 해소 | ✅ 완전 해소 (nkeys/JWT subject ACL) |
| 단일 정적 바이너리 배포 | ❌ 불가 | ✅ 지원 | ✅ 지원 (nats-server 바이너리 1개) |
| 이벤트 영속성 (JetStream) | ❌ 미지원 | ✅ 지원 | ✅ 지원 (내장 JetStream 엔진) |
| 동기 CLI 호환성 | ✅ 우수 (paho-mqtt) | ❌ 심각 (asyncio 강제) | ✅ 우수 (기존 동기 핫패스 유지) |
| 되돌리기(Rollback) 비용 | — | 🔴 높음 (비가역 코드 재작성) | 🟢 0 (가역적 환경변수 스위치) |
| 최종 평가 | 기각 (보안 위험) | 기각 (비용 대비 실익 전무) | 🏆 최종 채택 |
2. 현행 아키텍처 실측 및 기술적 진단 (Ground Truth)
2.1 제어 경로 상의 MQTT 의존도 실측
run_loop.sh(899줄, 메인 오케스트레이터) 내 MQTT 직접 참조는:889의 임시 구독자 로그 파일 삭제 1건뿐입니다.- 작업 완료 감지는 11개 호출 지점(전체 12개 참조) 전체가
wait_for_job()함수를 통해.mam/jobs/<id>.json파일의status필드를 3초 간격으로 검사합니다. - 따라서 브로커가 다운되어도 제어 평면 자체는 독립적으로 완주할 수 있는 구조입니다.
2.2 paho-mqtt 결합도 (Blast Radius)
import paho는mqtt_common.py:32단 1곳에 캡슐화되어 있습니다.- 그러나
make_client()가 rawmqtt.Client인스턴스를 반환하여 다음 4개 지점에서 구동됩니다:mqtt_common.py:250-276(make_client)publish_event.py:102-122(발행 및 ACK 대기)job_subscriber.py:172-251(이벤트 큐잉 및 구독)reconcile.sh:245-292(내장 python 이벤트 수신)
3. 코드베이스 잠복 결함 분석 (F-1 ~ F-5)
브로커 제품 선택과 무관하게 현행 코드에 잠복해 있는 5가지 구조적 결함이 발굴되었습니다.
[발굴된 결함 체인]
F-1: publish 실패 시 return 2 ──> 레지스트리 상태 동기화 누락 ──> run_loop 3900초(65분) 정지
F-4: subscriber 미포착 예외 rc=1 ──> loop/discuss 경로에서 job_status="error" 오판정
F-2/F-3: 전역 토픽 + auth_token 조건부 ──> 워크스페이스 격리 및 HMAC 검증 사각지대 (A-2)
F-5: 매 실행 랜덤 client_id ──> 문서가 주장하는 durable session 구성 불가
3.1 F-1 (Critical): 발행 실패 시 레지스트리 갱신 누락 (65분 루프 정지)
publish_event.py:195-199에서 브로커 네트워크 오류 발생 시return 2로 조기 종료됩니다.- 이로 인해 뒤따르는
append_event,registry.append_event,update_job_status(status=completed)가 실행되지 못합니다. wait_for_job은status=running상태에서max_wait=3900s를 소진할 때까지 65분간 정지합니다.
3.2 F-2 (High): 워크스페이스 지문 토픽 미발행 (A-2)
reconcile.sh:236은 지문 토픽(mam/<fp>/jobs/+/events)을 구독하지만,mqtt_common.py:119는 전역 토픽(python/mqtt/jobs)으로만 발행합니다.- 워크스페이스 간 메시지 격리가 실질적으로 비활성화되어 있습니다.
3.3 F-3 (High): HMAC 인증 조건부 공허화 (A-2)
registry.py:75-79는 TLS나 사용자 인증이 켜진 보안 브로커 감지 시secrets.token_urlsafe(32)를 자동 생성하나, 기본 공개 브로커(또는 평문 TCP 브로커) 환경에서는 토큰이 발급되지 않아auth_token=None으로 남습니다.- 이로 인해
verify_hmac의if not auth_token: return True분기가 무조건 참이 되어 공개 브로커 환경에서 HMAC 검증이 무력화됩니다 (Track 2에서 전 브로커 대상 무조건 발급으로 심층 방어 적용 필요).
3.4 F-4 (Critical): loop/discuss 위임 경로의 오판정 결함
multi-agent-mux-delegate-job:331-341에서wait "$sub_pid"의sub_rc를 직접job_status로 매핑(rc=1->job_status="error").- 브로커 연결 실패 시
job_subscriber.py가 미포착 예외로rc=1을 내므로, 브로커 접속 실패가 작업 에러로 둔갑합니다.
3.5 F-5 (Medium): 영속 세션(Durable Session) 구성 불가
make_client()가 매 실행마다uuid.uuid4().hex[:8]로 랜덤client_id를 생성하므로, 브로커가 재연결 세션을 식별할 수 없습니다.
4. 리뷰 및 챌린지 검증 (Challenge Resolution)
4.1 C1 챌린지 분석 및 판정
Reviewer (agy)가 제기한 job_subscriber.py의 제어 경로 블로킹 및 디스크 폴백 누락 지적을 실측 검증하였습니다.
- C1-a (위임 대기 경로 실재):
multi-agent-mux-delegate-job:227에wait "$sub_pid"가 존재하며,run_loop.sh의 모든 호출부가--type direct로 이 경로를 통과함을 확인 (수용). - C1-b (디스크 폴백 부재):
job_subscriber.py는 오직watcher.events.get()만 대기하므로 브로커 단절 시 이벤트를 수신하지 못함 (수용). - C1-c (메커니즘 선후관계): C1은 F-1이 해결되어 디스크에 완료 상태가 쓰여진 이후에 드러나는 연쇄 결함임 (정정 및 반영).
- C1-d (지연 시간 실측): 브로커 도달 불가 시 구독자는 15~40초 내
rc=1로 조기 종료되어 실제 추가 블로킹은 0초임 (지연 영향 기각, 그러나 감사 로그 오염 및 거짓 실패 판정의 심각성으로 채택). - C1-e (해결책 채택):
job_subscriber.py의 대기 루프에 로컬 디스크(load_job/read_logged_status) 폴백을 도입하여 브로커 단절 시에도 즉시 정상 종료하도록 보강.
5. 단계별 실행 계획 (Actionable Roadmap)
[Track 0: 결함 교정] ──> [Track 1: nats-server 스파이크] ──> [Track 2: A-2 보안/격리 해소] ──> [Track 3: 문서화]
(F-1, C1, F-4 해결) (S-1 ~ S-9 매트릭스 검증) (F-2, F-3, Token, ACL) (MESSAGING, VERSIONS)
5.1 Track 0 — 가용성 및 결함 교정 (최우선 과제, 브로커 무관)
Step 순서 의존성 (Strict Ordering)
- Step 1 (
publish_event.py): 발행 실패 시에도 레지스트리 상태 동기화 및 감사 로그 작성을 완수하고return 2반환. - Step 2 (
job_subscriber.py):queue.Empty시 3초 스로틀로 디스크 터미널 상태를 확인하여source: disk-fallback합성 이벤트 출력 후rc=0조기 종료. - Step 3 (
multi-agent-mux-delegate-job): 인프라 예외에 전용rc=3을 부여하고job_status="broker_unavailable"분기 처리.
회귀 가드 매트릭스 (11종 신설 — G-1 ~ G-11, 목표 287/287 PASS)
- G-1: 브로커 도달 불가 발행 시
rc=2이면서 레지스트리status=completed확인. - G-2: 감사 로그에
published: false및publish_error필드 기록 확인. - G-3: 정상 브로커 발행 시
rc=0및published: true무회귀 확인. - G-4: 발행 실패 시 단조
last_seq증가 및 후속 발행 seq 보존 확인. - G-5: 디스크
status=completed선작성 시 브로커 다운 상태에서도job_subscriber.py가 3초 내rc=0종료. - G-6: 디스크 폴백 종료 시 stdout에
disk-fallback명시 확인. - G-7: 디스크
status=error시 폴백rc=1반환 확인. - G-8: 다중 잡 감시 시 전체 완료 전까지 조기 종료 방지.
- G-9: 디스크 터미널 부재 + 브로커 실패 시
rc=3반환 확인 (F-4 방어). - G-10:
loop위임 경로에서rc=3수신 시job_status가"error"로 오판되지 않음을 확인. - G-11:
registry.register_job()호출 시auth_token이 항상 비어있지 않게 생성됨을 단언 (secrets.token_urlsafe(32)유지).
5.2 Track 1 — nats-server 스파이크 검증 매트릭스 (S-1 ~ S-9)
격리 클론($SCRATCH/nats-spike)에서 검증 수행:
- S-1:
nats-server -jsMQTT 리스너 기본 구동 및started/progress/completed발행 수용 (rc=0). - S-2: paho-mqtt 2.x
CallbackAPIVersion.VERSION2CONNACK 호환성 검증. - S-3 (핵심 관문): Retained terminal event 정상 전달 검증 (늦은 구독자의 즉시 최종 상태 수신). 실패 시 mosquitto로 회귀.
- S-4: QoS 1
wait_for_publishACK 동작 검증. - S-5: 와일드카드 토픽(
mam/<fp>/jobs/+/events) 구독 및 라우팅 검증. - S-6: TLS 암호화 및 유저 인증 접근 제어 검증.
- S-7: Subject/Topic 레벨 권한 분리(Publisher write-only / Subscriber read-only) 검증.
- S-8: 전체 287건 회귀 테스트 100% PASS 검증.
- S-9: Track 0 디스크 폴백이
nats-server장애 상황에서도 정상 동작함을 통합 검증.
5.3 Track 2 — A-2 보안 및 워크스페이스 격리 해소
- F-3 해소:
registry.register_job()에서 브로커 설정(TLS/인증 유무)과 무관하게secrets.token_urlsafe(32)기반auth_token을 무조건 항상 발급. - F-2 해소:
DEFAULT_TOPIC_ROOT를mam/<sha256[:12]>/jobs로 전환. 발행측 전환 후reconcile.sh의 레거시 구독 단계적 제거. - 배포 설정에 nkeys 기반 계정 분리 적용.
5.4 Track 3 — 문서 및 설정 동기화
MESSAGING.md: 브로커 사양을nats-server로 갱신, F-1/C1 해소 기록, F-5 실측에 맞춘 영속 세션 설명 정정.IMPROVEMENTS.md&VERSIONS.md: A-2 완료 전환, F-1/F-4/F-5 백로그 이력 반영..mam.env:nats-server포트(1883/8883) 및 인증 템플릿 갱신.
6. 비-목표 (Explicit Non-Goals)
- ❌
nats-py라이브러리 도입 및 클라이언트 비동기 재작성: 불필요한 복잡도 및 장애 유발. - ❌
.mam/jobs/*.json의 JetStream KV 대체: 파일시스템 폴링 제어 계약을 훼손하므로 상태 계층 변경 제외. - ❌ Durable Session 강제 도입을 위한
client_id고정: 동시성 충돌 위험이 크며, Track 0 디스크 폴백이 동일 복원력을 무비용으로 제공함. - ❌
requirements.txt내paho-mqtt>=2.0.0제거: 현행 종속성 유지.
7. 결론
MAM 프레임워크의 메시징 백플레인은 **클라이언트 프로토콜(MQTT)을 100% 보존한 상태에서 nats-server를 전용 브로커로 채택(Option C)**하는 것이 기술적·운영적·보안적 최적해입니다.
선행 필수 과제인 **Track 0(F-1 + C1 + F-4 가용성 결함 교정)**을 우선 완수한 후, 스파이크 검증(Track 1) 및 A-2 보안 강화(Track 2)를 순차 전개합니다.