현장에 설치된 IoT 시스템을 운영한다는 것 — 단절·상태 불일치·원격 관리

IoT 시스템 구축 프로젝트에서 시연은 대체로 잘 끝납니다. 사무실에 기기를 놓고 앱에서 버튼을 누르면 불이 켜집니다. 여기까지는 어렵지 않습니다.

문제는 그 기기가 건물 지하 기계실에, 다른 도시의 매장에, 또는 통신 환경이 좋지 않은 현장에 설치된 다음부터 시작됩니다.

앱에서는 켜져 있다고 나오는데 실제로는 꺼져 있습니다. 버튼을 눌렀는데 반응이 없다가 3분 뒤에 갑자기 동작합니다. 기기가 며칠째 오프라인인데 아무도 모르고 있었습니다. 펌웨어를 업데이트하려면 현장에 가야 합니다.

이 글은 IoT 시스템을 운영 단계까지 고려해서 설계할 때 필요한 것들을 정리한 내용입니다. 개별 기기를 연동하는 방법보다는, 수십에서 수백 대가 흩어져 있는 상태를 어떻게 관리할지에 초점을 맞췄습니다.


1. 기기가 살아 있다는 것을 어떻게 아는가

가장 기본적이면서 가장 자주 잘못 구현되는 부분입니다.

마지막 통신 시각만으로는 부족합니다

흔한 구현은 기기가 데이터를 보낼 때마다 마지막 통신 시각을 갱신하고, 일정 시간이 지나면 오프라인으로 표시하는 방식입니다.

이 방식의 문제는 기기가 조용한 것과 죽은 것을 구분하지 못한다는 점입니다. 상태 변화가 있을 때만 데이터를 보내는 기기라면, 아무 일도 일어나지 않은 밤사이에 오프라인으로 잘못 표시됩니다.

그래서 기기가 주기적으로 "나 살아 있다"는 신호를 보내는 구조가 필요합니다. 이 주기는 기기 특성과 통신 비용을 함께 고려해 정합니다.

판정 기준을 명시적으로 정합니다

온라인·오프라인 판정에 들어가는 값들입니다.

항목 내용
신호 주기 기기가 생존 신호를 보내는 간격
판정 임계 몇 주기를 놓치면 오프라인으로 볼 것인가
복귀 조건 몇 번 연속 수신되어야 온라인으로 되돌릴 것인가
알림 기준 오프라인 상태가 얼마나 지속되면 알릴 것인가

복귀 조건을 한 번으로 잡으면 상태가 심하게 흔들립니다. 통신이 불안정한 환경에서 온라인과 오프라인이 몇 분 간격으로 번갈아 표시되면, 알림이 쏟아지고 결국 알림을 꺼버리게 됩니다. 몇 번 연속 수신을 조건으로 두는 편이 안정적입니다.

판정은 수신 시점마다 하지 않고 주기 작업으로 돌리는 편이 단순합니다. 아래는 생존 신호를 세 주기 연속 놓친 기기를 통신 두절로 바꾸는 예시입니다. 세 주기라는 값은 설명용이며, 현장 통신 품질에 맞춰 조정합니다.

-- 가정: PostgreSQL 9.4 이상, 1분마다 실행하는 배치
-- device(heartbeat_interval_sec, last_seen_at, status, status_changed_at)
UPDATE device
SET status = 'OFFLINE',
    status_changed_at = now()
WHERE status = 'ONLINE'
  AND last_seen_at < now() - make_interval(secs => heartbeat_interval_sec * 3);

status = 'ONLINE' 조건 덕분에 점검 중이나 미등록 상태의 기기는 건드리지 않습니다. 알림은 이 갱신과 분리해서, status_changed_at 이후 정해둔 시간이 지난 기기만 대상으로 보냅니다.

MQTT를 쓴다면 keep alive와 Last Will을 함께 씁니다

MQTT에는 연결 생존을 확인하는 장치가 프로토콜에 들어 있습니다. 직접 만들기 전에 이 동작을 정확히 알고 쓰는 편이 낫습니다.

  • Keep Alive — 클라이언트는 보낼 패킷이 없어도 이 간격 안에 PINGREQ를 보내야 하고, 브로커는 Keep Alive의 1.5배 동안 아무 패킷도 받지 못하면 연결을 끊습니다. 0으로 설정하면 이 동작이 꺼집니다.(MQTT 3.1.1, 3.1.2.10) MQTT 5.0에서는 브로커가 CONNACK의 Server Keep Alive로 값을 지정할 수 있고, 이때 클라이언트는 자신이 보낸 값 대신 그 값을 써야 합니다.(MQTT 5.0, 3.2.2.3.14)
  • Last Will — 연결할 때 유언 메시지를 등록해두면, 연결이 비정상적으로 닫혔을 때 브로커가 대신 발행합니다. 클라이언트가 DISCONNECT를 보내고 정상 종료하면 브로커는 유언을 발행하지 않고 버립니다.(MQTT 3.1.1, 3.1.2.5 / 3.14.4)
  • Retained 메시지 — RETAIN 플래그를 붙여 발행하면 브로커가 토픽당 마지막 한 건을 보관했다가 이후 구독자에게 전달합니다. 페이로드가 0바이트인 retained 메시지를 발행하면 보관된 메시지가 지워집니다.(MQTT 3.1.1, 3.3.1.3)

이 셋을 묶으면 기기별 상태 토픽 하나로 온라인 여부를 표현할 수 있습니다. 접속하면 online을, 유언으로는 offline을 같은 토픽에 retained로 남기는 구성입니다. 나중에 접속한 대시보드도 구독하는 즉시 마지막 상태를 받습니다.

# 가정: Python 3, paho-mqtt 2.x, MQTT 5.0 브로커, TLS(8883)
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

DEVICE_ID = "dev-0001"
STATUS_TOPIC = f"devices/{DEVICE_ID}/status"

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        return
    client.publish(STATUS_TOPIC, "online", qos=1, retain=True)
    if not flags.session_present:  # 브로커에 세션이 남아 있지 않을 때만 다시 구독
        client.subscribe(f"devices/{DEVICE_ID}/commands", qos=1)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=DEVICE_ID, protocol=mqtt.MQTTv5)
client.tls_set()

will_properties = Properties(PacketTypes.WILLMESSAGE)
will_properties.WillDelayInterval = 30        # 30초 안에 재접속하면 유언을 보내지 않음
client.will_set(STATUS_TOPIC, "offline", qos=1, retain=True,
                properties=will_properties)

connect_properties = Properties(PacketTypes.CONNECT)
connect_properties.SessionExpiryInterval = 3600  # 끊긴 뒤 1시간 동안 세션 유지
client.on_connect = on_connect
client.connect("broker.example.com", 8883, keepalive=60,
               clean_start=False, properties=connect_properties)
