블로그 목록
기술IoT 플랫폼기기 상태MQTT가용성운영

"지금 켜져 있나요?"에 정확히 답하기: 기기 상태 판정 분투기

화면에 초록 점이 켜져 있다는 것과 그 기기가 실제로 명령을 받을 수 있다는 것은 다른 이야기인데, 이 둘을 일치시키는 일이 생각보다 훨씬 까다로웠습니다. 결국 신호를 두 종류로 나눠 쓰게 됐고, 왜 그렇게까지 해야 했는지를 순서대로 적었습니다.

OpenIoT

OpenIoT 플랫폼팀

조회 21
"지금 켜져 있나요?"에 정확히 답하기: 기기 상태 판정 분투기

기기가 켜져 있는지 묻는 일은 참·거짓 하나를 고르는 문제로 보였습니다. 그런데 그 판정이 가장 많이 틀리는 때가 하필 장애가 난 순간이어서, 평상시가 아니라 그 순간을 기준으로 처음부터 다시 설계해야 했습니다.


초록 점 하나가 틀리면 나머지 기능이 같이 의심받습니다

기기 목록 화면에서 사용자가 가장 먼저 보는 것은 이름도 모델명도 아니라 초록 점이고, 이 점이 답하는 질문은 하나입니다. 지금 켜져 있나요. 이 답이 틀리면 그 뒤의 모든 기능이 같이 의심받는데, 조사 과정에서 실제로 확인했거나 구조상 반드시 생기는 증상은 다섯 가지였습니다.

사용자가 겪는 것실제로 무슨 일이 일어난 건가
"온라인인데 명령이 안 먹어요"기기가 조용히 끊겼는데 서버가 못 알아챘습니다. 화면만 초록입니다
"오프라인인데 데이터는 들어와요"끊김 알림이 늦게 도착해 살아있는 기기를 죽은 것으로 덮어썼습니다
"아무도 안 건드렸는데 갑자기 전부 오프라인"통신망이 잠깐 끊겼다 복구되며 수만 대가 한꺼번에 재접속했고, 그 폭주에 알림이 유실됐습니다
"기기가 없는데 접속 기록이 계속 찍혀요"기기가 아니라 이 접속한 것을 기기 접속으로 세고 있었습니다
"새로 산 기기가 고장 표시로 뜹니다"한 번도 켜본 적 없는 기기와 켜졌다가 죽은 기기를 구분하지 못했습니다

다섯 줄 전부 왼쪽과 오른쪽이 어긋나 있다는 것이 이 그림의 요점입니다. 기기가 고장 난 게 아니라 화면이 현장을 잘못 그리고 있는 것인데, 사용자에게는 둘의 구분이 아예 보이지 않습니다.

확인하는 쪽과 확인당하는 쪽이 다른 네트워크에 있습니다

"살아있는지 확인한다"는 요구는 말로는 쉽지만, 확인하는 쪽과 확인당하는 쪽이 서로 다른 네트워크에 있고 그 사이가 언제든 끊길 수 있다는 점이 문제입니다. 옆 건물 사람이 자리에 있는지 알고 싶다고 해보면, 5분마다 전화를 걸면 그 사람 업무를 방해하고 1분마다 손을 흔들라고 하면 흔든 직후 자리를 떠도 알 수 없습니다. 출입 기록을 보면 즉시 알 수 있지만 기록기가 가끔 기록을 빠뜨립니다.

셋 다 단독으로는 부족한데, IoT에는 성질이 하나 더 붙습니다. 가장 정확해야 할 순간인 장애 복구 직후에 가장 부정확해진다는 것이고, 이게 이 글의 핵심입니다.


성숙한 플랫폼은 하나만 고르지 않았습니다

무엇을 신호로 삼을지를 놓고 네 가지를 비교했습니다.

