MQTT 입문 - 가벼운 메시징은 어떻게 가벼운가

2021-06-15 · 엔지니어링
MQTT 입문 - 가벼운 메시징은 어떻게 가벼운가

최근 직장을 옮기게 되면서 MQTT를 마주할 일이 많아졌습니다. 이전에도 찍먹 해본 적은 있지만, 한번 구체적으로 학습해볼 필요가 있을 것 같아 정리해보려고 합니다.

MQTT는 무엇인가

MQTT(Message Queuing Telemetry Transport)는 발행-구독(pub/sub) 방식의 경량 메시징 프로토콜입니다. TCP 위에서 동작하고, 메시지를 보내는 쪽(publisher)과 받는 쪽(subscriber)이 서로를 전혀 모른 채 브로커(broker) 라는 중개자를 통해서만 통신합니다.

이름에 “Queuing”이 들어가 있어서 메시지 큐 솔루션처럼 들리지만, 찾아보니 정작 MQTT 자체는 큐를 핵심으로 내세우는 프로토콜은 아니었습니다. 이름은 역사적인 흔적에 가깝고, 본질은 “누가 받을지 모르는 메시지를 토픽(topic)에 던지면, 그 토픽을 구독한 쪽이 받는다” 는 단순한 모델입니다.

처음엔 “그럼 그냥 작은 Kafka 같은 건가?” 싶었는데, 들여다볼수록 지향점이 달랐습니다. Kafka가 대용량 로그 스트림을 다룬다면, MQTT는 약한 네트워크에 붙은 수많은 작은 기기를 다루는 쪽에 가깝습니다.

왜 HTTP를 두고 MQTT인가

가장 먼저 든 의문이 이거였습니다. 이미 HTTP가 있는데 왜 또 다른 프로토콜을 쓸까요?

센서나 모바일 기기처럼 배터리·대역폭이 빠듯하고 연결이 불안정한 환경을 떠올려보면 답이 나옵니다. HTTP는 요청-응답마다 헤더가 무겁고, 기본적으로 클라이언트가 물어봐야(polling) 서버의 변화를 알 수 있습니다. 기기 수천 대가 1초마다 “새 거 있어요?”를 물어보는 그림은 상상만 해도 비효율적입니다.

MQTT는 추구하는 방향이 조금 다릅니다.

  • 한 번 TCP 연결을 맺어 계속 유지하고, 서버(브로커)가 변화가 생기면 밀어넣어주는 형태입니다(push).
  • 고정 헤더가 2바이트부터 시작할 만큼 작습니다. 제어 패킷 자체가 군더더기가 거의 없습니다.
  • 한 메시지를 여러 구독자에게 한 번에 퍼뜨리는 다대다 구조가 기본입니다.

즉 “가볍다”는 건 위와 같은 헤더 크기·연결 유지·푸시 모델이라는 구체적인 내부 설계 덕분인 것입니다.

브로커와 토픽

구조 자체는 단순합니다. 모든 통신은 브로커를 거칩니다.

  • 발행자는 home/livingroom/temperature 같은 토픽에 메시지를 보냅니다.
  • 구독자는 관심 있는 토픽을 브로커에 등록해두고, 그 토픽으로 들어온 메시지를 받습니다.
  • 발행자와 구독자는 서로의 존재를 모릅니다. 오직 토픽이라는 약속만 공유합니다.

토픽은 /로 계층을 나누고, 구독할 때 와일드카드를 쓸 수 있습니다.

  • + 는 한 단계를 의미합니다. home/+/temperature 는 거실·침실 등 한 층의 모든 방 온도를 받습니다.
  • # 는 그 아래 전부를 의미합니다. home/# 는 home 밑의 모든 토픽을 받습니다.

이 발행-구독 분리가 주는 이점은, 기기를 추가하거나 빼도 서로의 코드를 고칠 필요가 없다는 점입니다. 새 센서는 그냥 약속된 토픽에 던지기만 하면 됩니다. 생산자와 소비자를 이렇게 떼어놓는 발상은 카프카 같은 다른 브로커류와도 비슷한 컨셉인 것 같습니다.

QoS 메세지 전달 보장

MQTT를 제대로 이해해보는 건 사실상 QoS(Quality of Service) 를 이해하는 일이었습니다. 메시지를 “얼마나 확실하게” 전달할지를 세 단계로 고를 수 있는데, 이게 곧 신뢰성과 비용 사이의 트레이드오프입니다.

QoS 0 · 최대 한 번 (at most once)

던지고 끝입니다. 브로커가 받았는지 확인하지 않습니다. 네트워크가 끊기면 그 메시지는 그냥 사라집니다. 대신 가장 빠르고 가볍습니다. 1초마다 갱신되는 온도값처럼 “하나쯤 놓쳐도 다음 값이 곧 온다”면 이걸로 충분합니다.