client.loop_forever()

버전에 따라 달라지는 부분입니다.

항목 MQTT 3.1.1 MQTT 5.0
세션 유지 Clean Session 플래그 하나로 결정. 만료 시간은 규격에 없음 Clean Start와 Session Expiry Interval로 분리. 생략하면 0(연결 종료 시 세션 종료), 0xFFFFFFFF는 만료 없음
유언 지연 없음. 끊기면 바로 발행 Will Delay Interval. 지연 시간과 세션 종료 중 먼저 오는 시점에 발행하고, 그 전에 재접속하면 발행하지 않음
메시지 만료 없음 Message Expiry Interval. 만료된 메시지는 구독자에게 전달하지 않음
응답 코드 PUBACK에 실패 사유가 없음 모든 응답 패킷에 Reason Code

출처는 MQTT 5.0 규격의 3.1.2.11.2, 3.1.3.2.2, 3.3.2.3.3과 부록 C입니다.(MQTT 5.0) Clean Start 1과 Session Expiry Interval 0의 조합이 3.1.1의 Clean Session 1과 같습니다. Will Delay Interval은 앞에서 말한 "복귀 조건을 한 번으로 잡으면 흔들린다"는 문제를 브로커 단에서 줄여주므로, 5.0을 쓸 수 있다면 켜두는 편이 좋습니다.

현장에서 자주 만나는 증상과 원인입니다.

증상 원인 해결
기기를 정상 종료했는데 계속 온라인으로 표시 정상 DISCONNECT에서는 유언이 발행되지 않음 종료 직전에 offline을 직접 발행
전원을 뽑았는데 한참 뒤에야 오프라인 표시 브로커는 Keep Alive의 1.5배가 지나야 끊김을 인지 Keep Alive를 줄이되 회선 요금과 배터리를 함께 고려
폐기한 기기가 계속 오프라인 목록에 남음 retained 상태 메시지가 브로커에 남아 있음 폐기 절차에 0바이트 retained 발행을 포함
재접속 후 명령이 오지 않음 세션이 만료되어 구독이 사라졌는데 다시 구독하지 않음 접속 응답의 Session Present를 확인하고 없으면 재구독
재접속 직후 오래된 명령이 한꺼번에 실행 세션에 쌓여 있던 QoS 1 메시지가 전달됨 명령에 유효 기간을 두고 기기에서 검사(2장 참고)

한 가지 한계는 알고 써야 합니다. Keep Alive와 유언이 알려주는 것은 브로커와의 연결이 살아 있는지까지입니다. 프로세스는 접속을 유지하고 있는데 센서 읽기나 제어 루프가 멈춘 상태는 잡지 못합니다. 그래서 MQTT를 쓰더라도 마지막 측정 시각, 가동 시간 같은 값을 담은 애플리케이션 수준의 생존 신호는 따로 두는 편이 안전합니다.

기기 교체와 재설치를 전제에 넣습니다

현장 기기는 고장 나고 교체됩니다. 이때 시스템에서 어떻게 처리할지가 정해져 있지 않으면, 교체할 때마다 데이터가 끊기거나 이력이 사라집니다.

구분해야 할 개념이 두 가지입니다.

  • 물리적 기기 — 실제 장치. 교체되면 다른 개체가 됩니다
  • 논리적 위치 — "3층 회의실 조명"이라는 역할. 기기가 바뀌어도 유지됩니다

이력과 통계는 논리적 위치 기준으로 쌓여야 합니다. 기기 기준으로 쌓으면 교체하는 순간 그 위치의 과거 데이터를 볼 수 없게 됩니다.

교체 절차에서 정할 것들입니다.

  • 기존 기기를 삭제할 것인가, 비활성 상태로 보존할 것인가
  • 새 기기를 같은 위치에 연결하는 절차는 무엇인가
  • 교체 시점을 기록에 남기는가
  • 교체 전후의 데이터를 연결해서 볼 수 있는가

삭제보다 비활성 처리를 권합니다. 삭제하면 그 기기가 남긴 과거 로그의 참조가 깨집니다. 나중에 이력을 조회할 때 "알 수 없는 기기"로 표시되는 상황이 생깁니다.

상태를 세 가지 이상으로 나눕니다

온라인과 오프라인 두 가지만으로는 현장 상황을 담기 어렵습니다. 실무에서 필요한 구분입니다.

  • 정상 — 통신도 되고 응답도 정상
  • 응답 지연 — 통신은 되지만 명령 처리가 느림
  • 통신 두절 — 신호가 끊김
  • 미등록 — 설치는 되었으나 시스템에 등록되지 않음
  • 점검 중 — 의도적으로 제외된 상태

점검 중 상태가 특히 유용합니다. 설비 교체나 공사로 기기를 의도적으로 껐는데 장애 알림이 계속 오는 상황을 막을 수 있습니다.


2. 명령이 전달되었다는 보장이 없습니다

제어 명령은 네트워크를 건너가므로, 도중에 유실될 수 있습니다. 이것을 전제로 설계해야 합니다.

요청과 확인을 분리합니다

버튼을 눌렀을 때 화면이 곧바로 "켜짐"으로 바뀌는 구현이 있습니다. 사용자에게는 반응이 빨라 보이지만, 실제로 켜졌는지는 확인되지 않은 상태입니다.

명령의 생애를 단계로 나누면 정확하게 표현할 수 있습니다.

① 요청 접수 — 사용자 조작을 시스템이 받음
② 전송     — 기기 또는 허브로 명령을 보냄
③ 수신 확인 — 기기가 명령을 받았음을 알림
④ 상태 반영 — 기기의 실제 상태가 바뀐 것을 확인

화면에는 이 단계가 드러나야 합니다. ①에서 바로 완료로 표시하지 말고, ④까지 확인된 뒤에 최종 상태로 바꾸는 편이 정확합니다. 그 사이에는 진행 중임을 표시합니다.

응답이 없을 때의 처리를 정합니다

일정 시간 안에 확인이 오지 않으면 어떻게 할지 정해야 합니다.

  • 재시도할 것인가, 몇 번까지 할 것인가
  • 재시도해도 안전한 명령인가 — 토글 방식 명령은 재시도 시 원래대로 돌아갑니다
  • 최종 실패를 사용자에게 어떻게 알릴 것인가
  • 실패한 명령을 기록으로 남기는가

토글 명령의 위험성은 자주 간과됩니다. "현재 상태를 반대로 바꿔라"는 명령을 두 번 보내면 원위치입니다. 명령은 "켜라", "꺼라"처럼 목표 상태를 지정하는 형태로 설계하는 편이 안전합니다. 이러면 여러 번 보내도 결과가 같습니다.

전송 품질 등급이 보장하는 범위를 구분합니다