방식어떻게 동작하나장점결정적 한계
① 앱이 주기적으로 물어보기(폴링)앱이 몇 초마다 서버에 "이 기기 살아있어요?"라고 조회구현이 가장 단순. 서버가 상태를 안 들고 있어도 됨앱을 열고 있는 동안만 동작. 배터리·데이터 소모. 기기가 많아질수록 조회가 선형으로 증가
② 기기가 주기적으로 알리기(heartbeat)기기가 30~60초마다 "살아있음" 메시지를 보냄기기가 직접 말하므로 신뢰도 높음. 통신 경로 전체가 살아있다는 증거느립니다. 끊긴 걸 알아채는 데 최소 주기의 2~3배. 통신량·전력을 계속 씁니다
③ 클라우드 연결 이벤트만 믿기기기가 접속·해제할 때 클라우드가 알림을 발행즉시. 추가 통신량 0. 기기가 아무것도 더 하지 않아도 됨클라우드가 전달을 보장하지 않고(best-effort) 순서도 보장하지 않습니다
④ 혼합③을 기본으로 두고 ②로 검사빠르면서 틀리지 않음두 신호가 충돌할 때의 규칙을 직접 정해야 함

용어 한 줄 정리

폴링은 궁금한 쪽이 주기적으로 물어보는 방식이고, heartbeat는 살아있는 쪽이 심장 박동처럼 규칙적으로 신호를 보내는 방식입니다.

best-effort는 최선을 다해 보내되 도착은 보장하지 않는다는 뜻으로, 등기우편이 아니라 일반우편입니다.

화살표가 시작되는 지점만 보면 성격이 갈립니다. ①만 화살표가 앱에서 출발하고 나머지는 전부 기기에서 출발하는데, 앱에서 출발하는 신호는 앱을 닫는 순간 함께 멈춥니다.

이전 세대 설계는 앱이 계속 물어보는 방식이었습니다

이전 세대 시스템의 설계도를 열어보니 기기 등록 직후 정상 동작 확인을 주기적 조회(REST 폴링) 로 하고 있었습니다. 접속 상태를 저장한 별도 테이블을 앱이 반복해서 읽는 구조라 동작은 하지만, 앱이 켜져 있는 동안만 상태가 갱신되고 기기가 늘어날수록 조회량도 그대로 늘어납니다.

바깥은 어떻게 하는지도 함께 대조했습니다. MQTT 브로커 계열(EMQX 등)은 접속·해제를 브로커 이벤트로 즉시 발행하고, 기기 관리 플랫폼(ThingsBoard 등)은 거기에 더해 "일정 시간 데이터가 안 옴"을 별도 규칙으로 둡니다. MCU 대상 관리 서비스(Golioth·Memfault 등)는 기기가 주기 보고를 하는 것을 전제로 최근 수신 시각을 함께 관리합니다.

성숙한 플랫폼은 거의 예외 없이 ③과 ②를 같이 쓰고 있었고 하나만 쓰는 곳은 없었습니다. 여기서 방향이 정해졌으니, 같은 저울질을 하고 계신다면 하나를 고르려는 시도부터 내려놓으시길 권합니다.


빠른 신호로 맞추고 느린 신호로 검산합니다

③을 주 신호로, ②를 보정 신호로 삼고 폴링은 폐기했습니다. 한 문장으로 줄이면 이렇습니다.

"빠르지만 못 믿는 신호"로 즉시 반영하고, "느리지만 믿을 수 있는 신호"로 계속 검산한다.

기기에서 나가는 선이 두 갈래라는 점이 이 그림의 요점입니다. 파란 쪽인 연결 이벤트가 화면을 즉시 맞추고 주황 쪽인 마지막 통신 시각이 5분마다 그 결과를 검산하므로, 어느 한쪽이 틀려도 다른 쪽이 뒤늦게 바로잡습니다.

두 신호는 경쟁 관계가 아니라 역할 분담입니다

처음에는 "②냐 ③이냐"로 고민했는데, 성질을 나란히 적어보니 애초에 다른 일을 하는 신호였습니다.

연결 이벤트(③)살아있음 알림(②)
반영 속도즉시주기의 2~3배 지연
신뢰성전달 보장 없음기기가 직접 보낸 증거
비용추가 통신 0기기 통신량·전력 소모
역할평소 화면을 빠르게 맞춘다틀린 것을 뒤늦게라도 바로잡는다

