# ๐Ÿ”’ MAM ๊ฐœ์ธ ์ „์šฉ ๋ธŒ๋กœ์ปค(Private Broker) ๊ตฌ์ถ• ๋ฐ ์—ฐ๋™ ๊ฐ€์ด๋“œ (`PRIVATE_SERVER.md`) - **์ž‘์„ฑ์ผ**: 2026-08-20 (Rev.2) - **๋ฌธ์„œ ๋ชฉ์ **: MAM(Multi-Agent Mux)์˜ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์˜์กด์„ฑ ๋ฐ ๋ณด์•ˆ ๊ฒฐํ•จ(A-2)์„ ํ•ด์†Œํ•˜๊ธฐ ์œ„ํ•œ ๊ฐœ์ธ ์ „์šฉ ๋ธŒ๋กœ์ปค(NATS / Mosquitto) ๊ตฌ์ถ•, ์šด์˜, ๋‹ค๋Šฅ์„ฑ ํ™œ์šฉ ๋ฐ MAM ํด๋ผ์ด์–ธํŠธ ์—ฐ๋™ ๊ฐ€์ด๋“œ. - **์—ฐ๊ณ„ ๋ฌธ์„œ**: [`NATS_REPORT.md`](NATS_REPORT.md), [`implementation_plan.md`](implementation_plan.md), [`IMPROVEMENTS.md`](IMPROVEMENTS.md) --- ## 1. ๊ฐœ์š” ๋ฐ ๋„์ž… ๋ฐฐ๊ฒฝ ํ˜„์žฌ MAM ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง• ๋ธŒ๋กœ์ปค๋Š” ๊ณต๊ฐœ ์„œ๋ฒ„(`broker.hivemq.com:1883`)๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ์ธ ์ „์šฉ ๋ธŒ๋กœ์ปค(Private Broker)๋ฅผ ๊ตฌ์ถ•ํ•˜์—ฌ ์—ฐ๊ฒฐํ•˜๋ฉด **ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด(0์ค„ ๋ณ€๊ฒฝ)** ๋ณด์•ˆ ์œ„ํ—˜์„ ์›์ฒœ ์ฐจ๋‹จํ•˜๊ณ  ๋„คํŠธ์›Œํฌ ์•ˆ์ •์„ฑ์„ ๋Œ€ํญ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ``` [MAM Orchestrator / Agents] โ”‚ โ–ผ (MQTT 3.1.1 / TLS) [Private Dedicated Broker] โ”€โ”€โ”€> ์‚ฌ์„ค๋ง/๊ฐœ์ธ ์„œ๋ฒ„ (NATS Server / Mosquitto) โ€ข ์™ธ๋ถ€ ๋ถˆ๋ฒ• ํŠธ๋ž˜ํ”ฝ 100% ์ฐจ๋‹จ (A-2 ๋ณด์•ˆ ํ•ด์†Œ) โ€ข JetStream ์˜์†์„ฑ ๋ฐ NKey/JWT ACL ์ง€์› โ€ข ์ดˆ์ €์ง€์—ฐ (<1ms) ๋ฐ ๋ฌด์ œํ•œ ๋Œ€์—ญํญ ``` MAM์˜ ์ œ์–ด ํ‰๋ฉด(`run_loop.sh`์˜ `wait_for_job` ํŒŒ์ผ์‹œ์Šคํ…œ ํด๋ง)์€ ๋ธŒ๋กœ์ปค์™€ 100% ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋ฏ€๋กœ, ๋ธŒ๋กœ์ปค๋Š” **๋น„๋™๊ธฐ ๊ด€์ธก(observability) ์‚ฌ์ด๋“œ์นด** ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. --- ## 2. ํ•ด๊ฒฐ ์˜์—ญ ๋งคํŠธ๋ฆญ์Šค (๋ธŒ๋กœ์ปค ์ „ํ™˜ vs ์ฝ”๋“œ ํŒจ์น˜) ์ „์šฉ ๋ธŒ๋กœ์ปค ๊ตฌ์ถ•์œผ๋กœ ์ฆ‰์‹œ ํ•ด๊ฒฐ๋˜๋Š” ์˜์—ญ๊ณผ, ๋กœ์ปฌ ์ฝ”๋“œ ํŒจ์น˜(Track 0)๊ฐ€ ๋ณ‘ํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ์˜์—ญ์˜ ๋ช…ํ™•ํ•œ ๊ตฌ๋ถ„์ž…๋‹ˆ๋‹ค. | ๊ตฌ๋ถ„ | ๋‹น๋ฉด ๊ณผ์ œ | ๊ฐœ์ธ ๋ธŒ๋กœ์ปค ๊ตฌ์ถ• ์‹œ | ๋กœ์ปฌ ์ฝ”๋“œ ํŒจ์น˜ ํ•„์š” ์—ฌ๋ถ€ (Track 0) | |---|---|:---:|:---:| | **๋ณด์•ˆ (A-2)** | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ๋…ธ์ถœ ๋ฐ ์™ธ๋ถ€ ์•…์˜์  ์ด๋ฒคํŠธ ์ˆ˜์‹  ์œ„ํ˜‘ | ๐ŸŸข **100% ์ฆ‰์‹œ ํ•ด์†Œ** (์‚ฌ์„ค๋ง/ACL ๊ฒฉ๋ฆฌ) | Track 2์—์„œ ํ† ํฐ ๋ฐœ๊ธ‰ ๊ฐ•์ œ | | **์•ˆ์ •์„ฑ** | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค์˜ ์˜ˆ๊ณ  ์—†๋Š” ์ˆœ๋‹จ ๋ฐ ์†๋„ ์ œํ•œ(Rate-limit) | ๐ŸŸข **100% ์ฆ‰์‹œ ํ•ด์†Œ** (์ „์šฉ ๋ฆฌ์†Œ์Šค) | โ€” | | **๋‚ด๊ฒฐํ•จ์„ฑ (B-14)** | ๋ธŒ๋กœ์ปค ์ผ์‹œ ์žฅ์•  ์‹œ 65๋ถ„ ๋ฃจํ”„ ์ •์ง€(Hang) ๊ฒฐํ•จ | โš ๏ธ ๋ธŒ๋กœ์ปค ์ ๊ฒ€/์ˆœ๋‹จ ์‹œ ์—ฌ์ „ํžˆ ์œ„ํ—˜ | ๐Ÿ”ด **ํ•„์ˆ˜ (Track 0 Step 1 ์„ ํ–‰ ํŒจ์น˜)** | | **์ง€์—ฐ/์˜คํŒ (B-15)** | ๋ธŒ๋กœ์ปค ๋‹ค์šด ์‹œ 120์ดˆ ์ง€์—ฐ ๋ฐ ์ •์ƒ ์ž‘์—…์˜ ์—๋Ÿฌ ์˜คํŒ์ • | โš ๏ธ ๋ธŒ๋กœ์ปค ์ ๊ฒ€/์ˆœ๋‹จ ์‹œ ์—ฌ์ „ํžˆ ์œ„ํ—˜ | ๐Ÿ”ด **ํ•„์ˆ˜ (Track 0 Step 2 & 3 ์„ ํ–‰ ํŒจ์น˜)** | > [!IMPORTANT] > **๋ฐฉํƒ„ ์•„ํ‚คํ…์ฒ˜ ์›์น™:** > "Track 0(`B-14`, `B-15`) ํŒจ์น˜๋ฅผ ํ†ตํ•ด ๋ธŒ๋กœ์ปค๊ฐ€ ๋‹ค์šด๋˜์–ด๋„ ๋ฃจํ”„๊ฐ€ 100% ์ •์ƒ ์™„์ฃผํ•˜๋„๋ก ๋กœ์ปฌ ๋””์Šคํฌ ๋‚ด๊ฒฐํ•จ์„ฑ์„ ๋จผ์ € ํ™•๋ณดํ•˜๊ณ , ๊ฐœ์ธ ๋ธŒ๋กœ์ปค๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ A-2 ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ์„ ์™„๊ฒฐํ•ฉ๋‹ˆ๋‹ค." --- ## 3. ์ „์šฉ ๋ธŒ๋กœ์ปค ์ถ”์ฒœ ๋ฐ ๋น„๊ต MAM ํด๋ผ์ด์–ธํŠธ๋Š” ํ‘œ์ค€ `paho-mqtt`๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, MQTT 3.1.1์„ ์ง€์›ํ•˜๋Š” ๋ชจ๋“  ๋ธŒ๋กœ์ปค์™€ 100% ํ˜ธํ™˜๋ฉ๋‹ˆ๋‹ค. | ๋น„๊ต ํ•ญ๋ชฉ | ๐Ÿ† `nats-server` (๊ฐ•๋ ฅ ๊ถŒ์žฅ) | `eclipse-mosquitto` (๋Œ€์•ˆ) | |---|---|---| | **์•„ํ‚คํ…์ฒ˜** | Go ๋‹จ์ผ ์ •์  ๋ฐ”์ด๋„ˆ๋ฆฌ (Zero Dependency) | C ๊ธฐ๋ฐ˜ ๊ฒฝ๋Ÿ‰ ์˜คํ”ˆ์†Œ์Šค ๋ธŒ๋กœ์ปค | | **์ฃผ์š” ํŠน์ง•** | โ€ข ๋‚ด์žฅ MQTT 3.1.1 ๋ฆฌ์Šค๋„ˆ (`mqtt { port: 1883 }`)
โ€ข JetStream ์—”์ง„ ๋‚ด์žฅ (์ด๋ฒคํŠธ ์˜์†ํ™” ๋ฐ ๋ณต๊ตฌ)
โ€ข NKey/JWT ๊ธฐ๋ฐ˜ ๊ณ„์ • ๋ฐ Subject๋ณ„ ์„ธ๋ถ„ํ™”๋œ ACL
โ€ข WebSocket ๋ฐ NATS ๋„ค์ดํ‹ฐ๋ธŒ ํ”„๋กœํ† ์ฝœ ๋™์‹œ ์„œ๋น™ | โ€ข ๊ฐ€์žฅ ๋„๋ฆฌ ์“ฐ์ด๋Š” ํ‘œ์ค€ ๊ฒฝ๋Ÿ‰ MQTT ๋ธŒ๋กœ์ปค
โ€ข ๋‚ฎ์€ ๋ฉ”๋ชจ๋ฆฌ ์ ์œ ์œจ (~10MB) | | **์ถ”์ฒœ ์šฉ๋„** | ๋ชจ๋˜ ์ธํ”„๋ผ, ํ™•์žฅ์„ฑ, ๊ฐ์‚ฌ ๋กœ๊ทธ ์˜์†ํ™”, ํ™ˆ๋žฉ ํ†ตํ•ฉ | ์ •ํ†ต ์ดˆ๊ฒฝ๋Ÿ‰ ์ž„๋ฒ ๋””๋“œ/๋‹จ์ผ ๋ชฉ์  ํ™˜๊ฒฝ | | **๋ฐฐํฌ ๋‚œ์ด๋„** | ๐ŸŸข ๋ฐ”์ด๋„ˆ๋ฆฌ 1๊ฐœ ์‹คํ–‰ ๋˜๋Š” Docker 1์ค„ | ๐ŸŸข ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ € (`apt`, `brew`) ๋˜๋Š” Docker | --- ## 4. ๊ฐœ์ธ ์„œ๋ฒ„ ๋ธŒ๋กœ์ปค ๋ฐฐํฌ ๊ฐ€์ด๋“œ ### 4.1 `nats-server` ๋ฐฐํฌ (๊ถŒ์žฅ) `nats-server`์—์„œ MQTT๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด ์„ค์ • ํŒŒ์ผ(`nats.conf`)์— `mqtt { port: 1883 }` ๋ธ”๋ก๊ณผ `jetstream { }` ๋ธ”๋ก์ด ๋ฐ˜๋“œ์‹œ ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. > [!NOTE] > `nats-server`์˜ `-m` ํ”Œ๋ž˜๊ทธ๋Š” HTTP ๋ชจ๋‹ˆํ„ฐ๋ง ํฌํŠธ(`--http_port`)๋ฅผ ์ง€์ •ํ•˜๋Š” ์˜ต์…˜์ด๋ฉฐ, MQTT๋ฅผ ์ผœ๋Š” ํ”Œ๋ž˜๊ทธ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. MQTT ํ™œ์„ฑํ™”๋Š” ๋ฐ˜๋“œ์‹œ `-c nats.conf` ์„ค์ • ํŒŒ์ผ์„ ํ†ตํ•ด ๊ตฌ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. #### 1) ๊ณตํ†ต ์„ค์ • ํŒŒ์ผ (`nats.conf`) ```conf # nats.conf server_name: mam-hub # JetStream ์˜์† ์Šคํ† ๋ฆฌ์ง€ (MQTT QoS 1 ๋ฐ Retained ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์— ํ•„์ˆ˜) jetstream { store_dir: "/data" # Docker ํ™˜๊ฒฝ ๊ธฐ๋ณธ ์Šคํ† ๋ฆฌ์ง€ (๋„ค์ดํ‹ฐ๋ธŒ ์‹คํ–‰ ์‹œ $HOME ์ „๊ฐœ) max_file: 10G # ํ™ˆ๋žฉ ๋””์Šคํฌ ์ƒํ•œ ์„ค์ • } # HTTP ๋ชจ๋‹ˆํ„ฐ๋ง ์—”๋“œํฌ์ธํŠธ (/varz, /jsz ๋Œ€์‹œ๋ณด๋“œ) http_port: 8222 # ํ‰๋ฉด A: MAM MQTT 3.1.1 ํ”„๋กœํ† ์ฝœ ๋ฆฌ์Šค๋„ˆ mqtt { port: 1883 } # ํ‰๋ฉด B: ํ™ˆ๋žฉ/์›น ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ์šฉ WebSocket ๋ฆฌ์Šค๋„ˆ (์„ ํƒ ์‚ฌํ•ญ) websocket { port: 8080 no_tls: true # ๋‚ด๋ถ€ ์‚ฌ์„ค๋ง ํ•œ์ • } ``` #### 2) ๋ฐฐํฌ ๋ฐฉ๋ฒ• A. Docker / Docker Compose (๊ถŒ์žฅ) **๋‹จ์ผ Docker ์‹คํ–‰:** ```bash # ํ˜ธ์ŠคํŠธ์— nats.conf ์ƒ์„ฑ ํ›„ ์‹คํ–‰ (D-2: alpine ๊ณ ์ • ํ•€, D-3: ๋ฃจํ”„๋ฐฑ/๋ฐ”์ธ๋“œ ํ•œ์ •) docker run -d \ --name mam-nats \ --restart unless-stopped \ -p "${MQTT_BIND:-127.0.0.1}:1883:1883" \ -p "${NATS_BIND:-127.0.0.1}:4222:4222" \ -p "127.0.0.1:8222:8222" \ -p "${WS_BIND:-127.0.0.1}:8080:8080" \ -v ./nats.conf:/etc/nats/nats.conf:ro \ -v nats-data:/data \ nats:2.12-alpine \ -c /etc/nats/nats.conf ``` **Docker Compose (`docker-compose.yml`):** ```yaml version: '3.8' services: nats: image: nats:2.12-alpine container_name: mam-nats restart: unless-stopped command: ["-c", "/etc/nats/nats.conf"] ports: - "${MQTT_BIND:-127.0.0.1}:1883:1883" # MQTT 3.1.1 ํฌํŠธ (ํ‰๋ฉด A: MAM) - "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS ๊ธฐ๋ณธ ํฌํŠธ (ํ‰๋ฉด B) - "127.0.0.1:8222:8222" # HTTP ๋ชจ๋‹ˆํ„ฐ๋ง (/varz, /jsz, /healthz) - "${WS_BIND:-127.0.0.1}:8080:8080" # WebSocket (ํ‰๋ฉด B) volumes: - ./nats.conf:/etc/nats/nats.conf:ro - nats-data:/data volumes: nats-data: ``` #### 3) ๋ฐฐํฌ ๋ฐฉ๋ฒ• B. ๋„ค์ดํ‹ฐ๋ธŒ ๋ฐ”์ด๋„ˆ๋ฆฌ ์„ค์น˜ (macOS / Linux โ€” ๋น„๋ฃจํŠธ ์‚ฌ์šฉ์ž ๊ณต๊ฐ„) macOS์˜ sealed APFS ๋ฃจํŠธ ๋ณผ๋ฅจ(`/data`) ๊ถŒํ•œ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ์ž ํ™ˆ ๋””๋ ‰ํ„ฐ๋ฆฌ(`~/.config/nats/`, `~/.local/share/nats/data`)๋ฅผ ๊ธฐ๋ณธ ์Šคํ† ๋ฆฌ์ง€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ```bash # ์„ค์ • ๋ฐ ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ƒ์„ฑ (sudo ๋ถˆํ•„์š”) mkdir -p ~/.config/nats ~/.local/share/nats/data # ์„ค์ • ํŒŒ์ผ ์ž‘์„ฑ (D-1: ๋น„์ธ์šฉ heredoc < ~/.config/nats/nats.conf server_name: mam-hub jetstream { store_dir: "$HOME/.local/share/nats/data" max_file: 10G } http_port: 8222 mqtt { port: 1883 } websocket { port: 8080 no_tls: true } EOF # macOS (Homebrew ์„ค์น˜ ๋ฐ ์‹คํ–‰) brew install nats-server nats-server -c ~/.config/nats/nats.conf & # Linux (x86_64 ๋‹จ์ผ ๋ฐ”์ด๋„ˆ๋ฆฌ ์„ค์น˜ ๋ฐ ์‹คํ–‰) curl -L https://github.com/nats-io/nats-server/releases/download/v2.10.20/nats-server-v2.10.20-linux-amd64.tar.gz | tar xz sudo mv nats-server-v2.10.20-linux-amd64/nats-server /usr/local/bin/ nats-server -c ~/.config/nats/nats.conf & ``` --- ### 4.2 `mosquitto` ๋ฐฐํฌ (๋Œ€์•ˆ) #### Docker ์‹คํ–‰: ```bash docker run -d \ --name mam-mosquitto \ --restart unless-stopped \ -p 1883:1883 \ -v ./mosquitto.conf:/mosquitto/config/mosquitto.conf \ eclipse-mosquitto:latest ``` **๊ธฐ๋ณธ `mosquitto.conf` ์„ค์ • ํŒŒ์ผ ์˜ˆ์‹œ:** ```conf listener 1883 allow_anonymous true persistence true persistence_location /mosquitto/data/ ``` --- ## 5. ํ•˜๋‚˜์˜ ์„œ๋ฒ„๋กœ ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ โ€” `nats-server` ๋‹ค๋Šฅ์„ฑ (Versatility) `nats-server`์˜ ๋‹ค๋Šฅ์„ฑ์€ **MAM์„ ๋„ค์ดํ‹ฐ๋ธŒ NATS๋กœ ์ด๊ด€ํ•  ์ด์œ ๊ฐ€ ์•„๋‹ˆ๋ผ, MAM ์ฝ”๋“œ๋ฅผ ํ•œ ์ค„๋„ ๋ฐ”๊พธ์ง€ ์•Š๊ณ ๋„ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ๋ถ€๊ฐ€์  ์ด๋“**์ž…๋‹ˆ๋‹ค. ### 5.1 ๋‘ ๊ฐœ์˜ ์†Œ๋น„ ํ‰๋ฉด ๋ฐ ์„ธ ๊ฐ€์ง€ ์ ‘์† ๊ฒฝ๋กœ (Consumption Planes & Transport Paths) `nats-server`๋Š” ๋‹จ์ผ ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ ์—ฌ๋Ÿฌ ํ”„๋กœํ† ์ฝœ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋™์‹œ์— ๊ตฌ๋™ํ•˜๋ฏ€๋กœ, MAM์˜ ๋‹จ์ˆœ์„ฑ๊ณผ ๊ฐœ์ธ ํ™ˆ๋žฉ์˜ ํ™•์žฅ์„ฑ์„ ์™„๋ฒฝํžˆ ์–‘๋ฆฝ์‹œํ‚ต๋‹ˆ๋‹ค. ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ nats-server (๋‹จ์ผ ์ธ์Šคํ„ด์Šค) โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ํ‰๋ฉด A: MAM ์›Œํฌ๋กœ๋“œ โ”‚ ํ‰๋ฉด B: ํ™ˆ๋žฉ/๊ฐœ์ธ ํ”„๋กœ์ ํŠธ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค ํ”„๋กœํ† ์ฝœ โ”‚ MQTT 3.1.1 (ํฌํŠธ 1883) โ”‚ NATS(4222), WebSocket(8080) โ”‚ ํด๋ผ์ด์–ธํŠธ โ”‚ paho-mqtt (์ฝ”๋“œ ๋ณ€๊ฒฝ 0์ค„) โ”‚ nats-py, nats.js, MQTT.js ๋“ฑโ”‚ ์‚ฌ์šฉ ๊ธฐ๋Šฅ โ”‚ QoS 1, Retain, ์™€์ผ๋“œ์นด๋“œ, TLSโ”‚ JetStream ๋ฆฌํ”Œ๋ ˆ์ด, KV, Objectโ”‚ ์„ค๊ณ„ ์›์น™ โ”‚ ์ดˆ๊ฒฝ๋Ÿ‰ ๋™๊ธฐ CLI ํ•ซํŒจ์Šค ๋ณด์กด โ”‚ ๊ณ ๊ธ‰ ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ โ”‚ ๊ณต์œ  ์ž์› โ”‚ โ””โ”€โ”€โ”€โ”€โ”€ ๋‹จ์ผ ์ •์  ๋ฐ”์ด๋„ˆ๋ฆฌ / JetStream ์Šคํ† ๋ฆฌ์ง€ / ACL โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ํŠนํžˆ **WebSocket ํฌํŠธ(8080)**๋Š” ์ ‘์† ๊ฒฝ๋กœ(URL Path)์— ๋”ฐ๋ผ ๋‘ ๊ฐ€์ง€ ํ”„๋กœํ† ์ฝœ์„ ๋™์‹œ์— ์„œ๋น™ํ•ฉ๋‹ˆ๋‹ค: 1. **MQTT 1883**: TCP ๋„ค์ดํ‹ฐ๋ธŒ MQTT 3.1.1 (MAM CLI ์—์ด์ „ํŠธ ํ‘œ์ค€ ๊ฒฝ๋กœ) 2. **WebSocket 8080 (`/mqtt` ๊ฒฝ๋กœ)**: **MQTT-over-WebSocket** (`MQTT.js` ๋“ฑ์œผ๋กœ ์ ‘์†). ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ 1883 ๋ฆฌ์Šค๋„ˆ์™€ ๋™์ผํ•˜๊ฒŒ ์ทจ๊ธ‰๋˜์–ด **retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์™„์ „ํ•˜๊ฒŒ ์ˆ˜์‹ **ํ•ฉ๋‹ˆ๋‹ค (N-7). 3. **WebSocket 8080 (`/` ๋˜๋Š” ๊ฒฝ๋กœ ์—†์Œ)**: **NATS ๋„ค์ดํ‹ฐ๋ธŒ WebSocket** (`nats.ws` ๋“ฑ์œผ๋กœ ์ ‘์†). ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆผ๋งŒ ์ˆ˜์‹ ํ•˜๋ฉฐ retained ๋ฉ”์‹œ์ง€๋Š” ์ˆ˜์‹ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (N-1). ### 5.2 ๊ต์ฐจ ํ”„๋กœํ† ์ฝœ ๋ธŒ๋ฆฌ์ง• (Cross-Protocol Bridging) - `nats-server`๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ MQTT ํ† ํ”ฝ(`/`)์„ NATS Subject(`.`)๋กœ ์‹ค์‹œ๊ฐ„ ์ž๋™ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. - MAM ์—์ด์ „ํŠธ๊ฐ€ MQTT ํ† ํ”ฝ `python/mqtt/jobs//events`๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด, ์›น ๋ธŒ๋ผ์šฐ์ €๋‚˜ ํƒ€ ํ”„๋กœ์ ํŠธ์˜ NATS ๊ตฌ๋…์ž๋Š” NATS Subject `python.mqtt.jobs..events` ๋˜๋Š” `python.mqtt.jobs.*.events`๋กœ ์ฆ‰์‹œ ์‹ค์‹œ๊ฐ„ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - **์‹ค์šฉ์  ์ด์ **: MAM ์†Œ์Šค ์ฝ”๋“œ๋ฅผ ๋‹จ 1์ค„๋„ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ ๋„ React/Vue ์›น ๋Œ€์‹œ๋ณด๋“œ๋‚˜ ํ„ฐ๋ฏธ๋„ ๋ชจ๋‹ˆํ„ฐ๋ง ํˆด์„ ์ฆ‰์‹œ ๋ถ€์ฐฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - **๊ฒฝ๊ณ„ (ํ•„์ˆ˜ ์ธ์ง€ โ€” N-1 & N-7)**: - NATS ๋„ค์ดํ‹ฐ๋ธŒ(๊ฒฝ๋กœ ์—†๋Š” WebSocket ๋˜๋Š” 4222)๋กœ ์ ‘์†ํ•˜๋Š” ๊ฒฝ์šฐ: ๊ต์ฐจ ํ”„๋กœํ† ์ฝœ ๋ธŒ๋ฆฌ์ง•์€ **๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ์— ํ•œ์ •**๋ฉ๋‹ˆ๋‹ค. ์žก์ด ๋๋‚œ ๋’ค ์ ‘์†ํ•œ ๋„ค์ดํ‹ฐ๋ธŒ NATS ๋Œ€์‹œ๋ณด๋“œ๋Š” ๊ทธ ์žก์˜ **์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค** (retained ๋ฉ”์‹œ์ง€๋Š” MQTT SUBSCRIBE ๊ฒฝ๋กœ ์ „์šฉ). - ์›น ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์‚ฌํ›„ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๊นŒ์ง€ ๋ฌด์ƒ์œผ๋กœ ์ˆ˜์‹ ํ•˜๋ ค๋ฉด **`ws://:8080/mqtt` ๊ฒฝ๋กœ๋กœ `MQTT.js` ํด๋ผ์ด์–ธํŠธ๋ฅผ ์—ฐ๊ฒฐ**ํ•˜์‹ญ์‹œ์˜ค (N-7). ๋ฆฌํ”Œ๋ ˆ์ด ์ŠคํŠธ๋ฆผ ๊ตฌ์ถ• ๋ฐ ๋””์Šคํฌ ๊ด€๋ฆฌ ๋ถ€๋‹ด ์—†์ด ์™„์ „ํ•œ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ์ฆ‰์‹œ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - **๊ณ„์ • ๋ฐฐ์น˜**: ๊ด€์ธก ํด๋ผ์ด์–ธํŠธ๋Š” ยง5.5 ์ฒ˜๋ฐฉ๋Œ€๋กœ `MAM` ๊ณ„์ • ์•ˆ์˜ ์ฝ๊ธฐ ์ „์šฉ ์‚ฌ์šฉ์ž(`mam_observer`)๋กœ ์ ‘์†ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ณ„์ •์—์„œ๋Š” subject๊ฐ€ ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. - **์ฃผ์˜ ์‚ฌํ•ญ**: ํ† ํ”ฝ ๋ ˆ๋ฒจ ๋‚ด์— ๋งˆ์นจํ‘œ(`.`)๊ฐ€ ํฌํ•จ๋˜๋ฉด NATS ๊ณ„์ธต์—์„œ ํ† ํฐ์ด ๋ถ„๋ฆฌ๋  ์ˆ˜ ์žˆ์œผ๋‚˜, MAM์˜ `job_id`๋Š” 8์ž๋ฆฌ hex, ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ง€๋ฌธ์€ 12์ž๋ฆฌ hex์ด๋ฏ€๋กœ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ### 5.3 JetStream ์ด๋ฒคํŠธ ๋ฆฌํ”Œ๋ ˆ์ด (Event Replay) - `python.mqtt.jobs.>` Subject๋ฅผ ๊ตฌ๋…ํ•˜๋Š” JetStream ์ŠคํŠธ๋ฆผ์„ ์ƒ์„ฑํ•˜๋ฉด, ์ง€๋‚œ ์ž‘์—…์˜ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ์ „์ฒด๋ฅผ ์‹œ์  ์ง€์ •(Time-based) ๋˜๋Š” ์‹œํ€€์Šค ์ง€์ •(Sequence-based)์œผ๋กœ ์‚ฌํ›„ ๋ฆฌํ”Œ๋ ˆ์ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - **์ฃผ์˜ ์‚ฌํ•ญ**: ์ด ๊ธฐ๋Šฅ์€ ์˜ตํŠธ์ธ(Opt-in)์ด๋ฉฐ, MQTT QoS 1 ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋‚ด๋ถ€ ์‹œ์Šคํ…œ ์ŠคํŠธ๋ฆผ(`$MQTT_*`)๊ณผ ๋ณ„๊ฐœ๋กœ ๊ด€๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ๋””์Šคํฌ ์šฉ๋Ÿ‰ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด `max_age`๋‚˜ `max_bytes` ์ƒํ•œ์„ ๋ฐ˜๋“œ์‹œ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ### 5.4 ๋‚ด์žฅ Key-Value (KV) ๋ฐ Object Store - ํ™ˆ๋žฉ ๋ฐ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์—์„œ Redis๋‚˜ MinIO ๊ฐ™์€ ๋ณ„๋„ ์ธํ”„๋ผ๋ฅผ ๋„์šฐ์ง€ ์•Š๊ณ ๋„ `nats-server` ๋‚ด์žฅ KV ๋ฐ Object Store๋ฅผ ์ฆ‰์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - **๊ธˆ์ง€ ์‚ฌํ•ญ (Non-Goal)**: MAM์˜ ๋กœ์ปฌ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ(`.mam/jobs/*.json`)๋ฅผ JetStream KV๋กœ ๋Œ€์ฒดํ•ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค (`wait_for_job`์˜ fcntl ๋ฐ ํŒŒ์ผ์‹œ์Šคํ…œ ํด๋ง ๊ณ„์•ฝ ์œ ์ง€). ### 5.5 ๋ฉ€ํ‹ฐํ…Œ๋„ŒํŠธ ๊ณ„์ • ๋ถ„๋ฆฌ ๋ฐ ๋ณด์•ˆ - ๋‹จ์ผ ์„œ๋ฒ„ ๋‚ด์—์„œ `MAM` ์ „์šฉ ๊ณ„์ •๊ณผ `HOME` ๊ฐœ์ธ ๊ณ„์ •์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ฆฌ์†Œ์Šค ์ฟผํ„ฐ์™€ ๊ถŒํ•œ์„ ์™„๋ฒฝํžˆ ๊ฒฉ๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. - **๊ถŒ๊ณ  ๋ฐฐ์น˜**: MAM๊ณผ ์ด๋ฅผ ๊ด€์ธกํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋™์ผํ•œ ๊ณ„์ •(`MAM`)์— ๋ฐฐ์น˜ํ•˜๊ณ (๊ด€์ธก์ž๋Š” ์ฝ๊ธฐ ์ „์šฉ `mam_observer` ์—ญํ•  ๋ถ€์—ฌ), ๋ฌด๊ด€ํ•œ ํ™ˆ๋žฉ ์„œ๋น„์Šค๋Š” ๋ณ„๋„ ๊ณ„์ •(`HOME`)์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค. --- ## 6. MAM ํด๋ผ์ด์–ธํŠธ ์—ฐ๋™ ์„ค์ • (`.mam.env`) ๊ฐœ์ธ ์„œ๋ฒ„ ๋ธŒ๋กœ์ปค๊ฐ€ ๊ตฌ๋™๋˜๋ฉด, MAM ์ €์žฅ์†Œ ๋ฃจํŠธ์˜ [`.mam.env`](file:///.mam.env) ํŒŒ์ผ์— ๊ฐœ์ธ ์„œ๋ฒ„ ์ฃผ์†Œ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. > [!NOTE] > MAM ์ฝ”๋“œ(`mqtt_common.py`)๋Š” `MQTT_*` ์ ‘๋‘์‚ฌ์˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. ์ด์ „ ๋น„๊ณต์‹ ๋ฌธ์„œ์˜ `MAM_MQTT_*` ๋ณ€์ˆ˜๋Š” ๋ฌดํšจํ•˜๋ฏ€๋กœ ๋ฐ˜๋“œ์‹œ ์•„๋ž˜์˜ ํ‘œ์ค€ ๋ณ€์ˆ˜๋ช…์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ```bash # ============================================================================== # MAM Private MQTT Broker Configuration (.mam.env) # ============================================================================== # โ”€โ”€ ๋ชจ๋ธ T (Tailscale ์‚ฌ์„ค ์˜ค๋ฒ„๋ ˆ์ด๋ง ๊ถŒ์žฅ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ MQTT_BROKER="mam-hub.tailXXXX.ts.net" # ๋˜๋Š” 100.x.y.z (ํ‰๋ฌธ์ด๋ฏ€๋กœ IP ์‚ฌ์šฉ ๊ฐ€๋Šฅ) MQTT_PORT=1883 MQTT_TLS=0 MQTT_USERNAME=mam_agent MQTT_PASSWORD=replace_me_with_token MQTT_KEEPALIVE=60 # WAN ๊ตฌ๊ฐ„ ๊ถŒ์žฅ ์—ฐ๊ฒฐ ์œ ์ง€ ์‹œ๊ฐ„ # โ”€โ”€ ๋ชจ๋ธ P (๊ณต๊ฐœ TLS / Let's Encrypt ๋ชจ๋ธ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # MQTT_BROKER="mam-broker.example.com" # D-4: ๋ฐ˜๋“œ์‹œ ์ธ์ฆ์„œ SAN ์˜ DNS ์ด๋ฆ„ (IP ๊ธˆ์ง€) # MQTT_PORT=8883 # MQTT_TLS=1 # MQTT_USERNAME=mam_agent # MQTT_PASSWORD=replace_me_with_token # MQTT_CA_CERTS ๋Š” Let's Encrypt ์‚ฌ์šฉ ์‹œ '์„ค์ •ํ•˜์ง€ ์•Š์Œ' (์‹œ์Šคํ…œ ์‹ ๋ขฐ ์ €์žฅ์†Œ ์‚ฌ์šฉ) ``` *์ฐธ๊ณ : OS ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ๋™์ผํ•œ ์ด๋ฆ„์ด ์ด๋ฏธ `export`๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ OS ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ `.mam.env` ํŒŒ์ผ ์„ค์ •๋ณด๋‹ค ์šฐ์„ ํ•ฉ๋‹ˆ๋‹ค.* --- ## 7. ์—ฐ๋™ ๋ฐ ๋™์ž‘ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ (4-Step Verification) ๊ฐœ์ธ ์„œ๋ฒ„ ๋ธŒ๋กœ์ปค์™€์˜ ์—ฐ๋™ ์ƒํƒœ๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ๊ฒ€์ฆํ•˜๋Š” 4๋‹จ๊ณ„ ์ ˆ์ฐจ์ž…๋‹ˆ๋‹ค. ### Step 1. ๋ธŒ๋กœ์ปค ๋ฆฌ์Šค๋„ˆ ๋ฐ JetStream ์ƒํƒœ ํ™•์ธ ```bash # MQTT ๋ฆฌ์Šค๋„ˆ ํ™œ์„ฑํ™” ํ™•์ธ curl -s http://127.0.0.1:8222/varz | grep -i mqtt # JetStream ์—”์ง„ ์ •์ƒ ๊ตฌ๋™ ํ™•์ธ curl -s http://127.0.0.1:8222/jsz ``` ### Step 2. ์ž„์‹œ ์žก ๋“ฑ๋ก ๋ฐ ์—ฐ๊ฒฐ ๊ฒ€์ฆ ์ด๋ฒคํŠธ ๋ฐœํ–‰ `publish_event.py`๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“ฑ๋ก๋œ ์žก์— ๋Œ€ํ•ด์„œ๋งŒ ๋ฐœํ–‰์„ ์ˆ˜ํ–‰ํ•˜๋ฏ€๋กœ, ์ž„์‹œ ์žก์„ ๋“ฑ๋กํ•˜๊ณ  ๋ฐœํ–‰ํ•œ ํ›„ ์™„๋ฃŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ```bash # 1) ์ž„์‹œ ์žก ๋“ฑ๋ก (์ž๋™ ์ฑ„๋ฒˆ๋œ JID ์บก์ฒ˜) JID=$(.venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \ --registry-dir .mam/jobs \ register \ --prompt "Private broker connectivity test" \ --agent-session "herdr:test") echo "registered test job: $JID" # 2) ์ด๋ฒคํŠธ ๋ฐœํ–‰ (์ƒ์„ธ ๋กœ๊ทธ ์ถœ๋ ฅ ๋ฐ rc=0 ๋‹จ์–ธ) .venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/publish_event.py \ --registry-dir .mam/jobs \ --job "$JID" \ --event progress \ --detail "Private broker connection verified" -v # 3) ํ…Œ์ŠคํŠธ ์žก ์ข…๊ฒฐ ์ฒ˜๋ฆฌ (๋ฏธ์ข…๊ฒฐ ์‹œ --wait-any ์œ ๋ น ์žก ์ž”์กด ๋ฐฉ์ง€) .venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py \ --registry-dir .mam/jobs status --job "$JID" --set completed ``` ### Step 3. ์ ‘์† ๋Œ€์ƒ ๋ธŒ๋กœ์ปค IP ๋‹จ์–ธ Step 2์˜ `-v` ์ถœ๋ ฅ ๋กœ๊ทธ ๋˜๋Š” `.mam/delegate_job_logs/$JID/events.ndjson` ํŒŒ์ผ์—์„œ ์‹ค์ œ ์ ‘์† ํ˜ธ์ŠคํŠธ๊ฐ€ ๊ฐœ์ธ ๋ธŒ๋กœ์ปค IP๋กœ ๋‚˜ํƒ€๋‚˜๊ณ  `broker.hivemq.com`์ด ํฌํ•จ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ### Step 4. ๋‹จ์œ„ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ๊ฒ€์ฆ ```bash .venv/bin/python -m pytest tests/ -q ``` *์ฐธ๊ณ : MAM์˜ ๊ธฐ๋ณธ ๋‹จ์œ„/์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋Š” ๋ชจ์˜(Mock) ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ์—ฌ๋ถ€์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ 100% ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์—ฐ๋™ ๊ฒ€์ฆ์€ Step 1~3์ด ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.* --- ## 8. ๊ถŒ์žฅ ์‹คํ–‰ ์ˆœ์„œ ``` [Phase 1: ๋‚ด๊ฒฐํ•จ์„ฑ ํ™•๋ณด] โ”€โ”€> [Phase 2: ๊ฐœ์ธ ๋ธŒ๋กœ์ปค ๊ฐ€๋™] โ”€โ”€> [Phase 3: A-2 ๋ณด์•ˆ ์™„์ „ ์ข…๊ฒฐ] Track 0 (B-14, B-15) nats-server (nats.conf) ์ง€๋ฌธ ํ† ํ”ฝ ๋ฐ ์ธ์ฆ ํ† ํฐ ๋ฐœ๊ธ‰ ๋กœ์ปฌ ๋””์Šคํฌ ํด๋ฐฑ ํŒจ์น˜ .mam.env ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์—ฐ๋™ ์™ธ๋ถ€ ๊ฐ„์„ญ 100% ์ฐจ๋‹จ ``` 1. **Phase 1 (Track 0 ์„ ํ–‰ ํŒจ์น˜)**: `publish_event.py`์™€ `job_subscriber.py`์˜ ๋กœ์ปฌ ๋””์Šคํฌ ํด๋ฐฑ(`B-14`, `B-15`)์„ ๋จผ์ € ์ ์šฉํ•˜์—ฌ ๋ธŒ๋กœ์ปค ๋‹ค์šด ์‹œ์—๋„ ๋ฃจํ”„๊ฐ€ ๋ฉˆ์ถ”์ง€ ์•Š๋Š” ๋ฐฉํƒ„ ๊ตฌ์กฐ๋ฅผ ํ™•๋ฆฝํ•ฉ๋‹ˆ๋‹ค. 2. **Phase 2 (๊ฐœ์ธ ๋ธŒ๋กœ์ปค ๊ฐ€๋™)**: ๊ฐœ์ธ ์„œ๋ฒ„์— `nats-server -c nats.conf`๋ฅผ ๊ตฌ๋™ํ•˜๊ณ  `.mam.env`์— `MQTT_BROKER`๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. 3. **Phase 3 (A-2 ๋ณด์•ˆ ์™„์ „ ์ข…๊ฒฐ)**: ์›Œํฌ์ŠคํŽ˜์ด์Šค ์ง€๋ฌธ ํ† ํ”ฝ(`mam//jobs/...`) ๋ฐ ๋ฌด์กฐ๊ฑด ํ† ํฐ ๋ฐœ๊ธ‰์„ ์ ์šฉํ•˜์—ฌ ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์œ„ํ—˜์„ ์™„์ „ํžˆ ์˜๊ตฌ ํ๊ธฐํ•ฉ๋‹ˆ๋‹ค. --- ## 9. ์›๊ฒฉ ์„œ๋ฒ„ `nats-server` Docker ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๊ฐ€์ด๋“œ (Track 1R) ์›๊ฒฉ VPS ๋˜๋Š” ์ƒ์‹œ ๊ฐ€๋™ ํ™ˆ๋žฉ ์„œ๋ฒ„์— ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€์˜ `nats-server`๋ฅผ Docker ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์ถ•ํ•˜๊ณ  MAM๊ณผ ์—ฐ๋™ํ•˜๋Š” ํ‘œ์ค€ ์ ˆ์ฐจ์ž…๋‹ˆ๋‹ค. > [!NOTE] > **์ •๋ณธ ์ž์‚ฐ ์•ˆ๋‚ด**: ๋ณธ ์ ˆ์˜ ์„ค์ •์€ [`docker/`](docker/) ๋””๋ ‰ํ„ฐ๋ฆฌ์— ์ •๋ณธ(canonical) ํŒŒ์ผ(`docker/docker-compose.yaml`, `docker/nats.conf`, `docker/.env.example`, `docker/README.md`)๋กœ ๊ด€๋ฆฌ๋˜๋ฉฐ, ์•„๋ž˜ ์ฝ”๋“œ ํŽœ์Šค๋Š” ๊ทธ ์‚ฌ๋ณธ์ž…๋‹ˆ๋‹ค. ๋‘ ๊ณณ์ด ์–ด๊ธ‹๋‚˜๋ฉด `tests/test_deploy_freshness.py`์˜ D-22 ~ D-30 ํšŒ๊ท€ ๊ฐ€๋“œ๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ### 9.1 ํ”„๋กœ๋•์…˜ `nats.conf` ```conf # ============================================================================== # docker/nats.conf โ€” MAM ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ๋ธŒ๋กœ์ปค (Track 1R) # # ์ •๋ณธ ๋ฌธ์„œ: PRIVATE_SERVER.md ยง9.1 # ์‹œํฌ๋ฆฟ: ์ด ํŒŒ์ผ์—๋Š” ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  password ๋Š” docker/.env โ†’ compose # environment โ†’ ์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…๋˜๋Š” $VAR ์ฐธ์กฐ์ž…๋‹ˆ๋‹ค. # # โš  ์•”ํ˜ธ ๋ฌธ์ž ์ œ์•ฝ: NATS ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์„ ์ž์ฒด ์„ค์ • ๋ ‰์„œ๋กœ ์žฌํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. # ์•”ํ˜ธ์— [๊ณต๋ฐฑ ; , ] } # ' " $] ๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ์„ค์ •์ด ๊นจ์ง€๊ฑฐ๋‚˜ ๋‹ค๋ฅด๊ฒŒ ํ•ด์„๋ฉ๋‹ˆ๋‹ค. # ๋ฐ˜๋“œ์‹œ `openssl rand -base64 32` (์•ŒํŒŒ๋ฒณ A-Za-z0-9+/=) ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. # ============================================================================== server_name: mam-hub # โ”€โ”€ JetStream: MQTT retained/QoS1 ์ €์žฅ์†Œ. ์ข…๋ฃŒ ์ด๋ฒคํŠธ ์žฌ์ˆ˜์‹ ์ด ์—ฌ๊ธฐ์— ์˜์กด โ”€โ”€ jetstream { store_dir: "/data" # ์ ˆ๋Œ€๊ฒฝ๋กœ ๊ณ ์ •. '~' ๋„ ์ธ์šฉ๋œ "$HOME" ๋„ ํ™•์žฅ๋˜์ง€ ์•Š์Œ max_file: 10G # ์ ‘๋ฏธ์‚ฌ๋Š” ๋Œ€๋ฌธ์ž๋งŒ ์œ ํšจ (K/M/G/T) max_mem: 256M } http_port: 8222 # ๋ฌด์ธ์ฆ ๋ชจ๋‹ˆํ„ฐ๋ง โ†’ ํ˜ธ์ŠคํŠธ ๊ฒŒ์‹œ๋Š” loopback ํ•œ์ • (compose) mqtt { port: 1883 ack_wait: 60s # WAN RTT ํก์ˆ˜ (๊ธฐ๋ณธ 30s) max_ack_pending: 1024 # ๋‹ค์ค‘ ์—์ด์ „ํŠธ ๋™์‹œ ๋ฐœํ–‰ ์—ฌ์œ  (์ƒํ•œ 65535) } websocket { port: 8080 no_tls: true # ์‚ฌ์„ค๋ง/tailnet ํ•œ์ •. ์ƒ๋žตํ•˜๋ฉด TLS ์„ค์ • ํ•„์ˆ˜๋ผ ๊ธฐ๋™ ์‹คํŒจ # โ”€โ”€ ์›์ (Origin) ์ •์ฑ… โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๊ธฐ๋ณธ๊ฐ’์€ ์ด๋ฏธ '๋ชจ๋“  ์ถœ์ฒ˜ ํ—ˆ์šฉ'์ž…๋‹ˆ๋‹ค. same_origin ์˜ ๊ธฐ๋ณธ๊ฐ’์€ false ์ด๊ณ  # allowed_origins ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด checkOrigin() ์ด Origin ํ—ค๋”๋ฅผ ์ฝ์ง€๋„ ์•Š๊ณ  # ์ฆ‰์‹œ nil ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค (server/websocket.go:1039). # โ†’ http://localhost:3000 ์˜ ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋ณ„๋„ ์„ค์ • ์—†์ด ์ ‘์†๋ฉ๋‹ˆ๋‹ค. # โ†’ `same_origin: false` ๋ฅผ ์ ๋Š” ๊ฒƒ์€ no-op ์ž…๋‹ˆ๋‹ค. # # 8080 ์„ tailnet ๋ฐ–์œผ๋กœ ๋…ธ์ถœํ•œ๋‹ค๋ฉด ์•„๋ž˜๋ฅผ ์ผœ์„œ ์ถœ์ฒ˜๋ฅผ ์ขํžˆ์‹ญ์‹œ์˜ค. # ์ฃผ์˜: allowed_origins ๋ฅผ ๋น„์šฐ์ง€ ์•Š๋Š” ์ˆœ๊ฐ„ ์›์  ๊ฒ€์‚ฌ๊ฐ€ '์ผœ์ง‘๋‹ˆ๋‹ค'. # "*" ๋Š” ์ ˆ๋Œ€ ์“ฐ์ง€ ๋งˆ์‹ญ์‹œ์˜ค โ€” ์˜ต์…˜ ๊ฒ€์ฆ ์‹คํŒจ๋กœ ์„œ๋ฒ„๊ฐ€ ๊ธฐ๋™ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค # (websocket.go:1142 "must be absolute URLs with http or https scheme"). # allowed_origins: ["https://dashboard.example", "http://localhost:3000"] # โ”€โ”€ ์ด ํฌํŠธ๋Š” MQTT-over-WebSocket ๋„ ์„œ๋น™ํ•ฉ๋‹ˆ๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๊ฒฝ๋กœ /mqtt ๋กœ ๋ถ™์œผ๋ฉด ์™„์ „ํ•œ MQTT ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค (mqtt.go:193, # websocket.go:1335 โ†’ createMQTTClient, 1883 ๋ฆฌ์Šค๋„ˆ์™€ ๋™์ผ ํ•จ์ˆ˜). # ws://:8080/mqtt โ†’ MQTT. retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. # ws://:8080/ โ†’ NATS ๋„ค์ดํ‹ฐ๋ธŒ. retained ๋ฅผ ๋ฐ›์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค (N-1). # ๋ธŒ๋ผ์šฐ์ € ๋Œ€์‹œ๋ณด๋“œ๋Š” MQTT.js ๋กœ /mqtt ์— ๋ถ™์ด๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. } # โ”€โ”€ ์ธ์ฆ ๋ฐ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ accounts { MAM: { jetstream: enabled # MQTT ๋‚ด๋ถ€ ์ŠคํŠธ๋ฆผ์ด ์ด ๊ณ„์ • ์•ˆ์— ์ƒ์„ฑ๋จ users: [ # ๋ฐœํ–‰์ž ๊ฒธ ๊ตฌ๋…์ž โ€” MAM ์—์ด์ „ํŠธ ๋ณธ์ฒด (.mam.env ์˜ MQTT_USERNAME) { user: mam_agent, password: $MAM_BROKER_PASS } # ๊ด€์ธก์ž โ€” ๋Œ€์‹œ๋ณด๋“œ/๋ชจ๋‹ˆํ„ฐ๋ง. ๋ฐ˜๋“œ์‹œ MAM ๊ณ„์ • ์•ˆ์— ์œ„์น˜ { user: mam_observer, password: $MAM_OBSERVER_PASS, permissions: { subscribe: { allow: ["python.mqtt.jobs.>"] } # M3 ์ดํ›„: "mam..jobs.>" publish: { deny: [">"] } } } ] } # MAM ๊ณผ ๋ฌด๊ด€ํ•œ ํ™ˆ๋žฉ ์„œ๋น„์Šค ์ „์šฉ. MAM subject ๋Š” ๋ณด์ด์ง€ ์•Š์Œ(์˜๋„๋œ ๊ฒฉ๋ฆฌ) HOME: { jetstream: enabled, users: [ { user: home, password: $HOME_BROKER_PASS } ] } SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] } } system_account: SYS ``` > [!IMPORTANT] > **๊ด€์ธก์ž๋Š” ๋ฐ˜๋“œ์‹œ `MAM` ๊ณ„์ • ์•ˆ์— ๋‘ก๋‹ˆ๋‹ค.** NATS ๊ณ„์ •์€ ํ•˜๋“œ ๊ฒฉ๋ฆฌ ๊ฒฝ๊ณ„์ด๋ฏ€๋กœ `user: home`(๊ณ„์ • `HOME`)์œผ๋กœ ์ ‘์†ํ•œ ํด๋ผ์ด์–ธํŠธ๋Š” `python.mqtt.jobs.>`๋ฅผ ๊ตฌ๋…ํ•ด๋„ **0๊ฑด**์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์„œ๋กœ ๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ์ด๋ผ ๊ณ„์ •์„ ๋ฐ˜๋“œ์‹œ ๊ฐˆ๋ผ์•ผ ํ•˜๋Š” ์˜ˆ์™ธ ์ƒํ™ฉ์€ **๋ถ€๋ก X(Option A)** ๋ฅผ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค. ### 9.2 ํ”„๋กœ๋•์…˜ `docker/docker-compose.yaml` ```yaml # ============================================================================== # docker/docker-compose.yaml โ€” MAM ์›๊ฒฉ ํ”„๋กœ๋•์…˜ ๋ธŒ๋กœ์ปค (Track 1R) # ์ •๋ณธ ๋ฌธ์„œ: PRIVATE_SERVER.md ยง9.2 # # ์‚ฌ์šฉ๋ฒ•: cd docker && cp .env.example .env && <์‹œํฌ๋ฆฟ ์ฑ„์šฐ๊ธฐ> && docker compose up -d # ============================================================================== services: nats: image: nats:2.12-alpine # alpine ํ•„์ˆ˜: healthcheck ์˜ wget ์ด ์—ฌ๊ธฐ์—๋งŒ ์žˆ์Œ container_name: mam-nats restart: unless-stopped command: ["-c", "/etc/nats/nats.conf"] environment: # ๋ฏธ์„ค์ •/๋นˆ ๊ฐ’์ด๋ฉด ์ปจํ…Œ์ด๋„ˆ ์ƒ์„ฑ ์ „์— compose ๊ฐ€ ์ค‘๋‹จ โ†’ fail-closed MAM_BROKER_PASS: ${MAM_BROKER_PASS:?set MAM_BROKER_PASS in docker/.env} MAM_OBSERVER_PASS: ${MAM_OBSERVER_PASS:?set MAM_OBSERVER_PASS in docker/.env} HOME_BROKER_PASS: ${HOME_BROKER_PASS:?set HOME_BROKER_PASS in docker/.env} SYS_BROKER_PASS: ${SYS_BROKER_PASS:?set SYS_BROKER_PASS in docker/.env} ports: # โš  Docker ์˜ published ํฌํŠธ๋Š” UFW ๋ฅผ ์šฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๋…ธ์ถœ ํ†ต์ œ๋Š” ๋ฐฉํ™”๋ฒฝ์ด ์•„๋‹ˆ๋ผ # ์—ฌ๊ธฐ์˜ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๊ฐ€ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ ์ „๋ถ€ loopback. - "${MQTT_BIND:-127.0.0.1}:1883:1883" # MQTT 3.1.1 (ํ‰๋ฉด A: MAM) - "${NATS_BIND:-127.0.0.1}:4222:4222" # NATS ๋„ค์ดํ‹ฐ๋ธŒ (ํ‰๋ฉด B) - "127.0.0.1:8222:8222" # ๋ฌด์ธ์ฆ ๋ชจ๋‹ˆํ„ฐ๋ง โ€” loopback ๊ณ ์ • - "${WS_BIND:-127.0.0.1}:8080:8080" # WebSocket (NATS + /mqtt ๊ฒฝ๋กœ์˜ MQTT) volumes: - ./nats.conf:/etc/nats/nats.conf:ro - nats-data:/data # nats.conf ์˜ store_dir ์™€ ์ผ์น˜ healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"] interval: 30s timeout: 5s retries: 3 start_period: 20s logging: driver: json-file options: { max-size: "10m", max-file: "3" } volumes: nats-data: ``` > [!WARNING] > **Docker์˜ published ํฌํŠธ๋Š” UFW๋ฅผ ์šฐํšŒํ•ฉ๋‹ˆ๋‹ค.** Docker๊ฐ€ ์‚ฝ์ž…ํ•˜๋Š” NAT/FORWARD ๊ทœ์น™์ด `ufw`์˜ INPUT ์ฒด์ธ๋ณด๋‹ค ๋จผ์ € ํ‰๊ฐ€๋˜๋ฏ€๋กœ, `ufw deny 1883`์„ ๊ฑธ์–ด๋„ `-p 1883:1883`์œผ๋กœ ๊ฒŒ์‹œํ•œ ํฌํŠธ๋Š” ์ธํ„ฐ๋„ท์— ์—ด๋ฆฝ๋‹ˆ๋‹ค. ๋ณธ ๊ตฌ์„ฑ์ด ๋ฐฉํ™”๋ฒฝ์—๋งŒ ์˜์กดํ•˜์ง€ ์•Š๊ณ  **published ํฌํŠธ ์ž์ฒด์— ๋ฐ”์ธ๋“œ ์ฃผ์†Œ๋ฅผ ๋ช…์‹œ**(`${MQTT_BIND}:1883:1883`)ํ•˜๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค. ### 9.3 ์›๊ฒฉ ๋„คํŠธ์›Œํ‚น & ๋ณด์•ˆ ๊ฐ€์ด๋“œ | ํ•ญ๋ชฉ | ๐Ÿ† **๋ชจ๋ธ T: Tailscale/WireGuard ์˜ค๋ฒ„๋ ˆ์ด (๊ถŒ์žฅ)** | ๋ชจ๋ธ P: ๊ณต๊ฐœ TLS (Let's Encrypt) | |---|---|---| | ์ธํ„ฐ๋„ท ๋…ธ์ถœ ๋ฉด์  | **0** (๊ณต๊ฐœ ๋ฆฌ์Šค๋„ˆ ์—†์Œ) | 8883 1๊ฐœ | | ์ธ์ฆ์„œ ํ•„์š” | ๋ถˆํ•„์š” (`MQTT_TLS=0`) | ํ•„์ˆ˜ + 90์ผ ๊ฐฑ์‹  | | **D-4 ํ˜ธ์ŠคํŠธ๋ช… ์ œ์•ฝ** | **ํ•ด๋‹น ์—†์Œ** | ๋„๋ฉ”์ธ ํ•„์ˆ˜, IP ๋ถˆ๊ฐ€ | | ๋„๋ฉ”์ธ ํ•„์š” | ๋ถˆํ•„์š” | **ํ•„์ˆ˜** | | ์ด๋™์„ฑ | ์ž๋™ (Tailscale mesh) | ์ž๋™ | **๋ฐฉํ™”๋ฒฝ (UFW) ์„ค์ • (๋ชจ๋ธ T):** ```bash sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp # tailnet ์ธํ„ฐํŽ˜์ด์Šค๋งŒ ํ—ˆ์šฉ sudo ufw allow in on tailscale0 to any port 1883 proto tcp sudo ufw allow in on tailscale0 to any port 4222 proto tcp sudo ufw allow in on tailscale0 to any port 8080 proto tcp sudo ufw enable && sudo ufw status verbose ``` **์‹œํฌ๋ฆฟ ์ƒ์„ฑ ๋ฐ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฃผ์ž… (`docker/.env`):** ```bash # docker/.env.example ๋ณต์‚ฌ ๋ฐ ๊ถŒํ•œ ์ œํ•œ cd docker && cp .env.example .env && chmod 600 .env # ์‹œํฌ๋ฆฟ ์•”ํ˜ธ ์ƒ์„ฑ (openssl rand -base64 32 ์‚ฌ์šฉ) # .env ํŒŒ์ผ์„ ์—ด๊ณ  ์ƒ์„ฑ๋œ ์‹œํฌ๋ฆฟ ๋ฐ ๋ฐ”์ธ๋“œ ์ฃผ์†Œ ์ž…๋ ฅ: # MAM_BROKER_PASS=<์ƒ์„ฑ๋œ_ํ† ํฐ> # MAM_OBSERVER_PASS=<์ƒ์„ฑ๋œ_ํ† ํฐ> # HOME_BROKER_PASS=<์ƒ์„ฑ๋œ_ํ† ํฐ> # SYS_BROKER_PASS=<์ƒ์„ฑ๋œ_ํ† ํฐ> # MQTT_BIND=$(tailscale ip -4) # WS_BIND=$(tailscale ip -4) ``` ### 9.4 ์›๊ฒฉ ๊ฒ€์ฆ ํ”Œ๋ ˆ์ด๋ถ (R-1 ~ R-10) | ID | ๊ฒ€์ฆ ํ•ญ๋ชฉ | ๋ฐฉ๋ฒ• | ํ†ต๊ณผ ๊ธฐ์ค€ | |---|---|---|---| | **R-1** | ๋ธŒ๋กœ์ปค ํ—ฌ์Šค | SSH ํ„ฐ๋„ ํ›„ `curl -sf http://127.0.0.1:8222/healthz` | **HTTP 200** | | **R-2** | ๋ฆฌ์Šค๋„ˆ + TLS ์‹ ์› | `curl -s .../varz \| grep -i mqtt`; ๋ชจ๋ธ P๋Š” `openssl s_client -connect H:8883 -servername H` | MQTT ๋ฆฌ์Šค๋„ˆ ๋…ธ์ถœ, SAN์— `MQTT_BROKER` ํฌํ•จ | | **R-3** | ๋…ธ์ถœ ๋ฉด์  ๋‹จ์–ธ | ์™ธ๋ถ€ ๋ง์—์„œ `nmap -Pn -p 1883,4222,8222,8080 <๊ณต๊ฐœIP>` | ์ „๋ถ€ closed/filtered | | **R-4** | ์™•๋ณต pub/sub + ๊ณ„์ • JetStream | ์ž„์‹œ ์žก ๋“ฑ๋ก โ†’ ๊ตฌ๋…์ž ๊ธฐ๋™ โ†’ `progress` ๋ฐœํ–‰ โ†’ ์ˆ˜์‹  โ†’ ์ข…๊ฒฐ | ์ˆ˜์‹  ์„ฑ๊ณต, `JetStream not enabled for account` **๋ฏธ๋ฐœ์ƒ** | | **R-5** | **retained ์ข…๋ฃŒ ์ด๋ฒคํŠธ (MQTT)** | `--event completed` ๋ฐœํ–‰ **ํ›„** ์‹ ๊ทœ `job_subscriber.py` ๊ธฐ๋™ | ์ฆ‰์‹œ ์ตœ์ข… ์ด๋ฒคํŠธ ์ˆ˜์‹  | | **R-6** | ๋ธŒ๋กœ์ปค ์‹ ์› ๋‹จ์–ธ | `grep -c 'broker.hivemq.com' .mam/delegate_job_logs/$JID/events.ndjson` | **0**, ์›๊ฒฉ ํ˜ธ์ŠคํŠธ ๋“ฑ์žฅ | | **R-7** | freeze ํšŒ๊ท€ (H-1/H-4) | freeze ๊ฒฝ๋กœ `publish_event.py`๋ฅผ `MAM_ENV_FILE` ์—†์ด ์‹คํ–‰ / ์˜คํƒ€ ๊ฒฝ๋กœ ์‹คํ–‰ | ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค๋กœ ๋‚˜๊ฐ€์ง€ ์•Š์Œ | | **R-8** | ์ „์ฒด ํšŒ๊ท€ ์Šค์œ„ํŠธ | `.venv/bin/python -m pytest tests/ -q` | **์ „๊ฑด ํ†ต๊ณผ** | | **R-9** | **๊ด€์ธก์ž ๊ณ„์ • ๊ฒฝ๊ณ„** | `mam_observer`๋กœ NATS ๊ตฌ๋… โ†’ ์ˆ˜์‹  ํ™•์ธ. ์ด์–ด์„œ `home`์œผ๋กœ ๋™์ผ ๊ตฌ๋… | `mam_observer` **์ˆ˜์‹ **, `home` **0๊ฑด** | | **R-10** | **retained ๊ฒฝ๊ณ„ ํ™•์ธ (N-1)** | ์žก ์ข…๋ฃŒ **ํ›„** NATS ๋„ค์ดํ‹ฐ๋ธŒ ๊ตฌ๋…์ž๋ฅผ ์ƒˆ๋กœ ๋ถ™์ž„ | **0๊ฑด ์ˆ˜์‹ ** (retained๋Š” MQTT ์ „์šฉ) | **WAN ์ง€์—ฐ ์‹œ๊ฐ„ ์ธก์ • (Latency Probe):** ```bash .venv/bin/python - <<'PY' import sys, time, statistics sys.path.insert(0, '.agents/skills/multi-agent-mux-delegate-job/scripts') import mqtt_common as m cfg = m.broker_config_from_env() print(f"target: {cfg.host}:{cfg.port} tls={cfg.tls}") conn, rtt = [], [] for _ in range(5): c = m.make_client("latency", cfg) t0 = time.perf_counter(); c.connect(cfg.host, cfg.port, 10); c.loop_start() conn.append((time.perf_counter() - t0) * 1000) t1 = time.perf_counter() info = c.publish("mam/latency/probe", b"x", qos=1); info.wait_for_publish(10) rtt.append((time.perf_counter() - t1) * 1000) c.loop_stop(); c.disconnect() print(f"connect p50={statistics.median(conn):.1f}ms max={max(conn):.1f}ms") print(f"qos1 rtt p50={statistics.median(rtt):.1f}ms max={max(rtt):.1f}ms") PY ``` ### 9.5 ์ „ํ™˜(Cutover) ์ ˆ์ฐจ (H-2 ๋Œ€์‘) ``` [1 ๋“œ๋ ˆ์ธ] โ”€โ”€> [2 ์ž”์—ฌ ์Šค์บ”] โ”€โ”€> [3 .mam.env ๊ต์ฒด] โ”€โ”€> [4 R-1~R-10] โ”€โ”€> [5 ๋ ˆ๊ฑฐ์‹œ ์ฐจ๋‹จ] ``` 1. **๋“œ๋ ˆ์ธ**: ์‹ ๊ทœ ์œ„์ž„ ์ค‘๋‹จ, ์ง„ํ–‰ ์ค‘ ์žก์ด ๋ชจ๋‘ terminal ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ. 2. **์ž”์—ฌ ์Šค์บ”**: ์˜› ๋ธŒ๋กœ์ปค์— ํ•€ ๊ณ ์ •๋œ ๋ ˆ์ฝ”๋“œ ์Šค์บ” ํ›„ ์ •๋ฆฌ. 3. **`.mam.env` ๊ต์ฒด**: ์›๊ฒฉ ๋ธŒ๋กœ์ปค ์ฃผ์†Œ ๋ฐ ํ† ํฐ ์ ์šฉ. 4. **๊ฒ€์ฆ**: R-1 ~ R-10 ์ „๊ฑด ํ†ต๊ณผ ํ™•์ธ. 5. **๋ ˆ๊ฑฐ์‹œ ์ฐจ๋‹จ**: ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์„ค์ • ์™„์ „ ์ œ๊ฑฐ. --- ### ๋ถ€๋ก X. Option A (๊ณ„์ • ๊ฐ„ Export / Import) ์˜ˆ์™ธ ๊ฒฝ๋กœ ๊ด€์ธก์ž๊ฐ€ **์„œ๋กœ ๋‹ค๋ฅธ ์‹ ๋ขฐ ๋„๋ฉ”์ธ**์— ์†ํ•ด ๊ณ„์ •์„ ์—„๊ฒฉํžˆ ๋ถ„๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ: ```conf accounts { MAM: { jetstream: enabled users: [ { user: mam_agent, password: $MAM_BROKER_PASS } ] exports: [ { stream: "python.mqtt.jobs.>", accounts: [HOME] } ] } HOME: { users: [ { user: home, password: $HOME_BROKER_PASS } ] imports: [ { stream: { account: MAM, subject: "python.mqtt.jobs.>" } } ] } SYS: { users: [ { user: sys, password: $SYS_BROKER_PASS } ] } } system_account: SYS ```