MQTT의 QoS는 "전달 보장"으로 뭉뚱그려 이해되는 경우가 많습니다. 실제로 보장하는 범위는 좁습니다.

QoS 규격의 정의 명령 전달에 쓸 때
0 한 번 도착하거나 도착하지 않음. 응답도 재전송도 없음 주기 센서 값처럼 한 건이 빠져도 되는 데이터
1 최소 한 번 도착. PUBACK으로 확인 제어 명령의 기본값. 중복 수신을 전제로 처리
2 유실도 중복도 허용하지 않을 때. 4단계 교환 왕복이 늘고, 지원하지 않는 서비스가 있음

정의는 MQTT 3.1.1 규격 4.3절을 따랐습니다.(MQTT 3.1.1, 4.3) 관리형 브로커를 쓴다면 지원 범위를 먼저 확인합니다. 예를 들어 AWS IoT Core는 QoS 2를 지원하지 않습니다.(AWS IoT Core MQTT)

설계에서 놓치기 쉬운 지점이 세 가지 있습니다.

QoS는 구간별 보장입니다. PUBACK은 브로커가 메시지를 넘겨받았다는 뜻이지, 기기가 받았다는 뜻도 실행했다는 뜻도 아닙니다. 앞에서 나눈 ③ 수신 확인과 ④ 상태 반영은 기기가 별도 토픽으로 응답을 보내도록 직접 만들어야 합니다.

규격이 요구하는 재전송은 재접속할 때뿐입니다. 세션을 유지한 채 재접속하면 확인받지 못한 QoS 1·2 메시지를 다시 보내야 한다는 것이 규격이 정한 유일한 재전송 조건입니다. 5.0은 그 밖의 시점에 재전송하는 것을 금지하기까지 합니다.(MQTT 3.1.1, 4.4, MQTT 5.0, 4.4) 연결은 살아 있는데 기기가 응답하지 않는 경우는 프로토콜이 처리해주지 않으므로, 응답 대기 시간과 재시도는 애플리케이션에서 관리합니다.

QoS 1에서는 같은 명령이 두 번 올 수 있습니다. 그래서 명령마다 고유 식별자를 붙이고, 기기는 이미 처리한 식별자를 다시 실행하지 않아야 합니다. 서버 간 호출에서 쓰는 멱등성 키와 같은 원리이며, 외부 시스템 연동 글에서 자세히 다뤘습니다.

명령 메시지와 기기 쪽 처리의 최소 형태입니다.

{
  "id": "0b6f6f0e-3c1e-4d0a-9a53-6a1d0c1f7b21",
  "target": { "power": "on" },
  "expires_at": 1791874800
}

expires_at은 유닉스 시각(초)이며 값은 예시입니다.

# 가정: Python 3. seen 은 재부팅 뒤에도 남도록 실제로는 파일이나 플래시에 저장
import json
import time

seen = {}  # 명령 id -> 처리 결과

def handle_command(raw, apply, now=time.time):
    command = json.loads(raw)
    if command["id"] in seen:
        return seen[command["id"]]      # 중복 수신: 다시 실행하지 않고 이전 결과만 회신
    if now() > command["expires_at"]:
        result = {"id": command["id"], "status": "EXPIRED"}
    else:
        apply(command["target"])        # 목표 상태 적용
        result = {"id": command["id"], "status": "APPLIED"}
    seen[command["id"]] = result
    return result

반환값은 기기가 응답 토픽으로 발행하고, 서버는 같은 id의 응답이 여러 번 와도 한 번만 반영합니다. 처리한 식별자 목록은 무한히 쌓이지 않도록 유효 기간이 지난 것부터 지웁니다.

오프라인 기기에 대한 명령을 정의합니다

기기가 오프라인인데 사용자가 제어를 시도하면 어떻게 할지 정해야 합니다. 선택지는 세 가지입니다.

방식 동작 적합한 경우
즉시 거부 오프라인 상태를 알리고 명령을 받지 않음 즉시성이 중요한 제어
보류 후 전달 명령을 저장해두고 기기 복귀 시 전달 설정 변경, 스케줄 반영
무시 명령을 버림 권하지 않음

보류 후 전달을 선택한다면 유효 기간을 함께 정해야 합니다. 3일 전에 누른 "조명 켜기"가 기기 복귀 시점에 실행되면 곤란합니다. 일정 시간이 지난 보류 명령은 폐기하는 규칙이 필요합니다.

MQTT로 보류를 구현한다면 세션 유지와 QoS 1 구독을 조합합니다. 기기가 끊긴 동안 브로커가 메시지를 세션에 쌓아두었다가 재접속 시 전달합니다. 이때 유효 기간을 거는 자리가 버전마다 다릅니다.

  • MQTT 5.0 — 발행할 때 Message Expiry Interval을 지정하면 만료된 메시지를 브로커가 전달하지 않습니다. 세션 자체의 보관 시간은 Session Expiry Interval로 정합니다
  • MQTT 3.1.1 — 규격에 만료 개념이 없으므로 위 예시처럼 페이로드에 만료 시각을 넣고 기기에서 검사합니다. 세션 보관 기간은 브로커 설정에 따릅니다. AWS IoT Core는 MQTT 3 영구 세션의 기본 만료 시간이 1시간이며, 세션이 만료되면 쌓여 있던 메시지는 버려집니다(AWS IoT Core MQTT)

기기에서 만료 시각을 검사하는 방식은 기기 시계가 맞다는 전제가 필요합니다. 시계를 믿기 어려운 기기라면 브로커가 만료를 처리하는 5.0 방식이 더 안전합니다. 시계 문제는 3장에서 다시 다룹니다.

보류 명령을 메시지 큐에 쌓는 대신 "원하는 상태" 하나만 서버에 저장해두는 방법도 있습니다. 조명을 켰다 껐다 다섯 번 눌렀더라도 기기가 복귀했을 때 필요한 것은 마지막 값 하나이기 때문입니다. 이 방식은 다음 장의 desired/reported 모델로 이어집니다.


3. 화면의 상태와 실제 상태가 어긋납니다

운영 중 문의의 상당 부분이 여기서 나옵니다. "앱에는 꺼져 있다고 나오는데 실제로는 켜져 있어요."

어긋나는 경로는 정해져 있습니다

  • 현장에서 물리 스위치로 직접 조작함
  • 기기가 자체 스케줄이나 리모컨으로 동작함
  • 정전 후 복구되면서 기기 기본 상태로 돌아감
  • 명령은 전달됐지만 기기가 실행하지 못함
  • 상태 변경 알림이 네트워크에서 유실됨

이 중 물리 스위치 조작과 정전 복구가 가장 흔합니다. 둘 다 시스템이 개입하지 않은 상태 변화입니다.

주기적으로 실제 상태를 맞춥니다