한쪽은 속도를 한쪽은 정확성을 담당하므로 둘 다 쓰면 평소엔 즉시 정확하고 어긋나도 5분 안에 자동 복구되는 상태가 됩니다. 고객 입장에서 이 조합이 주는 것은 하나인데, 화면의 초록 점을 믿고 명령을 눌러도 된다는 것입니다.

사용자의 배터리와 데이터 요금을 쓰지 않습니다

폴링을 버린 가장 큰 이유가 여기에 있습니다. 앱이 5초마다 상태를 조회하면 앱을 켜 둔 1시간 동안 720번의 조회가 발생하고 그 비용은 전부 사용자의 배터리와 데이터인데, 게다가 앱을 닫으면 그동안의 상태 변화는 아무도 모릅니다.

폴링우리 방식
앱이 켜져 있을 때계속 조회바뀔 때만 수신
앱이 꺼져 있을 때갱신 안 됨서버는 계속 알고 있음
기기 수가 100배조회량도 100배상태 변화량에만 비례
알림(푸시) 연동불가능상태 변화가 곧 알림 트리거

마지막 줄이 실무에서 큽니다. 서버가 항상 상태를 알고 있으므로 "기기가 오프라인이 되면 담당자에게 알림"이 별도 개발 없이 성립합니다.

상태를 셋으로 나눠 "아직 설치 전"을 따로 표현했습니다

보통은 켜짐과 꺼짐 둘로 충분해 보이지만, 공장에서 갓 나와 한 번도 켜본 적 없는 기기를 뭐라고 표시할지가 문제였습니다. 오프라인이라고 쓰면 사용자는 고장으로 읽는데 실제로는 아직 전원조차 넣지 않은 새 제품이고, 그렇다고 온라인으로 둘 수도 없어서 세 번째 값을 만들었습니다.

상태정의화면에서 사용자가 읽는 의미
online접속 알림을 받았고 아직 해제 알림이 없음지금 명령을 보낼 수 있습니다
offline해제 알림을 받았거나, 검산에서 통신 두절로 판정됨연결이 끊겼습니다. 확인이 필요합니다
unknown등록된 뒤 한 번도 접속한 적이 없음아직 설치 전입니다

unknown으로 돌아오는 화살표가 없다는 점을 봐주세요. 한 번이라도 접속한 기기는 다시는 "설치 전"이 되지 않고, online에서 offline으로 가는 길이 알림을 받아서와 검산으로 걸러서 둘인 것도 중요합니다.

이 구분이 겨냥하는 것은 고객사의 A/S 비용입니다. 새 제품이 불량으로 오인돼 들어오는 반품과 문의가 줄고, 운영자의 오프라인 목록에도 한 번이라도 살았던 기기만 남습니다. 창고에 쌓인 미개봉 재고가 오프라인 목록을 가득 채우면 그 목록은 아무도 안 보게 됩니다.

상태 변화 자체를 하나의 사건으로 남깁니다

상태를 값으로만 두면 "지금 온라인이다"밖에 답하지 못하므로, 상태가 바뀌는 순간을 사건으로 취급해 바뀔 때마다 세 가지가 동시에 일어나게 했습니다.

무엇무엇에 쓰이나
기기 로그에 기록"이 기기가 지난주에 몇 번 끊겼나"를 나중에 조회 가능
실시간 구독자에게 즉시 전달여러 사람이 보고 있는 화면이 동시에 바뀜 (1편의 구조를 그대로 사용)
웹훅 발화(device.online · device.offline)고객사 자체 시스템이 우리 플랫폼 없이도 반응 가능

세 번째가 B2B에서 자주 요청받는 항목입니다. 고객사가 이미 쓰는 관제 시스템이나 메신저에 우리 화면을 거치지 않고 바로 연결됩니다.


가장 정확해야 할 순간에 가장 부정확해집니다

