내가 처음으로 디버깅한 GOOSE 구독은 엔지니어링 노트북에서는 완벽하게 정상으로 보였고, SCADA 게이트웨이에서는 완전히 죽어 있었다. 같은 변전소 LAN, 같은 퍼블리셔, 같은 데이터셋. IED 자체 툴은 모든 메시지를 디코딩했는데, 우리 게이트웨이는 차단기 위치가 전원 투입 시점 값에 그대로 얼어붙어 있었다. 원인은 게이트웨이 NIC이 조용히 벗겨내던 VLAN 태그 하나였는데 — 거기까지 도달하는 데 반나절이 걸렸다. GOOSE는 실패했을 때 피드백을 거의 주지 않기 때문이다. 그냥 조용해질 뿐이고, 조용한 상태는 "아무것도 바뀌지 않았다"와 똑같이 보인다.
이게 GOOSE 메시지를 하나라도 구독하기 전에 몸에 새겨야 할 사실이다. 폴링이나 MMS 리포트와 달리 GOOSE에는 요청도, 확인 응답도, 세션도 없다. 퍼블리셔는 멀티캐스트 이더넷 프레임을 뿌리고 잊어버린다. 당신이 못 받아도 아무도 모른다. 그래서 GOOSE 구독자의 임무 전체는 "값이 정말로 안 바뀐 것"과 "내가 퍼블리셔를 못 듣게 된 것"을 구분하는 것이고, 놓친 트립 신호가 실제로 의미 있을 만큼 빠르게 그렇게 하는 것이다.
두 카운터가 모든 일을 한다
모든 GOOSE 프레임에는 대부분 사람들이 대충 넘겼다가 나중에 후회하는 두 숫자가 실려 있다. stNum(상태 번호)과 sqNum(시퀀스 번호)이다. 사실 이 둘이 프로토콜의 전부라고 봐도 된다.
sqNum은 아무것도 바뀌지 않는 동안 재전송할 때마다 증가한다. 퍼블리셔는 프레임 하나 보내고 잠잠해지는 게 아니라, 방금 합류했거나 프레임을 놓친 구독자가 따라잡을 수 있도록 같은 상태를 점점 늘어나는 간격으로 다시 보낸다. stNum은 데이터셋의 값이 실제로 바뀔 때만 증가하고, 그때 sqNum은 0으로 리셋된다. 그래서 안정된 차단기의 전선 위 패턴은 이렇다:
stNum=41 sqNum=0 (차단기가 방금 열림)
stNum=41 sqNum=1
stNum=41 sqNum=2
stNum=41 sqNum=3 ... 간격이 최대치를 향해 늘어남
그리고 다시 트립되는 순간:
stNum=42 sqNum=0 (새 상태 — 즉시 전송 후 버스트)
stNum=42 sqNum=1
stNum=42 sqNum=2
페이로드 값만 보는 구독자는 프레임 안에서 가장 유용한 진단 정보를 버리는 셈이다. stNum을 보면 진짜 상태 변화가 일어났음을 안다. sqNum을 보면 재전송이 돌고 있음을 안다. 받은 두 프레임 사이에서 stNum이 1보다 크게 뛰었다면 상태를 놓친 것이다 — 붙잡고 있는 값이 열림→닫힘→열림을 거쳤는데 당신은 양 끝만 본 것일 수 있다. 이런 건 태그 값에는 절대 안 나타나지만, 사고 후 이벤트 순서(SOE)를 검토할 때는 반드시 드러난다.
allowedToLive가 유일한 생존 확인 수단이다
값을 하는 또 하나의 필드는 모든 프레임에 실려 오는 timeAllowedtoLive(TAL)다. 퍼블리셔가 이렇게 말하는 것이다: 이 밀리초 안에 나한테서 다시 소식을 못 들으면, 내가 없어졌다고 간주하라. 새 상태 변화 직후 퍼블리셔는 TAL을 작게 — 빠른 버스트에 맞춰 — 보내고, sqNum이 올라갈수록 늘려서, 정상 상태 값(보통 최대 재전송 간격의 두 배)에 안착한다.
구독자는 받는 프레임마다 TAL만큼의 타이머를 걸어야 하고, 새 프레임 없이 만료되면 구독을 stale로 표시하고 연관 태그를 bad quality로 몰아야 한다. 이건 선택이 아니다. TAL 감시가 없는 GOOSE 구독은 케이블이 뽑히는 순간 조용히 거짓말을 하는 상태 태그다. 퍼블리싱 IED를 정비하려고 뽑은 뒤 20분 동안 SCADA 화면에 차단기가 멀쩡히 "닫힘"으로 떠 있는 걸 본 적 있다. 아무도 타임아웃을 연결하지 않았기 때문이다. 값은 마지막으로 들은 것이었고, 품질은 정상이었고, 그리고 쓸모없었다.
정상 상태 최대 간격은 보통 10002000 ms이므로 조용한 상태의 TAL은 대략 20004000 ms에 놓인다. stale 검출은 하드코딩한 숫자가 아니라 수신한 TAL을 기준으로 잡아라 — 퍼블리셔마다 다르게 설정하고, 좋은 구독자는 프레임이 말하는 값을 존중한다.
구독은 실제로 어디서 죽는가
GOOSE 구독이 동작하지 않을 때, 페이로드가 문제인 경우는 드물다. 내가 겪은 빈도 순으로 대략:
VLAN과 우선순위 태깅. GOOSE 프레임은 거의 항상 802.1Q 태그가 붙어 VLAN ID와 우선순위(PCP)를 실어 나른다. 바쁜 변전소 LAN에서 줄을 앞질러 가는 게 목적이기 때문이다 — 우선순위 4가 IEC 61850의 GOOSE/SV 기본값이다. 경로상의 스위치 포트, NIC, 가상 스위치 설정 중 하나라도 그 VLAN을 벗기거나 필터링하면, 프레임은 당신 스택이 보기도 전에 사라진다. 이게 내 차단기 사건이었다. 게이트웨이 포트에서 Wireshark로 캡처해 태그가 있는지 확인하라. 엔지니어링 노트북은 태그된 프레임을 보는데 게이트웨이는 못 본다면, 문제는 설정이 아니라 그 둘 사이에 있다.
APPID / MAC 주소 불일치. GOOSE는 01-0C-CD-01-00-00 대역의 멀티캐스트 MAC으로 전달되고 16비트 APPID로 식별된다. 구독자는 이 둘로 필터링한다. 임포트한 SCL(SCD 파일)이 퍼블리셔에 실제로 설정된 것과 다르면 — 누군가 익스포트 이후 APPID를 바꿨다면 — 아무도 글을 올리지 않는 우편함을 구독하게 된다. APPID와 목적지 MAC을 넘겨받은 SCD가 아니라 라이브 캡처와 대조하라.
ConfRev 불일치. GoCB는 설정 개정 번호 confRev를 실어 나른다. 데이터셋을 바꾸면 — 멤버 추가, 순서 변경, 타입 변경 — confRev가 올라간다. 신중한 구독자는 confRev가 자신이 엔지니어링된 값과 맞는지 검증하고, 아니면 데이터를 거부한다. 데이터셋 안의 위치가 가진 전부이기 때문이다. GOOSE 멤버는 이름이 아니라 순서로 디코딩된다. 누군가 데이터셋 맨 앞에 새 boolean을 끼워 넣었는데 구독자가 confRev를 확인하지 않으면, 이후 모든 멤버가 밀려서 차단기 상태가 예전 알람 비트였던 값을 읽게 된다. 이건 그냥 짜증나는 게 아니라 위험한 실패 모드다 — 자신 있게 틀린 값을 만들어낸다.
시뮬레이션/테스트 비트. IEC 61850-8-1 Edition 2는 프레임 헤더에 simulation 플래그(Ed1의 옛 이름은 test)를 추가했다. 시뮬레이션 GOOSE를 주입하는 테스트 세트는 이걸 true로 설정한다. 시뮬레이션을 존중하는 구독자는 시뮬레이션을 받도록 지시받으면 실제 트래픽을 무시하고, 반대도 마찬가지다. 시운전 중에 이게 사람들을 끊임없이 문다 — 테스트 인젝터의 프레임은 시뮬레이션으로 표시돼 있는데 구독자는 시뮬레이션 모드가 아니라, 아무것도 받아들이지 않고, 마치 구독이 깨진 것처럼 보인다. 구독자가 어떤 모드인지 알아라.
조용한 게 아니라 살아 있음을 증명하기
정상적으로 유휴 상태인 구독과 죽은 구독 둘 다 정적인 값을 보여주므로, 생존성을 내장하고 화면에 올려라:
stNum,sqNum, 그리고 TAL 타이머로 구동되는 구독-살아있음 boolean을 태그로 노출하라. 운영자에겐 필요 없지만, 엔지니어는 뭔가 이상해 보이는 첫 순간에 필요하다.- 페이로드가 아니라 살아있음 boolean이 false로 가는 것에 알람을 걸어라. GOOSE 피드가 끊기는 건 통신 이벤트이고, 죽은 폴처럼 다뤄야 한다 — 파생 상태에 bad quality, 낮은 우선순위 알람, 그리고 값이 "닫힘"이 아니라 stale임을 화면에 분명히 표시.
- 시운전 중에는 상태 변화를 강제하고(테스트로 차단기 조작, 소스 토글)
stNum이 양쪽에서 증가하는지 확인하라. 퍼블리셔에서는stNum이 움직이는데 당신의sqNum=0 리셋 프레임이 구독자에 도착하지 않는다면, 초기 버스트를 잃은 것이다 — 보통 손실이거나 부하 상황에서 우선순위 처리를 제대로 못 하는 스위치다. - 부하 상황에서
sqNum결번을 주시하라. 재전송 덕분에 간헐적 단일 프레임 손실은 견딜 만하지만,sqNum=0 버스트 프레임을 상습적으로 놓친다면 실제 이벤트마다 지연이 붙어서, GOOSE를 폴링 대신 고른 이유 자체가 무너진다.
한 가지 더 지킬 만한 습관: 시운전 중 GOOSE 트래픽을 몇 분 캡처해 pcap을 프로젝트와 함께 보관하라. 1년 뒤 누군가 데이터셋을 바꿔서 상태 태그가 쓰레기 값을 읽기 시작하면, confRev가 올라갔음을 증명하는 가장 빠른 길은 라이브 프레임을 그 기준선과 diff하는 것이다. GOOSE는 바뀌었다고 알려주지 않는다 — "올바른" 상태가 어떻게 생겼는지 당신이 적어 뒀어야 한다.