기기가 상태 변화를 알려주는 것에만 의존하면 위 경로들을 놓칩니다. 일정 주기로 실제 상태를 조회해 시스템 기록과 비교하는 절차가 필요합니다.

정할 항목입니다.

  • 동기화 주기 — 짧을수록 정확하지만 통신량과 부하가 늘어납니다
  • 불일치 시 기준 — 기기의 실제 상태를 정답으로 볼 것인가, 시스템이 의도한 상태로 되돌릴 것인가
  • 기록 여부 — 불일치가 발견된 사실 자체를 로그로 남기는가

불일치 시 기준은 신중하게 정해야 합니다. 시스템 의도대로 되돌리는 방식은 현장에서 사람이 끈 것을 다시 켜는 결과가 될 수 있습니다. 대부분의 경우 실제 상태를 인정하고 기록만 남기는 편이 안전합니다.

원하는 상태와 보고된 상태를 따로 저장합니다

화면과 기기가 어긋나는 문제를 구조로 다루는 방법이 desired/reported 모델입니다. 서버가 기기마다 문서 하나를 두고, 시스템이 원하는 상태(desired) 와 기기가 마지막으로 보고한 상태(reported) 를 나눠 저장합니다. AWS IoT의 디바이스 섀도와 Azure IoT Hub의 디바이스 트윈이 이 구조이고, 직접 구축하는 시스템에도 그대로 적용할 수 있습니다.

{
  "state": {
    "desired":  { "power": "on",  "target_temp": 24 },
    "reported": { "power": "off", "target_temp": 24 },
    "delta":    { "power": "on" }
  },
  "version": 18
}

위는 AWS 섀도 문서 형식을 줄인 예시입니다. 동작 규칙은 다음과 같습니다.

  • 앱은 desired만 쓰고, 기기는 reported만 씁니다. 화면에는 reported를 보여주고, 두 값이 다르면 "반영 중"으로 표시합니다. 2장에서 나눈 명령의 단계가 문서 한 장에 담깁니다
  • 차이(delta)는 desired에 있는 항목 중 reported와 값이 다르거나 reported에 없는 것만 포함합니다. reported에만 있는 항목은 포함되지 않습니다(AWS IoT Device Shadow 문서 구조)
  • 문서는 갱신될 때마다 버전이 올라갑니다. 갱신 요청에 버전을 넣으면 최신 버전과 일치할 때만 처리되고, 기기는 자신이 가진 것보다 낮은 버전의 메시지를 무시할 수 있습니다. 순서가 뒤바뀌어 도착한 메시지를 걸러내는 장치입니다
  • Azure 트윈은 desired와 reported 각각에 증가만 하는 $version을 두고, 문서 전체에는 ETag를 둡니다. 크기는 desired와 reported 각각 32KB로 제한됩니다(Azure IoT Hub 디바이스 트윈)

재접속 절차가 핵심입니다. Azure IoT Hub는 끊겨 있던 기기를 위해 desired 변경 알림을 보관하지 않습니다. 그래서 문서가 권하는 순서는 접속, 변경 알림 구독, desired 전체 조회이며, 조회한 문서의 버전보다 낮거나 같은 알림은 무시합니다. 구독보다 조회를 먼저 하면 그 사이의 변경을 놓칩니다. AWS 섀도는 /get 토픽에 빈 메시지를 발행하면 전체 문서를 돌려주므로, 접속 직후 한 번 조회하도록 구현합니다.(AWS IoT Device Shadow MQTT 토픽)

이 모델에서 자주 나는 문제입니다.

증상 원인 해결
현장에서 끈 조명이 잠시 뒤 다시 켜짐 desired에 on이 남아 있어, 기기가 off를 보고할 때마다 차이가 다시 생김 수동 조작을 감지하면 reported와 함께 desired도 같은 값으로 갱신하거나 해당 항목을 null로 지움
재접속 후 설정이 예전 값으로 남음 끊긴 동안의 변경 알림을 받지 못했고 전체 조회를 하지 않음 접속할 때마다 구독 후 전체 조회
값이 이전 상태로 되돌아감 늦게 도착한 오래된 메시지를 그대로 적용 버전을 비교해 낮은 것은 버림
목록의 일부만 바꿨는데 전체가 바뀜 배열은 값 하나로 취급되어 통째로 교체됨 부분 갱신이 필요한 데이터는 키가 있는 객체로 설계
"반영 중" 표시가 끝나지 않음 기기가 지원하지 않는 값이 desired에 들어감 기기가 거부 사유를 reported에 남기고, 서버는 일정 시간 뒤 실패로 처리

첫 번째 행이 앞에서 말한 "불일치 시 기준" 문제의 구체적인 모습입니다. desired를 그대로 두면 시스템이 의도한 상태로 계속 되돌리는 동작이 되고, 수동 조작 시 desired를 맞춰주면 실제 상태를 인정하는 동작이 됩니다. 어느 쪽으로 할지를 항목별로 정해둡니다. 스케줄로 정한 냉방 설정 온도는 되돌리고, 조명 전원은 현장 조작을 인정하는 식입니다.

이력은 이 문서에 넣지 않습니다. 문서에는 마지막 값만 남으므로, 시계열로 봐야 하는 측정값과 제어 이력은 별도 메시지로 보내 따로 쌓습니다.

정전 복구 시나리오를 별도로 다룹니다

정전이 나면 여러 기기가 동시에 재기동합니다. 이때 두 가지를 정해야 합니다.

  • 기기 기본 상태 — 전원이 들어오면 켜진 상태로 돌아갈 것인가, 꺼진 상태로 있을 것인가. 기기 설정으로 지정할 수 있는 경우가 많습니다
  • 복구 후 동작 — 정전 전 상태로 되돌릴 것인가, 현재 시각의 스케줄을 적용할 것인가

동시 재기동은 건물 전기요금 글에서 다룬 전력 피크 문제와도 연결됩니다. 복구 시에도 순차적으로 올리는 구성이 필요합니다.

끊긴 동안의 데이터는 단말에 쌓았다가 보냅니다

통신이 끊긴 동안 측정값과 처리 결과를 버리면, 복구 뒤 그래프에 구멍이 생기고 정산이나 통계가 맞지 않게 됩니다. 단말이 로컬 저장소에 먼저 기록하고, 전송에 성공한 것만 지우는 구조가 기본입니다.

-- 가정: 단말의 SQLite
CREATE TABLE outbox (
  seq         INTEGER PRIMARY KEY AUTOINCREMENT,
  topic       TEXT    NOT NULL,
  payload     TEXT    NOT NULL,
  measured_at INTEGER,
  sent_at     INTEGER
);

-- 전송 대상: 오래된 것부터 정해진 개수만
SELECT seq, topic, payload FROM outbox
WHERE sent_at IS NULL ORDER BY seq LIMIT 100;

