어젯밤 11시 19분, 텔레그램에 알림 두 개가 연달아 떴다.

[iot-lab] 🔇 device-…: 120s 이상 무응답
[iot-lab] device-…: offline (Last Will) — 연결이 비정상 종료됨

홈랩에서 돌리는 IoT 실험의 기기 하나(센서가 달린 오래된 노트북)가 브로커와의 연결을 잃었다는 뜻이다. 30분 뒤 기기는 스스로 다시 붙었다. 사람이 한 일은 없다.

이 글은 이 30분을 재료로, “기기가 조용해졌다” 를 어떻게 알아차리고 어떻게 말해야 하는지 정리한다. 결론부터 말하면 감지 장치는 둘이 필요하고, 알림은 “죽었다” 가 아니라 “모른다” 로 말해야 한다.

1. 30분의 타임라인

브로커(Eclipse Mosquitto) 로그와 탐지기 기록을 합치면 이렇다. (시각은 KST, 기기·네트워크 식별 정보는 뺐다)

시각 일 누가 알아챘나
23:16:59 브로커가 기기 연결을 끊음 — 사유 exceeded timeout 브로커 (Keep Alive)
23:16:59 기기가 미리 맡겨 둔 {"state":"offline"} 발행 브로커 (Last Will) → 탐지기 알림 ①
23:19 마지막 데이터 이후 120초 경과 탐지기 (무응답 타이머) → 알림 ②
23:46:35 기기 재접속, 데이터 재개 탐지기 → “다시 수신됨” 알림

기기는 10초마다 가속도 값을 보내고, MQTT 연결에는 Keep Alive 30초를 걸어 두었다. 이 숫자 두 개가 위 표의 모든 시각을 설명한다.

2. Keep Alive — 브로커가 “들리지 않음” 을 판정하는 규칙

MQTT 클라이언트는 접속할 때 Keep Alive 값을 선언한다. 그 시간 동안 보낼 데이터가 없으면 PINGREQ 라는 작은 신호라도 보낸다. 표준은 브로커 쪽 규칙을 이렇게 정한다.1

“If the Keep Alive value is non-zero and the Server does not receive an MQTT Control Packet from the Client within one and a half times the Keep Alive time period, it MUST close the Network Connection to the Client as if the network had failed.”

MQTT 3.1.1 도 같은 1.5배 규칙을 둔다.2 Keep Alive 30초면 45초 동안 아무 패킷도 오지 않을 때 브로커가 연결을 끊는다. 로그의 exceeded timeout 이 정확히 이 판정이다.

여기서 중요한 건 브로커는 원인을 모른다는 점이다. 기기가 꺼졌는지, 절전에 들어갔는지, 무선이 끊겼는지, 프로그램이 멈췄는지 구분할 수 없다. 아는 것은 “45초 동안 안 들렸다” 뿐이다.

3. Last Will — 기기가 미리 써 두는 유언

Keep Alive 로 끊긴 사실을 다른 구독자들은 어떻게 알까? 그게 Last Will(유언 메시지)이다. 기기는 접속할 때 “내가 비정상적으로 사라지면 이 토픽에 이 메시지를 대신 발행해 달라” 고 브로커에 맡겨 둔다.

MQTT 5.0 표준은 유언이 발행되는 상황을 이렇게 나열한다.1

“An I/O error or network failure detected by the Server. The Client fails to communicate within the Keep Alive time. The Client closes the Network Connection without first sending a DISCONNECT packet with a Reason Code 0x00 (Normal disconnection)…”

반대로 기기가 정상 종료(DISCONNECT, 사유 코드 0x00) 를 보내면 유언은 지워지고 발행되지 않는다. 그래서 유언은 “비정상 종료” 의 신호로 쓸 수 있다. 우리 기기는 정상 종료할 때 offline 을 직접 발행하고, 유언에도 같은 offline 을 걸어 두었다. 둘 다 retain 이라, 나중에 접속한 구독자도 마지막 상태를 바로 받는다.

4. 감지 장치가 둘이어야 하는 이유

이번 사건에서 알림은 둘 다 울렸다. 그런데 둘이 잡는 상황은 다르다.

상황 연결 데이터 Last Will 무응답 타이머
전원 꺼짐, 절전, 무선 끊김 끊김 (Keep Alive 초과) 멈춤 ✅ 발행 ✅
프로그램 강제 종료 끊김 (소켓 닫힘) 멈춤 ✅ 발행 ✅
센서 읽기 스레드만 멈춤 (네트워크 스레드는 살아서 PING 계속) 유지 멈춤 ❌ 안 울림 ✅
정상 종료 정상 해제 멈춤 ❌ (지워짐) ✅ — 단, 기기가 직접 offline 을 보냈으니 “의도된 종료” 로 구분 가능