방향은 명확했지만 실제로 정확하게 만들려면 처리해야 하는 함정이 있었고, 그중 가장 무서운 것이 재연결 폭주였습니다. 정전이 복구되거나 통신망이 되살아나면 그 지역의 기기 수만 대가 거의 동시에 재접속하고 접속 알림도 수만 건이 한꺼번에 쏟아지는데, 처리 능력에는 한도가 있어 초과분은 처리되지 못하고 사라집니다.

장애가 복구된 직후, 즉 "우리 기기 다 살아났나?"를 운영자가 가장 절박하게 확인하고 싶은 바로 그 순간에 화면이 가장 부정확해집니다.

기기는 전부 살아났는데 화면 절반은 오프라인으로 남아 있으니 운영자는 현장에 사람을 보내게 되고, 가 보면 기기는 멀쩡합니다. 조사 과정에서 기존 사내 구현체 하나를 뜯어봤더니 알림을 받는 규칙이 처리 함수에 직결되어 있어 완충 장치가 없었는데, 평소에는 전혀 문제가 없고 정확히 장애 복구 때만 무너지는 구조였습니다.

해결은 사이에 대기 줄을 넣는 것이었습니다.

왼쪽 아래로 빠지는 🗑️ 화살표가 이 그림의 전부입니다. 처리 한도를 넘은 알림이 갈 곳이 없어 그냥 사라지고 그만큼 화면이 틀리는데, 오른쪽에는 그 화살표가 없어 처리가 늦어질 뿐 없어지지는 않습니다.

알림이 처리 함수에 곧바로 닿지 않는다는 점을 봐주세요. 대기 줄이 쏟아지는 알림을 일단 전부 받아 유실을 막고, 최대 10건씩 묶어 처리해 처리 횟수를 줄이며, 여러 번 시도해도 실패한 건은 버려지지 않고 실패 보관함에 남아 원인 파악이 가능합니다.

여기서 한 가지를 더 갈랐습니다. 큐는 안정성을 주는 대신 지연을 더하므로 조명을 켜는 명령까지 큐에 태우면 버튼 반응이 느려지는데, 상태 판정은 안 놓치는 것이 중요하고 제어 명령은 빠른 것이 중요합니다. 그래서 상태 알림만 큐를 태우고 제어 명령은 직결을 유지했습니다.

연결 이벤트가 best-effort라는 점도 함께 대비해야 했습니다. "끊김" 알림이 유실되면 죽은 기기가 계속 온라인으로 표시되므로, 기기가 보내는 센서값과 이벤트를 받을 때마다 마지막 통신 시각을 갱신하고 5분마다 전체를 훑어 임계를 넘긴 기기를 오프라인으로 강등합니다. 임계는 살아있음 알림 주기의 3배로 잡아 한두 번 빠뜨린 정도로는 오프라인이 되지 않게 했는데, 이 3배라는 값은 지금도 더 나은 기준이 있을 것 같습니다.

기기 쪽 규약도 함께 확정했습니다. 30~60초 주기의 살아있음 알림, 그리고 기기가 접속할 때 "내 연결이 끊기면 이 메시지를 대신 발행해 달라"고 미리 맡겨 두는 유언장(LWT) 입니다. 기기가 인사도 못 하고 죽어도 클라우드가 대신 알려줍니다.

옛날 소식이 뒤늦게 도착합니다

가장 반직관적인 함정이고, 돌아보면 저희도 이 가능성을 한참 뒤에야 떠올렸습니다. 기기가 잠깐 끊겼다 곧바로 다시 붙으면 정상 순서는 "끊김 → 연결"이지만, 두 알림은 서로 다른 경로로 전달되므로 연결이 먼저 도착하고 끊김이 뒤늦게 도착할 수 있습니다.

마지막 두 줄의 순서를 봐주세요. 기기는 이미 재연결을 마쳤는데 그보다 먼저 일어난 끊김이 나중에 도착하므로, 도착한 순서대로 받아 적으면 최신 사실이 옛 사실에 덮어써지고 그 상태는 다음 사건이 생길 때까지 스스로 풀리지 않습니다.