-- 저장 한도: 최근 50,000건만 유지(값은 예시)
DELETE FROM outbox WHERE seq <= (SELECT max(seq) FROM outbox) - 50000;

서버는 같은 건이 두 번 와도 한 번만 저장해야 합니다. 단말이 전송에는 성공했지만 응답을 받지 못한 경우 같은 건을 다시 보내기 때문입니다.

-- 가정: PostgreSQL 9.5 이상, telemetry 에 (device_id, seq) 유니크 제약
INSERT INTO telemetry (device_id, seq, measured_at, received_at, payload)
VALUES ($1, $2, $3, now(), $4)
ON CONFLICT (device_id, seq) DO NOTHING;

함께 정해야 할 것들입니다.

  • 저장 한도와 버릴 순서 — 저장소는 유한합니다. 가득 찼을 때 오래된 것부터 버릴지, 중요도가 낮은 것부터 버릴지 정합니다. 정산에 쓰이는 처리 결과와 주기 센서 값은 테이블을 나눠 한도를 따로 둡니다
  • 복구 직후의 전송 속도 — 며칠 치를 한꺼번에 올리면 그 자체가 부하가 됩니다. 한 번에 보내는 건수를 제한하고, 지금 발생한 이벤트가 밀리지 않도록 실시간 데이터를 우선합니다
  • 순번의 유효 범위 — 단말을 교체하거나 저장소를 초기화하면 순번이 1부터 다시 시작해 과거 데이터와 충돌합니다. 설치할 때 생성한 식별자를 함께 보내 유니크 키에 포함합니다
  • 쓰기 수명 — 플래시 저장소는 쓰기 횟수에 한계가 있으므로 매 측정마다 기록하지 않고 묶어서 기록하는 방식을 검토합니다

저희가 개발한 제로살롱 KIOSK도 서버 연결이 끊긴 동안 장치 제어와 화면 진행은 계속하고, 연결이 복구되면 결과를 다시 전송하는 구조로 만들었습니다. 설치 장소마다 통신 품질이 달랐기 때문에 이 경로는 예외 처리가 아니라 일상적으로 쓰이는 경로였습니다.

단말 시계를 그대로 믿지 않습니다

쌓아둔 데이터를 나중에 보내려면 "언제 측정한 값인가"가 정확해야 합니다. 그런데 단말 시계는 생각보다 자주 틀립니다. 배터리로 유지되는 시계가 없는 단말은 전원이 들어온 직후 초기값에서 시작하고, 시각 동기화가 끝나기 전에 측정이 먼저 시작됩니다. 네트워크가 끊긴 채 부팅하면 동기화 자체가 되지 않습니다.

  • 시각을 두 개 저장합니다 — 단말이 붙인 측정 시각과 서버가 받은 수신 시각을 모두 남깁니다. 차이가 비정상적으로 큰 건은 표시해두고 통계에서 구분합니다
  • 동기화 전에는 순번과 경과 시간으로 기록합니다 — 부팅 후 경과 시간과 순번만 기록해두었다가, 동기화가 끝난 시점을 기준으로 실제 시각을 역산합니다
  • 순서 판단에 시각을 쓰지 않습니다 — 어느 값이 최신인지는 버전이나 순번으로 판단합니다. 시계가 뒤로 가는 순간 시각 기반 비교는 깨집니다
  • 유효 기간 검사에 주의합니다 — 시계가 초기값인 단말은 만료된 명령을 유효하다고 판단합니다. 동기화가 확인되기 전에는 유효 기간이 있는 명령을 실행하지 않도록 합니다
  • 인증서 검증도 시계에 의존합니다 — TLS 인증서의 유효 기간 검사가 실패해 접속 자체가 안 되는 경우가 있습니다. 시각 동기화가 접속보다 먼저 끝나도록 기동 순서를 잡습니다

서버가 보내는 메시지에 서버 시각을 실어주는 것도 방법입니다. AWS 섀도는 응답에 타임스탬프를 넣어, 시계가 없는 기기도 각 값이 얼마나 오래된 것인지 계산할 수 있게 합니다.(AWS IoT Device Shadow 문서 구조)


4. 불필요한 통신이 쌓입니다

현장 기기가 늘어나면 통신량이 빠르게 증가합니다. 각각은 작아 보여도 합쳐지면 서버 부하와 통신 비용이 됩니다.

폴링 주기는 목적에 따라 나눕니다

모든 데이터를 같은 주기로 가져올 이유는 없습니다.

데이터 필요한 신선도 적정 주기
제어 명령 대기 즉시 실시간 연결 또는 짧은 주기
기기 온·오프 상태 수 분 이내 중간 주기
온도·습도 센서 수 분~수십 분 긴 주기
누적 사용량 시간 단위 매우 긴 주기

가능하면 주기적 조회보다 이벤트 전달 방식이 효율적입니다. 값이 바뀔 때만 보내면 통신량이 크게 줄어듭니다. 다만 앞서 다룬 것처럼 생존 확인용 주기 신호는 별도로 필요합니다.

쓰지 않는 기능의 폴링을 끕니다

실제로 겪은 유형의 문제입니다. 특정 기능을 사용하지 않는 현장의 단말이 그 기능 관련 조회를 5초마다 계속 호출하고 있었습니다. 단말 설정에 해당 기능 사용 여부를 나타내는 값이 없어서, 조건 분기 자체가 불가능한 구조였습니다.

단말 한 대라면 무시할 수준이지만, 수백 대가 같은 동작을 하면 이야기가 달라집니다.

설계 단계에서 확인할 것들입니다.

  • 단말 설정에 기능별 사용 여부 플래그가 있는가
  • 폴링 루프가 그 플래그를 확인하고 시작하는가
  • 설정이 변경되면 실행 중인 루프에 반영되는가
  • 필요 없는 상황에서 루프를 멈출 수 있는가

기능을 켜고 끄는 설정이 단말까지 전달되지 않으면, 나중에 서버에서 아무리 막아도 요청 자체는 계속 들어옵니다.

보내는 데이터의 크기도 줄입니다

호출 횟수만큼이나 한 번에 주고받는 양도 비용이 됩니다. 특히 이동통신 회선을 쓰는 단말이라면 직접적인 요금으로 이어집니다.

  • 변화분만 전송 — 전체 상태를 매번 보내지 않고 바뀐 항목만 보냅니다
  • 묶어서 전송 — 센서 값을 매번 개별 전송하지 않고 일정 개수를 모아 한 번에 보냅니다
  • 불필요한 필드 제거 — 서버가 쓰지 않는 값을 계속 실어 보내는 경우가 있습니다
  • 압축 적용 — 데이터가 크다면 전송 전 압축을 검토합니다

