단편모음2편

MQTT: 끊길 것을 전제로 설계된 프로토콜

웹이 요청하고 응답받는 동안, 사물의 세계는 다른 방식으로 대화해 왔습니다. 발행과 구독, 그리고 '유언'까지 준비하는 MQTT의 설계 철학을 소개합니다.

우리가 웹에서 익숙해진 대화 방식은 요청과 응답입니다. 브라우저가 서버에게 “이 페이지를 주세요”라고 묻고, 서버가 답합니다. 묻지 않으면 아무 일도 일어나지 않지요. 이 방식은 사람이 화면 앞에 앉아 무언가를 궁금해할 때는 더없이 자연스럽습니다.

그런데 사물의 세계로 넘어오면 이야기가 달라집니다. 창고 천장에 매달린 온도 센서 이백 개가 있다고 해봅시다. 서버가 이들에게 “지금 몇 도야?”라고 물으려면 이백 번을 물어야 하고, 언제 물어야 할지도 알 수 없습니다. 1분마다 물으면 아무 변화가 없는 199개에게 헛물을 켠 것이고, 10분마다 물으면 그 사이에 냉동고가 녹아도 모릅니다. 게다가 이 센서들은 배터리로 돌아가고, 신호는 약하고, 수시로 끊깁니다.

MQTT는 바로 이 상황에서 태어난 프로토콜입니다. 1999년, IBM의 앤디 스탠퍼드클라크와 아콤의 알렌 니퍼가 사막을 가로지르는 송유관을 위성으로 감시하기 위해 만들었습니다. 위성 회선은 느리고, 비싸고, 끊깁니다. 그러니 프로토콜은 가벼워야 하고, 무엇보다 끊길 것을 당연하게 여겨야 했습니다. 이 출신 성분이 MQTT의 거의 모든 설계를 설명합니다.

묻지 않고, 알린다

MQTT의 첫 번째 전환은 묻기를 그만두는 것입니다. 센서는 아무에게도 묻지 않고, 그저 자기가 아는 사실을 발행(publish) 합니다. “창고/냉동고A/온도는 -18.2도다.” 이 말은 브로커(broker)라는 중개자에게 전달됩니다. 그 사실이 궁금한 쪽은 미리 브로커에게 구독(subscribe) 을 걸어둡니다. “창고/냉동고A/온도가 바뀌면 알려줘.”

센서는 누가 듣는지 모릅니다. 듣는 쪽도 어느 센서가 말했는지 알 필요가 없습니다. 둘은 서로를 모른 채 주제(topic) 라는 이름표 하나로만 이어집니다. 온도를 기록하는 데이터베이스, 경보를 울리는 알림 서버, 관리자 화면 — 셋이 같은 주제를 구독하면 센서는 여전히 한 번만 발행하면 됩니다. 센서 코드는 한 줄도 바뀌지 않습니다.

주제는 슬래시로 계층을 만들고, 구독할 때 와일드카드를 쓸 수 있습니다.

창고/냉동고A/온도      ← 이 하나만
창고/+/온도            ← 모든 냉동고의 온도 (+ 는 한 계층)
창고/#                 ← 창고 아래 전부 (# 는 남은 전체)

코드로 보면 이 정도입니다. 브로커 주소만 알면 나머지는 두 동사뿐입니다.

import mqtt from 'mqtt';
const client = mqtt.connect('mqtt://broker.local');

// 듣는 쪽
client.subscribe('창고/+/온도');
client.on('message', (topic, payload) => {
  console.log(topic, String(payload)); // 창고/냉동고A/온도 -18.2
});

// 말하는 쪽
client.publish('창고/냉동고A/온도', '-18.2');

끊김을 다루는 세 가지 장치

여기까지는 여느 발행/구독 시스템과 크게 다르지 않습니다. MQTT가 사물의 세계에서 살아남은 이유는 그다음에 있습니다.

첫째는 QoS, 전달 보증 수준입니다. 0은 그냥 던집니다(도착 여부를 확인하지 않음). 1은 도착할 때까지 다시 보냅니다(그래서 중복 도착할 수 있음). 2는 정확히 한 번만 도착하도록 왕복을 더 겁니다(그만큼 느림). 온도 값처럼 1초 뒤에 어차피 새 값이 오는 데이터는 0으로 충분하고, “문을 열어라” 같은 명령은 1이나 2가 필요합니다. 신뢰성을 공짜로 주는 대신, 메시지마다 값을 치를지 고르게 한 것입니다.

둘째는 보관 메시지(retained message) 입니다. 브로커는 주제마다 마지막 값 하나를 붙들고 있습니다. 관리자 화면을 새로 열었을 때, 다음 온도 보고가 올 때까지 몇 분씩 빈 화면을 보고 있을 이유가 없습니다. 구독하는 순간 마지막으로 알려진 값이 즉시 도착합니다. 상태를 다루는 시스템에서 이건 생각보다 큰 편의입니다.

셋째가 가장 MQTT다운 장치인데, 유언(Last Will and Testament) 입니다. 기기는 브로커에 접속하면서 미리 유언을 맡깁니다. “내가 인사도 없이 사라지거든, 이 주제에 ‘오프라인’이라고 대신 알려줘.” 이후 기기가 정말로 죽거나, 전원이 끊기거나, 네트워크가 사라지면 — 브로커가 그 부재를 감지하고 유언을 대신 발행합니다.

mqtt.connect('mqtt://broker.local', {
  will: { topic: '창고/냉동고A/상태', payload: '오프라인', retain: true },
});

부재를 설계에 포함한다는 것

유언이라는 발상 앞에서 잠깐 멈추게 됩니다. 대부분의 프로토콜은 연결이 살아 있는 동안 무엇을 주고받을지를 설계합니다. MQTT는 거기서 한 걸음 더 나가, 연결이 끊어진 뒤에 무슨 말이 남을지까지 설계해 둡니다. 사라짐을 예외 처리가 아니라 정상적인 사건으로 취급하는 것이지요.

이건 사막의 송유관에서 배운 태도일 겁니다. 위성 링크는 끊깁니다. 그것은 고쳐야 할 버그가 아니라 받아들여야 할 조건입니다. 조건을 부정하는 대신 조건 위에서 설계할 때, 오히려 더 단단한 물건이 나옵니다. MQTT가 스무 해가 지나도록 공장과 자동차와 집 안의 전구를 이어주고 있는 이유가 여기 있다고 생각합니다.

우리가 만드는 것들도 대개 그렇습니다. 사용자는 앱을 중간에 닫고, 지하철에서 신호가 끊기고, 결제는 응답 없이 멈춥니다. 그 순간을 예외라고 부르며 급히 가려낼 것인지, 아니면 처음부터 “이 일은 일어난다”고 전제하고 그때 남길 말을 미리 준비해 둘 것인지. MQTT는 후자를 택한 프로토콜이고, 저는 그 선택이 기술적 결정이라기보다 하나의 태도에 가깝다고 봅니다.