해결은 단순했는데, 클라우드가 보내는 알림에는 접속·해제마다 1씩 커지는 버전 번호가 함께 오므로 저장할 때 "지금 들고 있는 버전보다 큰 경우에만 쓴다" 는 조건 하나를 거는 것으로 끝났습니다.

오른쪽이 도착 시각이 아니라 번호를 기준으로 판단한다는 것이 차이입니다. 늦게 도착했어도 번호가 작으면 옛 소식이므로 버리는데, 순서를 보장받으려 애쓰는 대신 순서가 뒤바뀌어도 결과가 같아지게 만든 것입니다.

앱도 기기와 같은 문으로 들어옵니다

앱과 기기는 같은 통신 엔드포인트에 접속하므로, 사용자가 앱을 켜고 끌 때마다 접속·해제 알림이 똑같이 발생합니다. 거르지 않으면 사용자가 앱을 켠 것이 기기 접속 기록으로 쌓이는데, 해결은 접속자에게 붙는 식별자의 작명 규칙을 미리 정해 애초에 갈라 놓는 것이었습니다. 기기는 항상 d-로 시작하고 앱과 웹은 항상 u-로 시작합니다.

두 접속이 같은 문으로 들어와 필터에서 갈린다는 점이 요점입니다. 나중에 골라내는 게 아니라 식별자 첫 글자만 보고 즉시 갈라지므로 앱을 아무리 켜고 꺼도 기기 상태에는 흔적이 남지 않고, 기기 식별자는 1편에서 다룬 출입증 판정에도 그대로 쓰이므로 이 규칙은 어차피 필요했습니다.

모든 끊김이 오프라인은 아닙니다

클라우드는 끊김 알림에 사유를 함께 보내는데, 사유별로 판정이 달라야 했습니다.

끊김 사유판정
정상 종료offline기기가 스스로 인사하고 나갔음
연결 손실 · 응답 시간 초과offline네트워크 이상. 반복되면 품질 경보
같은 식별자로 새 세션 접속offline 처리 금지재접속 과정에서 옛 연결이 정리된 것. 기기는 살아있습니다
정책 위반 · 요청 한도 초과offline + 운영 알람기기 문제가 아니라 설정·한도 문제. 감사 로그에 남김
클라우드 측 사유offline우리 쪽 원인이 아님을 구분해 기록

세 번째 줄을 놓치면 기기가 재접속할 때마다 잠깐씩 오프라인으로 깜빡여서, 실제로는 정상인데 사용자에게는 연결이 불안정한 제품으로 보입니다. 네 번째 줄도 운영 관점에서 값이 있는데, 기기가 죽은 것과 우리가 막은 것은 대응이 완전히 달라 전자는 현장 출동이고 후자는 설정 수정이기 때문입니다.


지금 손에 있는 것을 그대로 적습니다

현재 이 구조는 설계 확정 + 동작하는 화면 프로토타입 단계이고 실기기 연동과 성능 측정은 아직 하지 않았습니다. 지금까지 이야기한 것을 한 장으로 모으면 이렇습니다.

①~④로 이어지는 굵은 본선과 옆에서 들어오는 주황색 검산선이 이 글 전체입니다. 본선은 빠르게 화면을 맞추고 검산선은 본선이 놓친 것을 5분 안에 되돌리며, 그 결과 하나가 앱 화면과 고객사 시스템 양쪽으로 동시에 나갑니다.

구현이 따라야 하는 계약으로 고정한 값들입니다.

항목확정 내용
주 신호클라우드 접속·해제 이벤트
필터 조건식별자가 d-로 시작하는 접속만 (기기 전용)
저장 조건버전 번호가 기존보다 클 때만 기록
보정 신호30~60초 주기 살아있음 알림 + 유언장(LWT)
검산 주기5분. 임계는 알림 주기의 3배
처리 경로필터 → 대기 줄(+실패 보관함) → 묶음 처리
상태 값online · offline · unknown 3종
상태 변경 시로그 기록 + 실시간 브로드캐스트 + 웹훅 발화