묶어서 보내는 방식에서는 지연을 허용할 수 있는지 먼저 확인해야 합니다. 온도 값은 몇 분 늦어도 되지만, 이상 감지 신호는 즉시 보내야 합니다. 데이터 성격에 따라 즉시 전송 대상과 묶음 전송 대상을 나눕니다.

재시도가 몰리지 않게 합니다

서버가 잠시 불안정해지면 모든 단말이 동시에 재시도합니다. 회복하려는 서버에 부하가 몰려 복구가 늦어집니다.

  • 재시도 간격을 늘려가는 방식을 씁니다
  • 간격에 무작위 값을 섞어 시점을 분산시킵니다
  • 단말마다 최초 연결 시각을 조금씩 어긋나게 합니다

간격만 늘리고 무작위 값을 섞지 않으면, 같은 순간에 끊긴 단말들이 같은 시각에 다시 몰립니다. 대기 시간을 0부터 상한 사이에서 무작위로 고르는 방식이 구현도 단순하고 분산 효과가 좋습니다.(AWS Architecture Blog, Exponential Backoff And Jitter)

# 가정: Python 3. base 1초, 상한 300초는 예시 값
import random

def retry_delay(attempt, base=1.0, cap=300.0):
    return random.uniform(0, min(cap, base * 2 ** attempt))

상한은 반드시 둡니다. 상한이 없으면 오래 끊겼던 단말이 서버가 복구된 뒤에도 몇 시간씩 재접속을 시도하지 않습니다. 접속에 성공하면 시도 횟수를 0으로 되돌립니다. 정전 복구처럼 전 단말이 동시에 부팅하는 경우를 위해, 부팅 후 첫 접속에도 같은 방식의 무작위 지연을 넣습니다.


5. 현장에 가지 않고 관리할 수 있어야 합니다

설치 지점이 흩어져 있으면 현장 방문 비용이 운영비의 큰 부분이 됩니다.

원격으로 확인할 수 있는 정보

  • 단말·기기의 온라인 상태와 마지막 통신 시각
  • 설치된 소프트웨어·펌웨어 버전
  • 최근 오류 이력
  • 설정 값의 현재 상태
  • 통신 품질 지표

버전 정보는 반드시 포함되어야 합니다. 문제가 생겼을 때 그 단말이 어떤 버전으로 돌고 있는지 모르면 원인 파악이 불가능합니다.

원격 업데이트에는 안전장치가 필요합니다

  • 단계적 배포 — 일부에 먼저 적용하고 이상이 없으면 확대합니다
  • 롤백 — 문제 발생 시 이전 버전으로 되돌릴 수 있어야 합니다
  • 적용 시각 지정 — 운영 시간 중 업데이트로 서비스가 끊기지 않게 합니다
  • 실패 처리 — 업데이트 중 전원이 꺼지거나 통신이 끊긴 단말이 동작 불능 상태가 되지 않아야 합니다

마지막 항목이 가장 중요합니다. 업데이트 실패로 단말이 부팅되지 않으면 결국 현장에 가야 합니다. 이전 버전을 보존해두고 새 버전이 정상 동작을 확인한 뒤에 전환하는 방식이 일반적입니다.

업데이트 실패에서 스스로 돌아오게 만듭니다

이전 버전을 보존하는 표준적인 방법이 A/B 구성입니다. 저장소에 실행 영역을 두 벌 두고, 지금 돌고 있지 않은 쪽에 새 버전을 씁니다. 쓰는 도중 전원이 꺼져도 실행 중인 영역은 손대지 않았으므로 그대로 부팅됩니다.

① 내려받기  — 비활성 영역에 새 이미지를 기록
② 검증      — 서명과 해시를 확인. 실패하면 여기서 중단
③ 전환      — 다음 부팅 대상을 새 영역으로 표시하고 재시작
④ 자가 점검 — 새 버전이 부팅 후 서버 접속, 주변 장치 통신 등을 확인
⑤ 확정      — 점검을 통과하면 새 영역을 정상으로 표시
              확정 전에 재시작되면 부트로더가 이전 영역으로 복귀

플랫폼별로 이 구조가 이미 구현되어 있습니다.

  • Android — A/B 업데이트는 슬롯 두 개를 쓰고, 새 OS가 부팅에 실패하면 부트로더가 이전 슬롯으로 돌아갑니다. 부팅 후 무결성 검사를 거쳐 성공으로 표시되어야 확정됩니다(Android A/B 시스템 업데이트)
  • ESP32(ESP-IDF) — OTA 앱 영역 두 개와 부팅 대상을 기록하는 otadata 파티션을 씁니다. 롤백 옵션(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE)을 켜면 새 앱은 첫 부팅에서 확인 대기 상태가 되고, esp_ota_mark_app_valid_cancel_rollback()을 호출하기 전에 재시작되면 다음 부팅에서 이전 앱으로 돌아갑니다(ESP-IDF OTA)

④ 자가 점검에 무엇을 넣느냐가 설계의 핵심입니다. 부팅만 확인하면 "켜지기는 하는데 서버에 접속하지 못하는 버전"이 확정되어, 그 단말은 다음 업데이트도 받지 못합니다. 최소한 서버 접속과 다음 업데이트를 받을 수 있는 경로가 살아 있는지는 확인합니다. 반대로 점검 항목이 너무 많고 오래 걸리면, 점검 도중의 정전이 불필요한 롤백으로 이어집니다.

서명 검증은 내려받는 경로의 보안과 별개로 둡니다. TLS는 전송 구간을 보호할 뿐이고, 배포 서버가 침해되거나 이미지가 저장소에서 바뀌는 경우는 막지 못합니다. 빌드 단계에서 개인키로 서명하고, 단말은 내장된 공개키로 검증한 뒤에만 설치합니다.

# 가정: OpenSSL 3.x. 개인키는 빌드 서버에만 두고 단말에는 공개키만 넣습니다
openssl ecparam -name prime256v1 -genkey -noout -out ota-signing.pem
openssl ec -in ota-signing.pem -pubout -out ota-public.pem

# 빌드 서버: 서명
openssl dgst -sha256 -sign ota-signing.pem -out firmware.bin.sig firmware.bin

# 단말 또는 배포 전 점검: 검증. 한 바이트라도 다르면 Verification failure
openssl dgst -sha256 -verify ota-public.pem -signature firmware.bin.sig firmware.bin

서명이 유효하더라도 취약점이 있는 예전 버전을 다시 설치하게 만드는 공격이 남습니다. ESP-IDF는 앱의 보안 버전을 칩의 eFuse 값과 비교해 낮은 버전의 설치와 부팅을 막는 옵션을 제공합니다. 다만 올릴 수 있는 횟수에 한계가 있으므로 모든 릴리스가 아니라 보안 수정이 있을 때만 올립니다.(ESP-IDF OTA) 이 옵션을 켜면 정상적인 롤백 대상도 제한되므로, 롤백 정책과 함께 정해야 합니다.

