- 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/
177 lines
13 KiB
Markdown
177 lines
13 KiB
Markdown
# 📊 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-server` AS 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)
|
|
```
|
|
|
|
1. **제어 평면과 관측 평면의 분리**:
|
|
MAM의 핵심 루프(`run_loop.sh`)는 MQTT 메시지를 구독하지 않으며, 로컬 파일시스템(`.mam/jobs/<id>.json`)을 3초 주기로 폴링(`wait_for_job`)하여 작업 완료를 판정합니다. 브로커는 **비동기 관측(observability) 사이드카**이며 제어 평면을 차단하지 않습니다.
|
|
2. **NATS의 실질적 이점은 '서버'에 존재**:
|
|
NATS의 핵심 강점(단일 무의존 Go 바이너리, JetStream 영속성, nkeys/JWT 계정·Subject별 ACL)은 서버 계층의 속성입니다. `nats-server`는 **MQTT 3.1.1 프로토콜을 네이티브로 수용**하므로, 클라이언트 코드를 한 줄도 바꾸지 않고 서버의 모든 운영·보안 이점을 100% 확보할 수 있습니다.
|
|
3. **네이티브 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()`가 raw `mqtt.Client` 인스턴스를 반환하여 다음 4개 지점에서 구동됩니다:
|
|
1. `mqtt_common.py:250-276` (`make_client`)
|
|
2. `publish_event.py:102-122` (발행 및 ACK 대기)
|
|
3. `job_subscriber.py:172-251` (이벤트 큐잉 및 구독)
|
|
4. `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`의 제어 경로 블로킹 및 디스크 폴백 누락 지적을 실측 검증하였습니다.
|
|
|
|
1. **C1-a (위임 대기 경로 실재)**: `multi-agent-mux-delegate-job:227`에 `wait "$sub_pid"`가 존재하며, `run_loop.sh`의 모든 호출부가 `--type direct`로 이 경로를 통과함을 확인 (수용).
|
|
2. **C1-b (디스크 폴백 부재)**: `job_subscriber.py`는 오직 `watcher.events.get()`만 대기하므로 브로커 단절 시 이벤트를 수신하지 못함 (수용).
|
|
3. **C1-c (메커니즘 선후관계)**: C1은 F-1이 해결되어 디스크에 완료 상태가 쓰여진 이후에 드러나는 연쇄 결함임 (정정 및 반영).
|
|
4. **C1-d (지연 시간 실측)**: 브로커 도달 불가 시 구독자는 15~40초 내 `rc=1`로 조기 종료되어 실제 추가 블로킹은 0초임 (지연 영향 기각, 그러나 감사 로그 오염 및 거짓 실패 판정의 심각성으로 채택).
|
|
5. **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)
|
|
1. **Step 1 (`publish_event.py`)**: 발행 실패 시에도 레지스트리 상태 동기화 및 감사 로그 작성을 완수하고 `return 2` 반환.
|
|
2. **Step 2 (`job_subscriber.py`)**: `queue.Empty` 시 3초 스로틀로 디스크 터미널 상태를 확인하여 `source: disk-fallback` 합성 이벤트 출력 후 `rc=0` 조기 종료.
|
|
3. **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 -js` MQTT 리스너 기본 구동 및 `started/progress/completed` 발행 수용 (`rc=0`).
|
|
- **S-2**: paho-mqtt 2.x `CallbackAPIVersion.VERSION2` CONNACK 호환성 검증.
|
|
- **S-3 (핵심 관문)**: **Retained terminal event 정상 전달 검증** (늦은 구독자의 즉시 최종 상태 수신). *실패 시 mosquitto로 회귀*.
|
|
- **S-4**: QoS 1 `wait_for_publish` ACK 동작 검증.
|
|
- **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 보안 및 워크스페이스 격리 해소
|
|
1. **F-3 해소**: `registry.register_job()`에서 브로커 설정(TLS/인증 유무)과 무관하게 `secrets.token_urlsafe(32)` 기반 `auth_token`을 **무조건 항상 발급**.
|
|
2. **F-2 해소**: `DEFAULT_TOPIC_ROOT`를 `mam/<sha256[:12]>/jobs`로 전환. 발행측 전환 후 `reconcile.sh`의 레거시 구독 단계적 제거.
|
|
3. 배포 설정에 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)
|
|
|
|
1. ❌ **`nats-py` 라이브러리 도입 및 클라이언트 비동기 재작성**: 불필요한 복잡도 및 장애 유발.
|
|
2. ❌ **`.mam/jobs/*.json`의 JetStream KV 대체**: 파일시스템 폴링 제어 계약을 훼손하므로 상태 계층 변경 제외.
|
|
3. ❌ **Durable Session 강제 도입을 위한 `client_id` 고정**: 동시성 충돌 위험이 크며, Track 0 디스크 폴백이 동일 복원력을 무비용으로 제공함.
|
|
4. ❌ **`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)를 순차 전개합니다.
|