← 전체 글
MQTT/약 9분 읽기/ 조회

HMI에서 죽은 MQTT 게이트웨이와 조용한 게이트웨이 구분하기

MQTT Last Will, Birth, retain 메시지로 SCADA 화면에서 오프라인·데이터 정지·정상을 서로 다른 상태로 표시하는 방법입니다.

MQTTSCADA태그네트워킹문제 해결프로젝트 노트

현장 민원은 대개 이렇게 시작합니다. "게이트웨이는 초록불인데 탱크 레벨이 한 시간째 안 움직여요." 초록불은 아직 열려 있는 TCP 소켓에서 나온 것이고, 레벨 값은 20분 전에 응답을 멈춘 뒤쪽 PLC에서 온 것입니다. 아이콘 하나가 서로 다른 두 질문에 답하고 있으니 최소한 한쪽은 틀립니다.

Last Will을 흔히 오해합니다

Last Will and Testament(LWT)은 클라이언트가 직접 publish하는 게 아닙니다. will 토픽과 페이로드를 CONNECT 패킷에 등록해 두면 브로커가 보관하고 있다가, 클라이언트가 정상 DISCONNECT 없이 사라지면 대신 발행합니다. 전원이 나간 장비는 자기 부고를 쓸 수 없으니 미리 맡겨 두는 방식입니다.

문제는 브로커가 "죽었다"고 판단하는 시점입니다. MQTT 3.1.1(OASIS, 3.1.2.10)에 따르면 keepalive의 1.5배 동안 패킷이 없으면 연결을 끊고 will을 발행합니다. keepalive 60초면 케이블을 뽑아도 최대 90초 동안 초록불이 유지됩니다. 중요한 설비라면 keepalive를 20~30초로 낮추고 PINGREQ 트래픽이 조금 느는 것을 감수하세요.

정상 종료는 반대입니다. 클라이언트가 DISCONNECT를 보내면 브로커는 will을 발행하지 않고 버립니다. 그래서 계획된 정지를 HMI에 표시하려면, 끊기 직전에 클라이언트가 스스로 online=false 메시지를 보내야 합니다. will은 비정상 종료만 처리합니다.

페이로드에 담을 것

true/false 하나만 보내면 새벽에 필요한 정보를 다 버리는 셈입니다.

{ "online": true, "source": "packaging-gw-02", "ts": "2026-06-11T14:08:31Z", "seq": 18422, "reason": "birth" }
  • ts — 게이트웨이가 상태를 만든 UTC 시각. HMI가 인지한 시각이 아닙니다. 나중에 정지 여부를 계산하는 근거가 됩니다.
  • seq — 발행마다 증가하는 카운터. onlinetrue인데 seq가 멈췄으면 죽은 게 아니라 조용한 것이고, 값이 역행하면 게이트웨이가 재시작된 것입니다.
  • reason — will 페이로드는 unexpected_disconnect, 종료 직전 직접 보내는 메시지는 planned_stop. HMI는 앞쪽만 알람으로 처리합니다.

상태 메시지는 retain=true로 두어 새 구독자가 접속 즉시 현재 상태를 받게 하세요. 반면 고속 공정값을 retain하면 안 됩니다. 여섯 시간 뒤 재접속한 구독자가 아침에 retain된 레벨 값을 실시간처럼 그립니다. 상태는 retain, 데이터는 timestamp입니다.

Sparkplug라면 이미 규격에 있습니다

Sparkplug B는 birth/death 전체를 규격에 넣어 두었으니 위에 다시 만들지 마세요. 노드는 NDEATH를 MQTT will로 등록하고 접속 시 NBIRTH를 발행하며, 뒤쪽 장비는 DBIRTH/DDEATH를 씁니다. bdSeq가 death와 birth를 짝지어 이전 세션의 낡은 will을 현재 것으로 오인하지 않게 하고, 메시지별 seq(0~255 순환)로 누락을 감지합니다. 평범한 MQTT에는 이런 보장이 전혀 없으니 직접 만들어야 합니다. 평범한 MQTT가 Sparkplug 보장을 공짜로 준다고 가정하는 게 흔한 실수입니다.

커미셔닝 때 무는 설정

  • Will Delay Interval(MQTT 5.0, 3.1.3.2) — will 발행을 N초 지연시킵니다. 재접속 시간보다 조금 크게 잡으면 3초짜리 Wi-Fi 끊김이 HMI에서 death→birth 깜빡임을 만들지 않습니다.
  • Session Expiry(MQTT 5.0) — 3.1.1의 cleanSession을 대체합니다. 짧은 재접속은 구독과 QoS 1/2 큐를 유지하되, 몇 시간씩 사라진 장비가 거대한 오프라인 큐를 쌓지 않도록 잡으세요.
  • QoS전달은 개선해도 신선도는 손대지 않습니다. QoS 2로 온 한 시간 전 값은 여전히 한 시간 전 값입니다.
  • Client ID — 같은 설정을 복사한 게이트웨이 두 대가 ID를 공유하면 오후 내내 서로를 브로커에서 밀어냅니다. 증상은 간헐적 네트워크 장애와 똑같아서, 두 장비의 단절 시각이 정확히 겹치는 걸 알아채기 전까지 스위치와 케이블을 하루종일 뒤집니다.

HMI에서는 boolean을 믿지 말고 정지를 계산하세요

online=true는 힌트일 뿐 데이터가 신선하다는 증거가 아닙니다. 중요한 값은 now - ts > 3 × 발행주기onlinetrue라도 Data_Stale로 표시하세요. 연결은 멀쩡한데 뒤쪽 PLC가 멈췄거나 폴링 드라이버가 폴트나거나 게이트웨이 앱이 멈춰서 새 값이 안 나올 수 있습니다.

  • 오프라인 소스는 정상 공정색이 아니라 별도의 정지/통신 색으로.
  • unexpected_disconnect는 짧은 디바운스 후 알람, planned_stop/maintenance는 이벤트로만.
  • 온라인↔오프라인 전환은 전부 historian에 기록. "요즘 좀 불안정해요"를 상관 분석 가능한 timestamp로 바꿔 줍니다.
  • 브로커 경로 장애와 장비 단위 will은 분리 표시. "브로커가 안 잡힘"과 "PLC 하나 죽음"은 다른 작업지시입니다.

실제로 돌리는 테스트

콘솔을 지켜보는 건 테스트가 아닙니다. 시각이 찍힌 체크리스트로 돌리세요.

  1. publisher 시작 — birth가 retain 플래그와 함께 나온다.
  2. 새 구독자 접속 — retain된 현재 상태를 즉시 받는다.
  3. 앱 정상 종료 — 계획 오프라인이 표시되고 will은 뜬다.
  4. 전원 차단/케이블 분리 — keepalive 1.5배 후 브로커가 will을 발행한다.
  5. 브로커 재시작 — 클라이언트가 복구되고 낡은 retain 데이터가 실시간으로 그려지지 않는다.
  6. 게이트웨이는 살린 채 뒤쪽 PLC만 끊기 — 장비 상태만 바뀌고 게이트웨이 상태는 그대로다.

6번은 다들 건너뛰지만 처음의 탱크 레벨 민원이 바로 이 경우입니다. 테스트 계획이 4번과 6번을 구분하지 못하면 HMI도 구분하지 못합니다.