배포 중에 자주 나는 문제입니다.

증상 원인 해결
업데이트 후 일부 단말이 며칠 뒤 이전 버전으로 돌아감 확정 처리가 빠져 있어, 다음 재시작 때 롤백됨 자가 점검 통과 후 확정 호출. 서버에는 "설치됨"과 "확정됨"을 구분해 보고
내려받기가 끝나지 않는 단말이 있음 회선이 느려 매번 처음부터 다시 받음 이어받기 지원, 이미지 크기 축소
업데이트 후 설정이 초기화됨 새 버전이 설정 저장 형식을 바꿨고 변환 절차가 없음 설정에 형식 버전을 두고 변환 코드를 포함. 롤백 시 이전 형식으로 읽을 수 있는지도 확인
롤백했더니 서버와 통신이 안 됨 서버 API가 새 버전에 맞춰 이미 바뀜 서버는 직전 버전 단말과의 호환을 유지한 채 배포
배포 직후 서버가 느려짐 전 단말이 동시에 내려받기 시작 단말별 시작 시각 분산, 단계별 대상 확대
특정 현장만 계속 실패 저장 공간 부족, 방화벽이 배포 서버를 차단 설치 전 여유 공간 확인, 실패 사유를 서버로 보고

단계적 배포에서는 다음 단계로 넘어가는 기준을 미리 숫자로 정해둡니다. "1차 대상의 확정 보고 비율이 기준 이상이고, 재시작 횟수와 오류 보고가 이전 버전보다 늘지 않았을 때"처럼 정해두지 않으면, 확대 여부를 매번 감으로 판단하게 됩니다. 1차 대상에는 통신 환경이 나쁜 현장과 구형 하드웨어를 일부러 포함합니다.

제로살롱 KIOSK는 200대 이상이 카페, 상가, 공공시설에 흩어져 설치된 장비여서, 개발 초기부터 "모든 장비가 같은 소프트웨어를 돌린다"와 "방문 없이 갱신한다"를 조건으로 두었습니다. 장비마다 다른 값은 설정으로 분리하고 빌드는 하나로 유지했습니다. 장비별로 버전이 갈라지기 시작하면 위 표의 문제들을 원인별로 가려내기가 어려워지기 때문입니다.

현장 방문이 필요한 상황을 줄입니다

원격 관리 설계의 목표는 결국 현장에 가야만 해결되는 상황을 줄이는 것입니다. 어떤 상황이 방문을 유발하는지 정리해두면 설계 우선순위가 잡힙니다.

상황 원격으로 해결 가능하게 만드는 방법
단말이 응답 없음 원격 재시작 기능, 워치독으로 자동 복구
설정이 잘못됨 설정을 서버에서 내려받는 구조
소프트웨어 오류 원격 업데이트와 롤백
네트워크 설정 변경 대체 접속 경로 확보
하드웨어 고장 원격으로는 불가. 사전 진단으로 예측

마지막 행이 핵심입니다. 하드웨어 고장은 원격으로 고칠 수 없지만, 미리 알면 방문을 계획할 수 있습니다. 오류 빈도가 늘어나는 추세, 응답 시간이 느려지는 변화, 재시작 횟수 증가 같은 신호를 모아두면 고장 전에 조치할 여지가 생깁니다.

여러 현장을 관리한다면 한 번의 방문에 여러 작업을 묶는 것도 운영 비용을 줄이는 방법입니다. 긴급하지 않은 점검 항목을 모아두었다가 방문 시 함께 처리하는 방식입니다. 이를 위해서는 각 단말의 미처리 작업 목록이 시스템에 남아 있어야 합니다.

설정도 원격으로 바뀌어야 합니다

소프트웨어 업데이트와 별개로, 운영 중 바뀌는 값들이 있습니다.

  • 서버 주소
  • 폴링 주기
  • 기능 사용 여부
  • 화면 표시 항목
  • 인증 정보 갱신

이런 값이 빌드에 고정되어 있으면, 값 하나 바꾸는 데도 배포가 필요합니다. 설정과 코드를 분리하고 설정은 서버에서 내려받는 구조로 두는 편이 운영이 수월합니다.


6. 여러 고객사의 기기가 섞이지 않게 합니다

SaaS 형태로 여러 건물이나 사업장을 관리한다면 격리가 중요합니다.

모든 요청에서 소속을 확인합니다

기기 식별자만으로 제어가 가능한 구조는 위험합니다. 식별자를 알아내면 다른 고객사의 기기를 조작할 수 있게 됩니다.

요청을 처리할 때마다 확인해야 할 것들입니다.

  • 이 토큰이 유효한가
  • 이 토큰의 소속 조직은 어디인가
  • 요청 대상 기기가 그 조직에 속하는가
  • 해당 동작을 수행할 권한이 있는가

세 번째 확인이 빠지는 경우가 실제로 자주 있습니다. 인증은 통과했지만 대상 기기의 소속을 검증하지 않는 구조입니다. 목록 조회는 자기 것만 보이는데, 상세 조회나 제어는 식별자만 있으면 되는 상태가 됩니다.

단말용 인증과 사용자용 인증을 구분합니다

현장 단말은 사람이 로그인하지 않습니다. 단말 자체를 위한 인증 수단이 필요합니다.

  • 단말마다 개별 토큰을 발급합니다. 공용 키를 여러 단말이 나눠 쓰면 하나가 유출됐을 때 전체를 교체해야 합니다
  • 토큰의 권한 범위를 최소로 제한합니다. 해당 단말이 담당하는 공간과 기능으로 한정합니다
  • 폐기 절차를 만듭니다. 단말이 분실되거나 교체될 때 해당 토큰만 무효화할 수 있어야 합니다
  • 갱신 주기를 정합니다

MQTT를 쓴다면 소속 확인을 브로커의 토픽 권한으로도 걸어야 합니다. 토픽에 기기 식별자를 넣는 구조(devices/{기기ID}/commands)에서 단말이 devices/#을 구독할 수 있으면, 인증된 단말 하나로 전체 기기의 명령과 상태를 볼 수 있습니다. 단말 계정은 자기 식별자가 들어간 토픽에만 발행·구독하도록 제한하고, 와일드카드 구독은 서버 쪽 계정에만 허용합니다.

접근 기록을 남깁니다

문제가 생겼을 때 추적할 수 있어야 합니다. 어떤 토큰이, 어떤 기기에, 어떤 명령을 보냈는지가 기록되어야 합니다. 이 기록은 보안 목적뿐 아니라 장애 원인 파악에도 쓰입니다.


7. 자주 나오는 질문

오픈소스 IoT 플랫폼을 상용 서비스에 써도 됩니까

기술적으로는 가능하고 실제로 널리 쓰입니다. 다만 그대로 쓰는 것과 서비스로 만드는 것은 다른 작업입니다.