QoS 1 · 최소 한 번 (at least once)

받는 쪽이 PUBACK으로 “받았다”고 응답합니다. 응답이 안 오면 발행자가 다시 보냅니다. 그래서 유실은 없지만, 재전송 과정에서 같은 메시지가 중복으로 도착할 수 있습니다. 여기서 처음 “어, 그럼 중복 처리는 누가 하지?”라는 고민이 생깁니다.

QoS 2 · 정확히 한 번 (exactly once)

유실도 중복도 없습니다. 대신 그 보장을 위해 4단계 핸드셰이크(PUBLISH → PUBREC → PUBREL → PUBCOMP)를 주고받습니다. 가장 안전하지만 가장 비싼 네트워크 비용이 듭니다. 왕복이 늘어나니 지연도, 브로커가 상태를 들고 있어야 하는 부담도 커집니다.

결제·과금처럼 “두 번 처리되면 큰일”인 메시지가 아니라면, QoS는 상황에 맞게 비교해보고 고르는 게 좋을 것 같았습니다.

여기서 짚고 넘어갈 것이 하나 있습니다. QoS는 발행자–브로커 구간과 브로커–구독자 구간에서 따로 적용됩니다. 발행을 QoS 2로 했다고 구독자까지 자동으로 exactly once가 되는 게 아니라, 구독자가 구독할 때 요청한 QoS와 둘 중 낮은 쪽으로 맞춰집니다. 이 부분이 제일 헷갈릴수 있는 부분인것 같아 꼭 기억해두면 좋을것 같습니다!

처음엔 몰랐던 장치들

QoS 말고도, 그 외의 주요한 기능들이 몇 개 있었습니다.

Retained 메시지. 마지막 값을 붙잡아 두는 용도입니다. 보통 pub/sub은 구독을 시작하기 전에 발행된 메시지는 못 받습니다. 그런데 retained 플래그를 켜서 발행하면, 브로커가 그 토픽의 마지막 메시지 하나를 보관해뒀다가, 나중에 새로 구독하는 쪽에 즉시 건네줍니다. “지금 거실 온도 몇 도야?”를 매번 물을 필요 없이, 구독하자마자 최신값을 받는 식입니다.

LWT(Last Will and Testament). 유언장. 기기가 비정상적으로 연결이 끊겼을 때, 브로커가 대신 “이 녀석 죽었다”는 메시지를 미리 정해둔 토픽에 발행해줍니다. 센서가 조용히 사라졌는지를 다른 쪽이 알아챌 수 있게 하는 장치입니다. 이런 게 프로토콜 레벨에 들어가 있다는 점에서, MQTT가 “불안정한 연결”을 얼마나 기본 전제로 깔고 설계됐는지 느껴졌습니다.

Keep-alive. 일정 시간 보낼 게 없으면 클라이언트가 PINGREQ라는 작은 핑을 보내 “나 살아있다”를 알립니다. 이게 없으면 브로커는 조용히 붙어만 있는 연결이 살아있는지 죽었는지 구분할 수 없습니다. 연결을 계속 살려둔다는 컨셉은 HTTP의 keep-alive와도 이런 부분이 비슷해보였습니다.

3.1.1과 5.0 (작성 시점 기준)

지금(2021년) 가장 많이 쓰이는 버전은 MQTT 3.1.1(2014년 OASIS 표준)이고, 2019년에 MQTT 5.0이 나와 도입이 늘어가는 중입니다. 5.0에서는 실패 원인을 알려주는 reason code, 메시지 만료 시간, 여러 구독자가 메시지를 나눠 받는 shared subscription, user property 같은 게 추가됐습니다. 새로 시작한다면 5.0을 보되, 붙으려는 브로커·기기가 5.0을 지원하는지부터 확인하는 게 순서일 것 같습니다.

정리

직접 정리해보니 MQTT의 “가볍다”는 결국 푸시 기반 지속 연결 + 작은 헤더 + 발행-구독 분리였고, 진짜 고민거리는 그 위에서 QoS로 신뢰성과 비용을 어디에 둘 것인가였습니다. QoS 1을 골랐다면 중복을 누가 어떻게 거를지, QoS 2가 정말 필요한지를 같이 정해야 비로소 설계가 완성되는 느낌이었습니다.

다음 편에서는 이 MQTT 프로토콜을 실제로 실행해줄수 있는 구현체인 ActiveMQ에 대해서 알아보려고 합니다.

참고

MQTT Version 3.1.1 (OASIS Standard) MQTT Version 5.0 (OASIS Standard)

댓글

GitHub(giscus) 댓글은 설정 완료 후 활성화됩니다.