프로토타입은 클릭되는 화면으로 만들어 이 구조가 실제 화면에서 성립하는지 확인했습니다. 기기 목록에는 상태 배지와 마지막 수신 시각 열, 상태 필터를 두었고 상단에 전체·온라인·오프라인·고장·미소유 집계를 붙였습니다. 상태 배지는 목록·상세·검색 결과가 같은 컴포넌트를 공유해 표기가 어긋나지 않게 했습니다.

가장 값을 한 것은 기기 상세 화면이었습니다. 오프라인 기기의 제어 패널을 막지 않고 대신 "지금은 예약만 되고 기기 접속 시 반영됩니다"라는 안내가 뜨도록 정리했는데, 상태 판정이 정확해야 이 안내도 정확해집니다.

화면을 만들며 설계와 대조하다 불일치도 하나 찾았습니다. 설계상 생존 판정값은 3종인데 화면 배지는 온라인·오프라인·프로비저닝·미소유·고장 5종이었습니다. 틀린 게 아니라 층위가 달랐는데, 생존 판정은 "지금 통신이 되는가"만 답하고 화면 배지는 거기에 등록 생애주기를 얹어 표시하는 것이었습니다. 사용자에게 "unknown"이라고 쓰는 것은 아무 의미가 없으니 화면 쪽이 옳아서, 판정값과 화면 표시값을 별개의 두 축으로 분리하고 변환 규칙을 문서에 명시했습니다. 구현 중에 발견했다면 두 값이 뒤섞여 저장됐을 것입니다.


평상시가 아니라 장애 시를 기준으로 설계해야 합니다

처음에는 더 정확한 신호 하나를 고르려 했지만 그런 신호는 없었습니다. 즉시 오는 신호는 못 믿고 믿을 수 있는 신호는 느리니, 답은 하나를 고르는 게 아니라 둘에게 다른 일을 시키는 것이었습니다. 1편에서도 같은 결론에 도달했으니, 배타적으로 보이는 선택지가 실은 층위가 다른 경우가 설계에서 반복해서 나타나는 셈입니다.

더 크게 배운 것은 검토 기준 자체였습니다. 완충 장치가 없는 구조도 평상시에는 완벽하게 동작하고 테스트에서도 통과하는데, 무너지는 것은 정확히 장애 복구 순간이고 그때가 상태 정확도가 가장 절실한 때입니다. "평소에 잘 도는가"가 아니라 "최악의 순간에 어떻게 되는가"로 기준을 바꾼 것이 이 설계에서 얻은 습관입니다.

작은 결정이 운영 비용을 바꾼다는 것도 확인했습니다. unknown을 추가하는 것은 코드로는 사소한 변경이지만 이것 하나로 새 제품이 고장으로 보이는 오인이 줄고 운영자의 오프라인 목록에서 창고 재고가 빠집니다.

고객이 얻는 것근거
화면의 상태를 믿고 명령을 눌러도 됨즉시 반영 + 5분 주기 자동 검산
틀려도 스스로 복구됨알림이 유실돼도 마지막 통신 시각으로 보정
장애 복구 직후에도 상태가 정확함대기 줄 완충 + 묶음 처리 + 실패 보관함
새 제품이 고장으로 오인되지 않음unknown 상태로 "설치 전"을 별도 표현
앱 배터리·데이터를 쓰지 않음폴링 폐기. 상태가 바뀔 때만 신호가 흐름
오프라인 알림을 고객사 시스템에 바로 연결상태 변화가 로그·실시간·웹훅으로 동시 발화
재접속 중에 상태가 깜빡이지 않음끊김 사유별 분기 판정

가장 신경 쓴 것은 세 번째 줄입니다. 평소에 정확한 시스템은 만들기 쉽지만, 무너진 직후에도 정확한 시스템이 어렵습니다.

초록 점 하나를 믿게 만들려고 빠른 신호와 느린 신호를 함께 쓰기로 한 것이 이 글의 전부입니다. 비슷한 고민을 하고 계신 분께 조금이나마 도움이 되었으면 합니다.

읽어주셔서 감사합니다.


IoT 서비스,
지금 바로 시작하세요

디바이스 연결부터 대시보드까지 올인원

무료 플랜으로 부담 없이 체험