MQTT 프로토콜 완전 정리 (Pub/Sub 모델부터 QoS까지)
MQTT(Message Queuing Telemetry Transport) 프로토콜의 동작 원리, Publisher/Subscriber 패턴, QoS 레벨, Broker 구조부터 IoT와 PNS 실무 적용까지 경량 메시징 프로토콜의 모든 것을 정리합니다.
제가 다니는 회사에는 PNS(Push Notification Server)라는 서버가 있어요. 사내 주요 서비스의 실시간 메시지와 알림이 전부 이 서버를 지납니다.
그런데 이 PNS가 사실은 MQTT 브로커의 구현체예요. 메시지를 주고받는 규칙을 우리가 정한 게 아니라, MQTT라는 프로토콜이 정해 둔 것을 따르고 있었어요.
그래서 프로토콜부터 봐야 했어요. 이 글은 그때 정리한 내용입니다.
MQTT는 센서가 보내는 데이터를 모으려고 만들어졌어요
MQTT는 Message Queuing Telemetry Transport의 약자예요. 떨어져 있는 장치들이 데이터를 주고받게 해주는 경량 메시징 프로토콜입니다.
1990년대에 IBM과 Eurotech가 만들었어요. 목적이 분명했습니다. 석유나 가스 같은 대형 산업에서는 수많은 센서가 각자 데이터를 보내오는데, 그것을 중앙에서 모아 관리해야 했거든요. 원격 감시(telemetry) 장비의 데이터를 효율적으로 수집하려고 설계한 프로토콜이에요.
그 뒤로 이렇게 자리를 잡았어요.
- 2004년: mqtt.org (새 창에서 열림)가 열리면서 커뮤니티가 생겼습니다
- 2012년: Eclipse 재단이 Paho 프로젝트로 오픈소스화했습니다
- 2014년: OASIS 표준이 됐습니다. 2013년에 기술위원회가 꾸려졌고 이듬해 10월에 3.1.1이 표준으로 확정됐어요
가벼움과 신뢰성 사이를 쓰는 쪽이 고릅니다
MQTT를 고르는 이유는 몇 가지로 모여요.
프로토콜 구조가 단순해서 구현이 쉽고 메시지도 가볍습니다. 배터리로 도는 장치에서 적은 전력으로 동작하고, 대역폭이 좁은 환경에서도 버텨요. 보내는 쪽과 받는 쪽이 비동기로 주고받으니 한쪽이 응답을 기다리며 멈춰 있지도 않습니다.
여기에 QoS 레벨이 붙어요. 메시지를 얼마나 확실하게 전달할지 세 단계 중에 고를 수 있어요. 가벼움과 신뢰성 사이에서 어디쯤에 설지를 쓰는 쪽이 정하는 구조예요.
보내는 쪽과 받는 쪽은 서로를 모릅니다
MQTT는 Publish/Subscribe(발행/구독) 모델로 동작해요. 셋이 나옵니다.
- Publisher: 데이터를 만들어 특정
Topic으로 발행합니다 - Broker: Publisher가 보낸 메시지를 받아 알맞은 Subscriber에게 전달합니다
- Subscriber: 받고 싶은
Topic을 브로커에 등록해 두고 그 토픽의 메시지를 받습니다
중요한 것은 Publisher와 Subscriber가 서로를 모른다는 점이에요. 누가 받는지 모르는 채로 토픽에 던지고, 누가 보냈는지 모르는 채로 토픽에서 받아요. 그래서 장치가 늘거나 줄어도 보내는 쪽 코드를 고치지 않아요.
주고받는 패킷에 이름이 다 붙어 있어요. 접속은 CONNECT로 시작해서 CONNACK로 확인받고, 구독은 SUBSCRIBE를 보내 SUBACK를 받습니다. 발행은 PUBLISH 하나예요. 브로커는 이 PUBLISH를 받아 그 토픽을 구독 중인 클라이언트 모두에게 다시 PUBLISH로 보냅니다.
클라이언트 하나가 Publisher와 Subscriber를 겸할 수 있어요. 발행만 하든 구독만 하든 둘 다 하든 상관없어요. 대신 브로커는 그만큼 할 일이 많아요. 메시지 라우팅에 더해 큐잉과 QoS 관리까지 브로커가 떠안습니다.
Topic이 주소 역할을 합니다
토픽(Topic)은 보내는 쪽과 받는 쪽을 이어 주는 이름이에요. 클라이언트는 자기가 받을 토픽을 지정하고, 그 토픽으로 온 메시지를 받아요.
토픽은 /로 구분하는 계층 구조를 가져요. 대소문자를 구분하는 UTF-8 문자열이에요. home/kitchen/fridge나 vehicles/car/status처럼 씁니다. 최소 한 글자는 있어야 해요.
와일드카드로 여러 토픽을 한 번에 구독합니다
토픽을 하나씩 등록하지 않고 범위로 묶을 수 있어요. 기호가 둘이에요.
+는 한 레벨에 대응해요.foo/+/bar처럼 씁니다#는 여러 레벨에 대응해요.foo/#처럼 씁니다. 필터의 맨 끝에만 올 수 있어서foo/#/bar는 쓸 수 없어요
| 필터 | 받는 토픽 | 받지 못하는 토픽 |
|---|---|---|
| home/kitchen/+ | home/kitchen/fridge home/kitchen/oven | home/kitchen/fridge/temperature (레벨이 하나 더 있어서 못 받습니다) |
| home/# | home/kitchen/fridge home/kitchen/oven home/livingroom/sofa home/kitchen/fridge/temperature | 없음 |
| vehicles/+/status | vehicles/car/status vehicles/bike/status | vehicles/car/status/battery |
+는 딱 그 자리 한 칸만 채우고, #는 그 아래 전부를 가져옵니다. 표에서 home/kitchen/+가 home/kitchen/fridge/temperature를 못 받는 이유가 그거예요.
$로 시작하는 토픽은 따로 정해진 용도가 있어요
$로 시작하는 토픽은 일반 토픽과 다르게 다룹니다. 여기서 두 가지를 갈라야 해요.
$SYS/는 브로커가 자기 상태를 알리는 토픽입니다. 접속한 클라이언트 수나 처리한 메시지 수 같은 것이 여기로 올라와요. 애플리케이션이 여기에 발행하지 않아요.
$share/는 반대로 애플리케이션이 쓰는 것이에요. 공유 구독(Shared Subscription)이라고 부르고, MQTT 5.0에서 들어온 기능입니다.
일반 구독은 그 토픽을 구독한 클라이언트 전부가 같은 메시지를 받아요. 공유 구독은 다릅니다. 같은 그룹 이름으로 묶인 클라이언트들 중 하나만 받아요. 그림에서 consumer1 그룹과 consumer2 그룹이 메시지를 각각 받고, 그룹 안에서는 한 클라이언트만 처리하는 식입니다.
같은 일을 하는 서버를 여러 대 띄워 놓고 메시지를 나눠 처리하고 싶을 때 씁니다. 한 대가 죽어도 나머지가 받아요.
QoS는 확인 응답을 몇 번 주고받을지 정합니다
메시지를 얼마나 확실하게 전달할지는 QoS(Quality of Service) 레벨로 고릅니다. 세 단계가 있고, 단계가 올라갈수록 주고받는 패킷이 늘어나요.
QoS 0은 보내고 잊습니다
PUBLISH를 한 번 보내고 끝이에요. 받았는지 확인하지 않아요.
그래서 제일 빠르고 제일 가볍지만, 중간에 유실돼도 아무도 모릅니다. 최대 한 번(At most once)이라고 부르는 이유가 그거예요. 한 번 도착하거나 아예 안 도착하거나 둘 중 하나입니다.
QoS 1은 응답이 올 때까지 다시 보냅니다
PUBLISH를 보내고 PUBACK를 기다려요. 정해진 시간 안에 안 오면 같은 메시지를 다시 보냅니다.
덕분에 도착은 보장되는데, 응답만 늦게 오고 메시지는 이미 도착했던 경우에도 재전송이 나갑니다. 받는 쪽에 같은 메시지가 두 번 들어올 수 있어요. 최소 한 번(At least once)이에요.
QoS 2는 네 번 주고받습니다
중복까지 없애려면 단계가 더 필요해요. PUBLISH, PUBREC, PUBREL, PUBCOMP 네 개를 주고받습니다.
받는 쪽이 PUBREC로 "받았다"를 알리면 보내는 쪽이 PUBREL로 "그럼 처리해라"를 보내고, 처리가 끝나면 PUBCOMP가 돌아와요. 이렇게 해야 재전송이 일어나도 받는 쪽이 같은 메시지를 두 번 처리하지 않습니다. 정확히 한 번(Exactly once)이에요.
대신 왕복이 네 번이라 느리고 네트워크 부하도 큽니다.
양쪽 설정이 다르면 낮은 쪽을 따릅니다
여기서 헷갈리기 쉬운 것이 하나 있어요. QoS는 발행하는 쪽과 구독하는 쪽이 따로 정합니다. 둘이 다르면 어떻게 될까요.
항상 낮은 쪽이 적용됩니다. Publisher가 QoS 2로 보내도 Subscriber가 QoS 0으로 구독했다면 그 Subscriber에게는 QoS 0으로 갑니다. 이것을 QoS Downgrade라고 불러요.
발행 쪽만 QoS 2로 올려놓고 안심하면 안 되는 이유가 이겁니다. 구독 쪽 설정까지 맞춰야 원하는 보장이 걸려요.
연결이 끊기면 브로커가 대신 알려줍니다
LWT(Last Will and Testament) 는 클라이언트가 접속할 때 브로커에게 미리 맡겨 두는 메시지예요. 연결이 정상적으로 끊기지 않았을 때 브로커가 이 메시지를 대신 발행해요.
장치가 갑자기 전원이 나가거나 네트워크가 끊기면 스스로 "나 죽는다"를 보낼 수 없어요. 그래서 미리 맡겨 둡니다. 구독하던 쪽은 이 메시지를 받고 그 장치가 사라졌다는 것을 알게 돼요. 장치 상태를 지켜보거나 장애를 알릴 때 씁니다.
어디에 쓰이고 있나
Facebook Messenger가 2011년에 MQTT를 도입했어요. 네트워크 대역폭과 배터리 소모를 줄이려는 목적이었어요.
클라우드 쪽에서는 AWS IoT와 Azure IoT Hub가 MQTT를 받아들였어요. 그 밖에 스마트홈과 산업용 센서 네트워크, 실시간 알림 서비스에서 널리 씁니다.
브로커 구현체는 골라 쓸 수 있어요.
- Mosquitto: 가벼운 브로커입니다. 여러 플랫폼에서 돌아가고 범용으로 씁니다
- EMQX: 고성능 브로커입니다. 클러스터링과 여러 프로토콜을 지원합니다
- HiveMQ: 대규모 IoT에 맞춘 상용 브로커예요
- Aedes: Node.js 기반의 가벼운 브로커예요
한계는 구조에서 나옵니다
모든 메시지가 브로커를 지납니다. 그래서 브로커가 멈추면 메시징이 전부 멈춰요. 단일 브로커로는 규모를 키우는 데도 한계가 있어요. 보안도 프로토콜이 다 해주지 않아요. 접속할 때 사용자 이름과 비밀번호를 실어 보낼 자리는 있지만 그 값을 암호화하지는 않습니다. 전송 구간 암호화는 TLS로 따로 붙여야 해요.
그래서 실제 운영에서는 이렇게 보완해요.
- 브로커를 클러스터로 묶어 한 대가 죽어도 이어지게 합니다
- TLS를 올려 전송 구간을 암호화하고 인증을 겁니다
- 규모가 커지면 브로커를 분산 배치합니다
마무리
MQTT는 제한된 환경에서 메시지를 주고받으려고 만든 프로토콜이에요. 보내는 쪽과 받는 쪽이 서로를 모른 채 토픽으로만 만나고, 얼마나 확실하게 전달할지는 QoS로 고릅니다.
다만 이 단순함은 브로커가 복잡함을 떠안아서 나오는 것이에요. 프로토콜을 읽는 것과 브로커를 운영하는 것은 다른 일입니다. 이 글은 앞쪽까지만 다뤘어요.