MQTT 프로토콜과 발행-구독 메시징 모델 이해하기

들어가며: MQTT는 왜 등장했는가
MQTT(Message Queuing Telemetry Transport)는 1999년 IBM의 Andy Stanford-Clark와 Arcom의 Arlen Nipper가 석유 파이프라인 원격 감시 시스템을 위해 설계한 경량 메시징 프로토콜입니다. 당시 요구 조건은 명확했습니다. 위성 회선처럼 대역폭이 좁고 지연이 크며, 배터리로 동작하는 기기에서도 안정적으로 데이터를 주고받아야 했습니다. 이 배경 때문에 MQTT는 처음부터 최소한의 헤더, 단순한 상태 머신, 낮은 전력 소비를 목표로 설계되었고, 지금은 OASIS 표준(MQTT 3.1.1, 5.0)으로 자리 잡아 IoT 센서, 스마트홈, 커넥티드카, 산업 자동화(IIoT) 전반의 사실상 표준 프로토콜이 되었습니다.
REST API에 익숙한 개발자가 MQTT를 처음 접하면 “왜 굳이 또 다른 프로토콜이 필요한가”라는 의문이 들 수 있습니다. 핵심은 통신 모델 자체가 다르다는 점입니다. HTTP는 클라이언트가 서버에 요청을 보내고 응답을 받는 요청-응답(Request-Response) 모델인 반면, MQTT는 메시지를 보내는 쪽과 받는 쪽이 서로의 존재를 몰라도 되는 발행-구독(Publish-Subscribe, Pub/Sub) 모델을 씁니다. 이 차이가 IoT 환경에서 왜 중요한지, 지금부터 하나씩 살펴보겠습니다.
발행-구독(Pub/Sub) 메시징 모델의 동작 원리
Pub/Sub 모델에는 세 가지 주체가 있습니다. 메시지를 만들어 보내는 Publisher(발행자), 관심 있는 메시지를 받아 처리하는 Subscriber(구독자), 그리고 둘 사이를 중개하는 Broker(브로커)입니다. 여기서 가장 중요한 특징은 Publisher와 Subscriber가 서로를 전혀 알지 못한다는 점입니다. Publisher는 특정 Subscriber를 향해 메시지를 보내지 않고, 그저 “이 주제(Topic)로 메시지를 발행한다”고만 브로커에 알립니다. Subscriber도 마찬가지로 “이 Topic을 구독하겠다”고 브로커에 등록해 두면, 해당 Topic으로 발행되는 모든 메시지를 자동으로 전달받습니다.
이런 구조를 시간적·공간적 결합 해제(decoupling)라고 부릅니다. 공간적으로는 Publisher와 Subscriber가 서로의 IP 주소나 존재 여부를 알 필요가 없고, 시간적으로는 Publisher가 메시지를 보낼 때 Subscriber가 반드시 온라인 상태일 필요가 없습니다(뒤에서 설명할 Retained Message와 세션 지속 기능이 이를 보완합니다). 예를 들어 100개의 온도 센서가 있다고 가정하면, 각 센서는 sensors/room1/temperature 같은 Topic으로 값을 발행하기만 하면 됩니다. 이 값을 대시보드, 알람 시스템, 데이터 로거가 각각 독립적으로 구독해 사용할 수 있고, 센서 코드는 이들이 몇 개인지, 어디에 있는지 전혀 신경 쓸 필요가 없습니다. 새로운 구독자가 추가되어도 Publisher 쪽 코드는 단 한 줄도 바뀌지 않습니다.
sequenceDiagram participant S as 온도 센서(Publisher) participant Br as Broker participant Dash as 대시보드(Subscriber) participant Alarm as 알람 시스템(Subscriber) Dash->>Br: SUBSCRIBE sensors/room1/temperature Alarm->>Br: SUBSCRIBE sensors/room1/temperature S->>Br: PUBLISH sensors/room1/temperature = 24°C Br-->>Dash: PUBLISH 24°C Br-->>Alarm: PUBLISH 24°C Note over S,Alarm: Publisher는 Dash·Alarm이 몇 개인지, 존재하는지조차 모른다
핵심 개념: Broker, Topic, Publisher, Subscriber
Broker(브로커)
브로커는 MQTT 통신의 중심에 있는 서버 프로세스입니다. 모든 클라이언트(Publisher든 Subscriber든 하나의 TCP 연결을 맺으면 클라이언트로 취급됩니다)는 브로커와만 연결하며, 브로커가 어떤 클라이언트가 어떤 Topic을 구독하고 있는지 추적하고, 발행된 메시지를 해당 구독자들에게 라우팅합니다. Mosquitto, EMQX, HiveMQ가 대표적인 오픈소스/상용 브로커이고, AWS IoT Core, Azure IoT Hub 같은 클라우드 관리형 서비스도 널리 쓰입니다.
Topic(토픽)
Topic은 메시지를 분류하는 문자열 경로로, 슬래시(/)로 계층을 구분합니다. 예를 들어 home/livingroom/temperature, factory/line1/machine3/status처럼 실제 물리적 구조나 논리적 분류를 그대로 반영하는 것이 일반적인 관례입니다. 특히 유용한 것은 와일드카드 구독입니다.
+(단일 레벨 와일드카드): 한 단계만 임의의 값을 허용합니다.home/+/temperature로 구독하면home/livingroom/temperature와home/bedroom/temperature를 모두 받습니다.#(다중 레벨 와일드카드): 해당 위치 이후의 모든 하위 레벨을 포함합니다.home/#로 구독하면home아래의 모든 Topic 메시지를 받습니다. 반드시 Topic 필터의 마지막에만 쓸 수 있습니다.
와일드카드 덕분에 대시보드 하나가 factory/#처럼 넓은 범위를 구독해 전체 현황을 모니터링하면서, 개별 알람 로직은 factory/line1/machine3/status처럼 좁은 범위만 구독하는 식의 유연한 설계가 가능합니다.
Publisher와 Subscriber
하나의 클라이언트가 동시에 Publisher와 Subscriber 역할을 겸할 수 있습니다. 예를 들어 스마트 온도조절기는 현재 온도를 home/thermostat/temperature에 발행(Publish)하는 동시에, 사용자 앱이 보낸 목표 온도 명령을 home/thermostat/set에서 구독(Subscribe)해서 받아들입니다. 이렇게 하나의 연결로 양방향 통신이 가능하다는 점도 MQTT가 IoT 기기에 적합한 이유 중 하나입니다.
QoS(Quality of Service) 0/1/2 레벨
MQTT의 QoS는 메시지가 얼마나 확실하게 전달되는지를 발행자와 브로커, 브로커와 구독자 사이의 각 홉(hop)마다 개별적으로 보장하는 계약입니다. 즉 발행자가 QoS 1로 보내도, 브로커가 특정 구독자에게 전달할 때는 그 구독자가 구독 시 지정한 QoS와 발행 시 QoS 중 낮은 쪽으로 다시 협상됩니다.
| QoS | 이름 | 보장 수준 | 전달 절차 | 적합한 상황 |
|---|---|---|---|---|
| 0 | At most once | 최대 1회, 유실 가능 | PUBLISH 한 번 전송 후 응답 없이 종료 | 갱신 주기가 짧아 유실돼도 다음 값이 곧 오는 센서 스트림 |
| 1 | At least once | 최소 1회, 중복 가능 | PUBLISH → PUBACK, 응답이 없으면 재전송 | 중복 처리가 가능한 이벤트·알림·명령 |
| 2 | Exactly once | 정확히 1회 | PUBLISH → PUBREC → PUBREL → PUBCOMP (4-way 핸드셰이크) | 과금·결제·설비 제어 명령처럼 중복이 치명적인 경우 |
QoS 0은 TCP 위에서 한 번 쏘고 끝이라 가장 빠르고 가볍지만, 네트워크 단절 시점에 발행된 메시지는 그대로 사라질 수 있습니다. QoS 1은 확인 응답(PUBACK)을 기다렸다가 응답이 없으면 재전송하므로 유실을 방지하지만, 그 대가로 동일 메시지가 중복 수신될 수 있어 구독자 쪽에서 멱등 처리가 필요할 수 있습니다. QoS 2는 4단계 핸드셰이크로 중복과 유실을 모두 막지만, 왕복이 늘어나는 만큼 지연과 브로커의 상태 관리 부담이 커집니다. 실무에서는 “정말 정확히 한 번이 필요한 메시지가 무엇인가”를 먼저 좁히고, 대부분의 트래픽은 QoS 0이나 1로 처리하는 편이 전체 시스템 처리량에 유리합니다.
HTTP·Kafka·RabbitMQ와 비교했을 때 MQTT의 위치
| 항목 | MQTT | HTTP | Kafka | RabbitMQ |
|---|---|---|---|---|
| 통신 모델 | Pub/Sub | 요청-응답 | Pub/Sub(로그 기반) | Pub/Sub·큐(AMQP) |
| 헤더 오버헤드 | 매우 작음(최소 2바이트) | 큼(텍스트 헤더) | 중간 | 중간 |
| 연결 유지 | 상시 연결(Keep Alive) | 요청마다 또는 커넥션 풀 | 상시 연결 | 상시 연결 |
| 대역폭·전력 최적화 | 매우 적합 | 상대적으로 무거움 | 고성능 서버 환경 전제 | 서버 환경 전제 |
| 저장·재처리 | 제한적(Retained 1개) | 없음 | 강력함(로그 보존) | 큐 기반 보존 |
| 주된 용도 | 기기 원격 측정·제어 | API 통신 | 대용량 이벤트 스트리밍 | 작업 큐·서비스 간 메시징 |
MQTT의 가장 큰 강점은 패킷 크기와 상태 관리 비용을 극단적으로 줄였다는 점입니다. 고정 헤더가 단 2바이트부터 시작하고, TCP 연결 하나를 계속 유지하기 때문에 매 메시지마다 새로 커넥션을 맺고 TLS 핸드셰이크를 반복하는 HTTP보다 배터리로 동작하는 임베디드 기기에 훨씬 유리합니다. 반면 Kafka는 대용량 이벤트를 디스크에 로그로 영속화하고 파티션 단위로 재처리·재생(replay)할 수 있다는 점에서 압도적이지만, 이는 브로커가 충분한 컴퓨팅·스토리지 자원을 갖춘 서버 환경을 전제로 합니다. 마이크로컨트롤러나 저전력 센서에는 Kafka 클라이언트 자체를 올리기 어려운 경우가 많습니다. RabbitMQ 역시 강력한 라우팅(Exchange)과 신뢰성 있는 큐를 제공하지만, AMQP 프로토콜 오버헤드와 클라이언트 구현 복잡도가 MQTT보다 높습니다. 결론적으로 “수천~수백만 개의 저전력·저대역폭 기기가 짧은 상태값을 자주 보내는” 시나리오에는 MQTT가, “대용량 이벤트를 안정적인 서버 인프라 사이에서 안전하게 처리·재처리”해야 하는 시나리오에는 Kafka나 RabbitMQ가 더 적합합니다.
브로커 준비: Mosquitto 실행하기
로컬에서 실습하려면 Docker로 Eclipse Mosquitto를 가장 빠르게 띄울 수 있습니다.
docker run -it --rm --name mosquitto \
-p 1883:1883 \
eclipse-mosquitto:2
기본 설정에서는 익명 접속이 막혀 있을 수 있으므로, 로컬 실습용 설정 파일을 하나 마운트해 두면 편리합니다.
# mosquitto.conf
listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
docker run -it --rm --name mosquitto \
-p 1883:1883 \
-v $(pwd)/mosquitto.conf:/mosquitto/config/mosquitto.conf \
eclipse-mosquitto:2
프로덕션 환경에서는 반드시 사용자 인증(username/password 또는 클라이언트 인증서)과 TLS(포트 8883)를 활성화해야 합니다. allow_anonymous true는 로컬 개발 전용 설정입니다.
실전 예제: Python paho-mqtt
Python의 paho-mqtt는 가장 널리 쓰이는 MQTT 클라이언트 라이브러리입니다. 센서 데이터를 발행하는 쪽과, 이를 구독해 처리하는 쪽을 각각 살펴보겠습니다.
pip install paho-mqtt
Publisher: 온도 센서 시뮬레이터
# publisher.py
import time
import json
import random
import paho.mqtt.client as mqtt
BROKER_HOST = "localhost"
BROKER_PORT = 1883
TOPIC = "home/livingroom/temperature"
def on_connect(client, userdata, flags, reason_code, properties=None):
if reason_code == 0:
print("브로커 연결 성공")
else:
print(f"연결 실패, reason_code={reason_code}")
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="sensor-livingroom-01")
client.on_connect = on_connect
# Keep Alive 60초: 60초 이내에 PINGREQ/PUBLISH 등 아무 패킷도 없으면
# 브로커가 연결이 끊겼다고 판단합니다.
client.connect(BROKER_HOST, BROKER_PORT, keepalive=60)
client.loop_start() # 백그라운드 스레드에서 네트워크 루프 실행
try:
while True:
payload = json.dumps({
"temperature": round(random.uniform(18.0, 26.0), 1),
"timestamp": time.time(),
})
# qos=1: 브로커까지는 최소 1회 도달을 보장
client.publish(TOPIC, payload, qos=1, retain=True)
print(f"발행: {TOPIC} -> {payload}")
time.sleep(5)
except KeyboardInterrupt:
client.loop_stop()
client.disconnect()
client.loop_start()는 백그라운드 스레드를 하나 열어 네트워크 송수신과 Keep Alive용 PINGREQ 전송을 자동으로 처리해 줍니다. 이 루프를 직접 관리하지 않고 loop_forever()를 쓰는 방식도 있지만, 위처럼 메인 스레드에서 다른 작업(센서 값 읽기 등)을 병행해야 할 때는 loop_start()가 더 실용적입니다. retain=True를 주면 이 메시지가 Retained Message로 브로커에 저장되어, 이후 새로 구독하는 클라이언트가 즉시 마지막 온도값을 받을 수 있습니다.
Subscriber: 대시보드 백엔드
# subscriber.py
import json
import paho.mqtt.client as mqtt
BROKER_HOST = "localhost"
BROKER_PORT = 1883
TOPIC_FILTER = "home/+/temperature" # 모든 방의 온도를 구독
def on_connect(client, userdata, flags, reason_code, properties=None):
print("브로커 연결, 구독 시작")
# QoS 1로 구독: 브로커가 이 클라이언트에게 전달할 때도 최소 1회 보장
client.subscribe(TOPIC_FILTER, qos=1)
def on_message(client, userdata, msg):
data = json.loads(msg.payload.decode())
print(f"[{msg.topic}] 온도={data['temperature']}°C (retain={msg.retain})")
# 여기서 DB 저장, 알람 판정, 웹소켓 브로드캐스트 등을 수행합니다.
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="dashboard-backend")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER_HOST, BROKER_PORT, keepalive=60)
client.loop_forever() # 메인 스레드를 이 루프에 맡김 (재연결도 자동 처리)
구독 Topic으로 와일드카드 +를 사용해 home/livingroom/temperature, home/bedroom/temperature 등 방 이름과 무관하게 모든 온도 데이터를 한 번의 구독으로 받아냅니다. on_message 콜백의 msg.retain 값을 확인하면, 이 메시지가 새로 발행된 값인지 아니면 접속 직후 브로커가 보내준 Retained Message인지 구분할 수 있습니다.
실전 예제: Node.js mqtt.js
백엔드가 Node.js 기반이거나, 웹 대시보드에서 WebSocket으로 MQTT에 직접 붙어야 한다면 mqtt.js가 표준적인 선택입니다.
npm install mqtt
Publisher: 기기 상태 발행
// publisher.ts
import mqtt from 'mqtt';
const client = mqtt.connect('mqtt://localhost:1883', {
clientId: 'gateway-device-01',
keepalive: 60,
clean: true, // true면 Clean Session(재접속 시 이전 구독·큐 초기화)
// Last Will and Testament: 비정상 종료 시 브로커가 대신 발행
will: {
topic: 'devices/gateway-01/status',
payload: JSON.stringify({ online: false, reason: 'unexpected_disconnect' }),
qos: 1,
retain: true,
},
});
client.on('connect', () => {
console.log('브로커 연결 성공');
// 정상 연결되면 online 상태를 Retained로 발행
client.publish(
'devices/gateway-01/status',
JSON.stringify({ online: true }),
{ qos: 1, retain: true }
);
});
setInterval(() => {
const payload = JSON.stringify({
cpuUsage: Math.random() * 100,
timestamp: Date.now(),
});
client.publish('devices/gateway-01/metrics', payload, { qos: 0 });
}, 3000);
Last Will and Testament(LWT)는 연결 시점에 미리 브로커에 등록해 두는 “유언” 메시지입니다. 클라이언트가 정상적으로 DISCONNECT를 보내고 종료하면 이 메시지는 발행되지 않지만, Keep Alive 시간 안에 PINGREQ도 오지 않고 TCP 연결이 그냥 끊어지는 등 비정상 종료가 감지되면 브로커가 대신 이 메시지를 발행합니다. 위 예제처럼 기기의 온라인/오프라인 상태를 Retained + LWT 조합으로 관리하면, 대시보드는 별도의 헬스체크 폴링 없이도 기기가 갑자기 꺼졌는지를 실시간으로 알 수 있습니다.
Subscriber: 상태 모니터링
// subscriber.ts
import mqtt from 'mqtt';
const client = mqtt.connect('mqtt://localhost:1883', {
clientId: 'monitoring-service',
});
client.on('connect', () => {
// 모든 기기의 status와 metrics를 다중 레벨 와일드카드로 구독
client.subscribe('devices/+/status', { qos: 1 });
client.subscribe('devices/#', { qos: 0 });
});
client.on('message', (topic, payload) => {
const data = JSON.parse(payload.toString());
if (topic.endsWith('/status')) {
console.log(`[상태] ${topic}:`, data);
if (data.online === false) {
// 알람 발송, 인시던트 티켓 생성 등
console.warn(`경고: ${topic} 오프라인 감지`);
}
} else {
console.log(`[메트릭] ${topic}:`, data);
}
});
client.on('reconnect', () => {
console.log('재연결 시도 중...');
});
mqtt.js는 기본적으로 연결이 끊기면 자동 재연결을 시도합니다. clean: false(Persistent Session)로 연결하면, 구독자가 오프라인이었던 동안 QoS 1·2로 발행된 메시지를 브로커가 세션 큐에 보관했다가 재접속 시 전달해 줍니다. 반대로 clean: true(Clean Session)는 매번 깨끗한 상태로 시작하므로 구독 정보와 큐가 초기화됩니다. 배터리 소모를 줄이려고 자주 슬립하는 기기는 Persistent Session을, 상태를 매번 새로 받아도 무방한 대시보드류 클라이언트는 Clean Session을 선택하는 것이 일반적입니다.
실무에서 중요한 세부 기능
Keep Alive
클라이언트는 연결 시 Keep Alive 시간(초 단위)을 브로커에 알립니다. 이 시간의 1.5배 동안 아무 패킷도 오가지 않으면 브로커는 연결이 끊긴 것으로 간주하고 등록된 Last Will을 발행합니다. 값이 너무 짧으면 불필요한 PINGREQ 트래픽이 늘어 배터리를 소모하고, 너무 길면 실제 단절을 늦게 감지합니다. 실제 네트워크 환경(모바일 데이터, 위성, NAT 타임아웃)에 맞춰 30~120초 사이에서 조정하는 경우가 많습니다.
Retained Message
Topic마다 브로커는 가장 최근에 retain=true로 발행된 메시지 단 하나만 저장합니다. 새로운 구독자가 해당 Topic을 구독하는 순간, 브로커는 과거 발행 이력과 무관하게 이 저장된 값을 즉시 전달합니다. “현재 온도가 몇 도인가”, “기기가 지금 온라인인가” 같은 최신 상태 조회에 최적화된 기능이며, 이벤트 로그가 아니라 상태(state)를 표현하는 Topic에 사용하는 것이 원칙입니다. 빈 페이로드로 발행하면 Retained Message를 삭제할 수 있습니다.
Last Will and Testament(LWT)
앞서 예제에서 설명했듯, 클라이언트가 예기치 않게 끊겼을 때 브로커가 대신 발행해 주는 메시지입니다. LWT와 Retained를 함께 쓰면 “기기가 살아있을 때는 online, 죽으면 자동으로 offline”이라는 상태 전이를 애플리케이션 코드 없이 프로토콜 레벨에서 구현할 수 있습니다.
sequenceDiagram participant Dev as 기기(Publisher) participant Br as Broker participant Mon as 모니터링(Subscriber) Dev->>Br: CONNECT (Will: devices/x/status = offline) Dev->>Br: PUBLISH devices/x/status = online (retain) Br-->>Mon: status = online Note over Dev,Br: 네트워크 단절 · Keep Alive 초과 Br-->>Mon: status = offline (Broker가 대신 발행한 LWT)
Clean Session / Persistent Session (MQTT 5.0에서는 Clean Start + Session Expiry Interval)
세션을 유지할지 여부를 결정합니다. Persistent Session에서는 구독 정보뿐 아니라 오프라인 중 QoS 1·2로 발행된 메시지도 브로커가 보관했다가 재접속 시 전달합니다. MQTT 5.0은 이를 Session Expiry Interval로 세분화해, 세션을 얼마나 오래 유지할지 명시적으로 지정할 수 있습니다.
실제 사용 사례
- IoT 센서 네트워크: 온도·습도·조도 센서 수백~수천 개가 각자의 Topic에 값을 발행하고, 수집 서버가 와일드카드로 한꺼번에 구독해 시계열 DB(InfluxDB, TimescaleDB 등)에 적재하는 구성이 표준적입니다.
- 홈 오토메이션: Home Assistant, openHAB 같은 오픈소스 홈 오토메이션 플랫폼은 MQTT를 핵심 통합 계층으로 사용합니다. 조명·플러그·온도조절기가 각자 상태를 발행하고, 자동화 규칙 엔진이 이를 구독해 조건에 맞는 명령을 다시 발행하는 식으로 동작합니다.
- 커넥티드카·차량 텔레메트리: 위치, 배터리 잔량, 진단 코드(DTC)를 이동통신망을 통해 저대역폭으로 서버에 전송할 때 MQTT가 자주 쓰입니다. 연결이 자주 끊기는 환경 특성상 Persistent Session과 QoS 1 조합이 흔합니다.
- 산업 자동화(IIoT): 공장의 PLC·센서 데이터를 SCADA 시스템과 연동할 때, Sparkplug B 같은 MQTT 기반 페이로드 규격을 얹어 표준화된 상태 보고를 구현하는 사례가 늘고 있습니다.
- 모바일 푸시·채팅: 배터리 소모를 최소화해야 하는 모바일 앱의 백그라운드 알림 채널로 MQTT를 채택하는 경우도 있습니다(대표적으로 Facebook Messenger가 초기에 MQTT를 활용한 사례가 알려져 있습니다).
모범 사례와 트러블슈팅
Topic 설계는 너무 세분화하지도, 너무 뭉뚱그리지도 않게 잡는 것이 중요합니다. 계층이 지나치게 깊으면 와일드카드 구독의 효율이 떨어지고, 반대로 하나의 Topic에 여러 종류의 데이터를 섞어 넣으면 구독자가 매번 페이로드를 파싱해 분기해야 합니다. {도메인}/{기기ID}/{속성} 같은 일관된 규칙을 팀 차원에서 정해 두는 편이 유지보수에 유리합니다.
| 증상 | 흔한 원인 | 대응 |
|---|---|---|
| 메시지가 간헐적으로 유실됨 | QoS 0 사용 중 네트워크 불안정 | 중요 메시지는 QoS 1로 상향, 재전송 로직 확인 |
| 같은 메시지가 중복 수신됨 | QoS 1의 At-least-once 특성 | 페이로드에 메시지 ID를 넣어 구독자 측에서 멱등 처리 |
| 재접속 후 상태가 비어 있음 | Clean Session으로 접속해 이전 구독·큐 초기화 | 유지가 필요한 클라이언트는 Persistent Session(clean: false) 사용 |
| 기기가 오프라인인데도 online으로 표시됨 | Last Will 미설정 또는 Keep Alive가 너무 김 | LWT 등록, Keep Alive 값을 네트워크 환경에 맞게 단축 |
| 브로커 연결 수가 갑자기 급증·급감 | 재연결 폭주(reconnect storm) | 지수 백오프 재연결 정책 적용, 클라이언트 ID 충돌 여부 점검 |
| 특정 Topic만 메시지가 안 옴 | 와일드카드 위치 오류, ACL(접근 제어) 설정 | Topic 필터 문법 재확인, 브로커 ACL·인증 로그 확인 |
특히 클라이언트 ID 충돌은 자주 놓치는 함정입니다. 동일한 clientId로 두 개의 연결이 시도되면 MQTT 스펙상 브로커는 기존 연결을 강제로 끊고 새 연결을 받아들입니다. 기기를 재배포하거나 스케일아웃할 때 클라이언트 ID를 고정 문자열로 하드코딩해 두면, 여러 인스턴스가 서로의 연결을 끊어버리는 문제가 발생할 수 있으므로 인스턴스별로 고유한 ID(호스트명, UUID 등)를 부여해야 합니다.
정리
MQTT는 발행-구독 모델과 최소한의 프로토콜 오버헤드를 결합해, 수많은 저전력·저대역폭 기기가 브로커 하나를 통해 느슨하게 결합된 채로 통신하도록 설계되었습니다. Broker·Topic·QoS라는 핵심 개념만 이해하면 Python의 paho-mqtt나 Node.js의 mqtt.js로 몇 줄 만에 Publish/Subscribe를 구현할 수 있고, Last Will·Retained Message·Keep Alive 같은 프로토콜 내장 기능을 활용하면 별도의 헬스체크나 상태 관리 로직 없이도 신뢰성 있는 IoT 애플리케이션을 만들 수 있습니다. HTTP나 Kafka·RabbitMQ와 경쟁 관계라기보다는, 각자 강점이 뚜렷한 도구이므로 기기 규모·대역폭·전력 제약·메시지 보존 요구사항을 기준으로 적합한 프로토콜을 선택하는 것이 바람직합니다.