세 번째 줄이 핵심이다. MQTT 클라이언트는 흔히 네트워크 처리를 별도 스레드나 이벤트 루프에서 하므로, 센서를 읽는 쪽이 멈춰도 PING 은 계속 나간다. 연결은 멀쩡하니 브로커도, Last Will 도 아무 일 없다고 본다. 이런 “살아 있지만 일하지 않는” 기기는 데이터가 끊긴 시간을 재는 타이머만 잡을 수 있다.

그래서 탐지기는 두 가지를 같이 본다.

  • Last Will — 연결 수준의 죽음을 빠르게 (이번엔 끊긴 그 순간)
  • 무응답 타이머 — 데이터 수준의 침묵을 늦게라도 확실하게 (이번엔 120초 뒤)

5. 알림은 “모른다” 로 말해야 한다

알림 문구를 “고장” 이나 “다운” 이 아니라 “무응답”, “연결이 비정상 종료됨” 으로 쓴 데는 이유가 있다. 2절에서 봤듯 브로커가 아는 건 “들리지 않는다” 뿐이다. 실제로 이번 원인은 고장이 아니었고, 기기는 30분 뒤 스스로 돌아왔다.

이건 판정을 셋으로 나누는 습관과 같다.

판정 이번 사례에서 다음 행동
살아 있음 10초마다 데이터 수신 없음
판단 불가 무응답 / 연결 끊김 — 원인 모름 기다리거나, 기기 쪽 로그를 확인
죽음 확인 기기 로그나 현장 확인으로 고장이 입증됨 수리·교체

“무응답” 을 곧바로 “죽음” 으로 처리하면, 절전이나 잠깐의 무선 끊김에도 자동 재부팅이나 교체 같은 과잉 대응이 따라온다. 무응답은 판단 불가 상태로 두고, 복구 알림(“다시 수신됨”)을 함께 보내는 게 맞다. 그래야 사람이 “30분 끊겼다가 스스로 돌아왔다” 는 전체 이야기를 받는다.

6. 튜닝할 때 고려할 것

Keep Alive 길이. 짧을수록 빨리 알지만 PING 이 잦아져 배터리와 트래픽을 쓴다. 무선이 불안정한 환경에서는 짧은 Keep Alive 가 잦은 끊김·재접속을 만든다. 표준대로 1.5배가 판정 기준이니, “몇 초 안에 알아야 하나” 에서 거꾸로 계산한다.

유언 지연(Will Delay Interval). MQTT 5.0 에는 연결이 끊긴 뒤 유언 발행을 미루는 설정이 있다. 표준에 따르면, 그 시간 안에 같은 클라이언트가 새 연결을 열면 유언은 발행되지 않는다.1 무선이 몇 초씩 끊겼다 붙는 기기라면, 이 값을 주는 것만으로 깜빡이는 offline 알림을 크게 줄일 수 있다.

무응답 타이머 길이. 데이터 주기의 몇 배로 잡는다. 이번엔 10초 주기에 120초였다. 너무 짧으면 일시적인 지연도 알림이 되고, 너무 길면 “살아 있지만 일하지 않는” 기기를 늦게 잡는다.

중복 억제와 복구 알림. 같은 기기의 같은 알림은 일정 시간 묶고, 복구는 반드시 알린다. 끊김 알림만 있고 복구 알림이 없으면, 사람은 그 기기가 지금도 죽어 있는지 매번 직접 확인해야 한다.

7. 체크리스트

  1. 기기는 접속 시 Last Will 을 걸고, 정상 종료할 때는 직접 offline 을 보낸다(둘 다 retain).
  2. Keep Alive 는 “몇 초 안에 알아야 하나 ÷ 1.5” 로 정한다.
  3. 연결 감시(Last Will)와 별도로 데이터 무응답 타이머를 둔다.
  4. 알림 문구는 “고장” 이 아니라 “무응답” — 판단 불가로 말한다.
  5. 복구 알림을 반드시 보낸다.
  6. 무선이 불안정하면 MQTT 5.0 의 Will Delay Interval 로 깜빡임을 흡수한다.

맺으며 — 조용함의 해석

IoT 모니터링에서 가장 흔한 신호는 오류 메시지가 아니라 침묵이다. 그리고 침묵은 해석을 요구한다. 브로커는 들리지 않는다는 사실만 알고, Last Will 은 연결이 비정상적으로 끝났다는 사실만 알고, 무응답 타이머는 데이터가 끊겼다는 사실만 안다. 셋 중 누구도 왜 인지는 모른다.

그래서 좋은 감시 시스템은 모르는 것을 모른다고 말한다. 이번 30분짜리 사건에서 알림 세 개(“무응답”, “비정상 종료”, “다시 수신됨”)가 한 일이 정확히 그것이었다. 원인 조사는 그다음, 기기 쪽 로그의 몫이다.


References

  1. OASIS, MQTT Version 5.0 (OASIS Standard, 2019) — 3.1.2.5 Will Flag, 3.1.2.10 Keep Alive. https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html ↩ ↩2 ↩3

  2. OASIS, MQTT Version 3.1.1 (OASIS Standard, 2014) — 3.1.2.10 Keep Alive. https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html ↩