확인해야 할 지점들입니다.

  • 라이선스 — 상용 배포 시 조건을 확인해야 합니다
  • 멀티 테넌시 — 대부분의 오픈소스 허브는 단일 사이트를 전제로 설계되어 있습니다. 여러 고객사를 하나의 시스템에서 관리하려면 별도 계층이 필요합니다
  • 버전 관리 — 상위 프로젝트의 업데이트 주기가 빠르면 호환성 검증 부담이 생깁니다
  • 장애 대응 — 문제가 생겼을 때 직접 해결해야 합니다. 지원 계약이 없다는 뜻입니다

실무적으로는 허브는 현장 제어 담당, 서비스 계층은 별도 구축하는 구성이 일반적입니다. 허브가 기기 프로토콜을 흡수해주고, 계정·권한·이력·과금 같은 서비스 요소는 위에서 처리하는 방식입니다.

통신 방식은 어떻게 고릅니까

기기 특성과 설치 환경에 따라 다릅니다.

방식 적합한 경우 유의점
유선 이더넷 고정 설비, 안정성이 중요한 경우 배선 공사 필요
Wi-Fi 기존 망 활용 가능한 실내 전력 소모, AP 수용 한계
저전력 무선 배터리 구동 센서 다수 별도 게이트웨이 필요
이동통신 망이 없는 현장, 독립 설치 회선 비용이 단말 수에 비례

Wi-Fi를 선택할 때 자주 놓치는 것이 AP당 접속 가능 단말 수입니다. 센서를 수십 개 설치하면 기존 사무용 AP의 수용 범위를 넘을 수 있습니다. 설치 전에 망 담당자와 확인이 필요합니다.

데이터는 얼마나 보관해야 합니까

용도별로 다르게 가져가는 편이 현실적입니다.

  • 원본 시계열 — 짧은 주기로 쌓이므로 용량이 빠르게 늘어납니다. 일정 기간 이후 요약값만 남기는 방식을 씁니다
  • 집계 데이터 — 시간·일·월 단위 요약은 장기 보관해도 부담이 적습니다
  • 제어 이력 — 분쟁이나 감사 목적이 있다면 보관 기간을 별도로 정합니다

보관 정책이 없으면 저장소가 계속 커지고, 나중에 삭제하려 해도 어디까지 지워도 되는지 판단하기 어려워집니다. 처음부터 주기적 정리 작업을 넣어두는 편이 낫습니다.

시스템이 멈추면 건물 제어도 멈춥니까

설계에 따라 다릅니다. 모든 제어가 외부 서버를 경유하는 구조라면 서버 장애가 곧 제어 불가로 이어집니다.

현장 허브가 로컬에서 규칙을 실행할 수 있는 구조라면, 외부 연결이 끊겨도 기본 동작은 유지됩니다. 이 경우 외부 시스템은 모니터링·설정·이력 수집 역할을 맡습니다.

건물 설비처럼 중단 시 업무에 직접 영향을 주는 대상은 로컬 동작이 가능한 구조를 권합니다. 다만 이 구성에서는 로컬 규칙과 서버 설정이 어긋나지 않도록 동기화 설계가 추가로 필요합니다.


8. 착수 전 점검 목록

상태 관리

  • 기기가 주기적으로 생존 신호를 보내는가
  • 온라인·오프라인 판정 기준과 복귀 조건이 정의되어 있는가
  • 점검 중 상태를 표현할 수 있는가
  • 오프라인 지속 시 알림 기준이 있는가

명령 전달

  • 명령의 단계별 상태를 화면에 표현하는가
  • 목표 상태를 지정하는 방식으로 명령을 설계했는가
  • 응답 없음에 대한 재시도 정책이 있는가
  • 오프라인 기기에 대한 명령 처리 방식이 정해져 있는가
  • 보류된 명령의 유효 기간이 있는가
  • 명령에 고유 식별자가 있고, 기기가 중복 수신을 걸러내는가
  • 사용하는 브로커의 MQTT 버전과 지원 QoS, 세션 만료 시간을 확인했는가

상태 동기화

  • 주기적으로 실제 상태를 조회해 비교하는가
  • 불일치 시 어느 쪽을 기준으로 할지 정해져 있는가
  • 정전 복구 시 기기 기본 상태와 복구 동작이 정의되어 있는가
  • 재접속할 때마다 원하는 상태 전체를 다시 받아오는가
  • 끊긴 동안의 데이터를 단말에 보관하고, 서버가 중복 수신을 한 번만 저장하는가
  • 단말 시각과 서버 수신 시각을 함께 저장하는가

통신 효율

  • 데이터 종류별로 조회 주기를 구분했는가
  • 사용하지 않는 기능의 폴링을 끌 수 있는가
  • 설정 변경이 실행 중인 단말에 반영되는가
  • 재시도에 백오프와 분산이 적용되어 있는가

원격 관리

  • 버전 정보를 원격으로 확인할 수 있는가
  • 단계적 배포와 롤백이 가능한가
  • 업데이트 실패 시 단말이 복구되는가
  • 새 버전이 자가 점검을 통과한 뒤에만 확정되는가
  • 단말이 설치 전에 이미지 서명을 검증하는가
  • 설정이 코드와 분리되어 원격 변경 가능한가

보안

  • 모든 요청에서 대상 기기의 소속을 검증하는가
  • 단말마다 개별 토큰을 발급하는가
  • 토큰 권한이 필요한 범위로 제한되어 있는가
  • 개별 토큰 폐기가 가능한가
  • 접근 기록을 남기는가

마치며

IoT 프로젝트에서 구축과 운영의 난이도 차이는 큽니다. 기기를 연동해 동작시키는 것까지는 예상 범위 안에서 끝나지만, 그 기기가 현장에서 몇 년간 돌아가는 동안 생기는 일들은 설계에 반영되어 있지 않으면 전부 사람의 작업으로 넘어옵니다.

핵심은 기기가 항상 응답한다고 가정하지 않는 것입니다. 끊기고, 어긋나고, 명령을 놓치는 상황을 정상 경로의 일부로 두고 설계하면 운영 부담이 크게 줄어듭니다.

저희는 건물·시설의 기기와 센서를 연동해 제어하고, 현장 단말까지 원격으로 관리하는 시스템을 구축해왔습니다. 자체 운영 중인 건물 에너지 절감 솔루션 B-Saver에서는 모든 제어를 성공·실패까지 기록하고 기기의 마지막 통신 시각을 함께 보여주는데, "명령이 안 먹었다"는 문의에 조회 한 번으로 답하기 위한 장치입니다. 설치 이후의 운영까지 함께 고려한 구축을 준비하고 계시다면 편하게 문의해주세요.


함께 보면 좋은 글

참고 자료