제2장부터 제4장까지는 홍수 데이터가 어떤 모양이고 API로 어떻게 오가는지를 다루었다. 이 장은 그 사이에서 일어나는 일을 다룬다. 예측 노드가 관측값을 홍수 예측으로 바꾸는 방법, 여러 모델을 하나의 앙상블로 묶는 방법, 예측 품질을 재는 방법, 그리고 주민에게 경보를 낼지 판단하고 실제로 전달하는 방법이다. 내용은 WIA-ENE-033 PHASE-3(프로토콜) §6–7과 참조 TypeScript SDK의 보조 함수를 따른다.
PHASE-3은 자신의 범위를 한 문장으로 정한다. "관측망, 예측 노드, 경보 발령 기관, 하류 소비자 사이의 프로토콜"이다. 이 네 역할이 이 장의 뼈대다. 이것은 제품이 아니라 논리적 역할이다. 한 기관이 네 역할을 하나의 시스템에서 모두 수행할 수도 있고, 국가 수문 기관이 관측망과 예측 노드를 운영하고 지자체 재난안전 부서가 경보 발령 기관을 맡을 수도 있다.
FloodAlert를 발령한다.PHASE-3은 자신이 참조하는 WMO·OGC 문서의 메시지 유형을 "반영"하되 "재정의하지 않는다"고 밝힌다. 구간마다 전송 수준의 절차(handshake)를 규정하지도 않는다. 따라서 아래 순서도는 ENE-033 객체가 각 역할에 어떻게 대응하는지에 대한 이 책의 해석이다. 명세가 제시한 규범적 도식이 아니라 실무적 해석임을 밝혀 둔다.
관측망 예측 노드 경보 발령 기관 하류 소비자
| | | |
|-- RiverLevelData --->| | |
|-- PrecipitationData->| | |
|-- DamStatusData ---->| | |
|-- DrainageData ----->| | |
| | 1D / 2D / AI 모델 실행 | |
| | EnsemblePrediction 구성 | |
| |-- FloodPredictionResult->| |
| | (+ 앙상블 요약) | |
| | | AlertCriteria 적용 |
| | | manualReview? -> 당직자 |
| | | 판단 |
| | |-- FloodAlert / CAP 1.2 -->|
| | |<-- 전달 확인 -------------|
| | | (AlertDistribution.status)
| | | |
|<================ 주기마다 새 관측값이 들어오고 순환이 반복된다 ================|
| 구간 | 주요 ENE-033 객체 | 정의 위치 | 함께 전달되어야 할 정보 |
|---|---|---|---|
| 관측망 → 예측 노드 | RiverLevelData, PrecipitationData, DamStatusData, DrainageData | PHASE-1 (제2장) | UTC 시각, 명시적 단위, 관측소 식별자, 각 수위의 기준면 |
| 예측 노드 → 경보 발령 기관 | FloodPredictionResult, EnsemblePrediction | PHASE-1, PHASE-3 §6.3 | 확률, 신뢰도, 선행 시간, 해당 실행에 쓰인 모델 버전 |
| 경보 발령 기관 → 소비자 | FloodAlert (CAP 1.2로 송출) | PHASE-3 §7.2 (제3장) | 발령자 신원, 영향 지역 도형, 시작·종료 시각, 한·영 행동 요령 |
| 소비자 → 경보 발령 기관 | AlertDistribution.status | PHASE-3 §7.3 | 채널별 발송·전달·실패·확인 건수 |
마지막 행은 보기보다 중요하다. 메시지가 도착했는지 되돌려 받지 못하는 경보 체계는 성공한 경보와 조용히 실패한 경보를 구분할 수 없다. 이 문제는 5.8절에서 다시 다룬다.
역할을 나누면 각 구간이 따로 실패할 수 있다는 점도 따라온다. 따라서 구간마다 실패했을 때의 동작이 미리 정해져 있어야 한다. 관측 자료가 끊기면 예측 노드는 마지막 값을 조용히 재사용하지 말고, 다음 실행이 오래된 입력에 기반했음을 표시해야 한다. 예측 노드가 멈추더라도 경보 발령 기관은 자신의 권한을 그대로 가지므로 관측 수위만으로 경보를 낼 수 있어야 한다. 배포 채널 하나가 끊겨도 나머지 채널은 계속 동작해야 한다. PHASE-3이 이런 대체 규칙을 열거하지는 않지만, 구간별로 문서화하는 것은 프로토콜 구현의 일부다.
하도를 따라 흐르는 흐름에 대해 PHASE-3 §6.2.1은 1차원 Saint-Venant 방정식을 지정한다. 이 방정식은 홍수파가 관측소 사이를 이동하면서 모양이 바뀌는 과정을 기술하며, 하도 추적(river routing)의 표준적인 물리적 기반이다.
연속 방정식: ∂A/∂t + ∂Q/∂x = q
운동량 방정식: ∂Q/∂t + ∂(Q²/A)/∂x + g·A·∂h/∂x + g·A·S_f = 0
| 기호 | 의미 | 단위 | 실무에서 얻는 곳 |
|---|---|---|---|
| A | 통수 단면적 | m² | 측량 단면을 수위의 함수로 정리한 자료 |
| Q | 유량 | m³/s | 모델 상태량. RiverLevelData.current.flowRate와 비교 |
| h | 수위(수면 표고) | m | 모델 상태량. current.waterLevel과 비교 |
| g | 중력 가속도 | 9.81 m/s² | 상수 |
| Sf | 마찰 경사 | 무차원 | 저항 법칙. 흔히 보정된 조도계수 n을 쓰는 Manning 식 |
| q | 단위 길이당 측방 유입량 | m³/s/m | 지류, 지역 유출, 도시 배수 방류구 |
| x, t | 하도 방향 거리, 시간 | m, s | 모델 격자 |
연속 방정식은 물이 생기지도 사라지지도 않는다는 뜻이다. 한 구간에 들어오는 물이 나가는 물보다 많으면 단면이 차오르고 수위가 오른다. 운동량 방정식은 흐름의 국부 가속도와 이송 가속도를 압력 경사(수면 경사) 및 마찰과 균형시킨다. 마찰 경사를 Manning 식으로 쓰면 Sf = n²Q|Q| / (A²R4/3)이 되며, 여기서 R은 동수 반경이다. 조도계수 n은 대부분의 하천 모델에서 가장 중요한 보정 변수다.
실무 시스템은 간략형을 쓰는 경우가 많다. 두 가속도 항을 빼면 확산파 근사가 되고, 압력 항까지 빼면 수면이 하상과 평행하다고 가정하는 운동파 근사가 된다. 간략형은 계산이 싸고 안정적이지만 배수위(backwater)를 표현하지 못한다. 본류 수위가 높아 지류가 역류하듯 막히는 경우나 감조 구간이 그 예다. ENE-033은 특정 해법을 강제하지 않는다. 완전 방정식에는 4점 음해법인 Preissmann 기법이 흔히 쓰인다. 양해법은 구현이 단순하지만 격자 간격, 파속, 시간 간격을 묶는 Courant 안정 조건을 지켜야 한다.
제2장의 데이터 객체는 1D 모델의 정해진 자리에 들어간다.
DamStatusData.flow.outflow를 쓴다. 제2장에서 권고한 해석에 따르면 outflow는 총 방류량이며 여수로 방류량 flow.discharge를 이미 포함하므로, 두 값을 더해서는 안 된다. 댐의 dischargePlan은 미래 경계 조건이 된다. 댐 방류 계획이 단순한 상태 보고가 아니라 예측 입력인 이유가 여기에 있다.PrecipitationData로 구동하고 soilSaturation을 반영한 강우–유출 모델에서 얻는다.waterLevel 쪽으로 보정한다.RiverLevelData.forecast(첨두 수위, 첨두 시각, 시간별 값)를 채운다.물이 하도를 벗어나거나, 도시에서 비가 배수 능력보다 빨리 고이면 흐름은 더 이상 한 방향에 갇히지 않는다. PHASE-3 §6.2.2는 이 경우에 2차원 천수 방정식(shallow-water equations)을 지정한다.
∂h/∂t + ∂(hu)/∂x + ∂(hv)/∂y = R
∂(hu)/∂t + ∂(hu²)/∂x + ∂(huv)/∂y = −g·h·∂z/∂x − τ_x
∂(hv)/∂t + ∂(huv)/∂x + ∂(hv²)/∂y = −g·h·∂z/∂y − τ_y
여기서 h는 수심(m), u와 v는 수심 평균 유속 성분(m/s), z는 지반 고도(m), R은 강우 원천항(m/s), τx와 τy는 바닥 전단 응력이다. 명세에 적힌 형태에서 응력 항은 단위를 맞추려면 운동학적 응력(바닥 전단 응력을 물의 밀도로 나눈 값)으로 읽어야 한다. 직접 해석기를 구현하는 독자는 인쇄된 식에 하상 경사 항 −gh∂z/∂x만 있다는 점도 확인해야 한다. 많은 해석기는 압력 항을 ∂(½gh²)/∂x로 따로 쓴다. 두 형태는 식을 일관되게 정리했을 때만 같으므로, 결과를 비교하기 전에 해석기가 어떤 표기 관례를 쓰는지 확인해야 한다.
2D 모델은 출력 시간 단계마다 수심 격자를 만든다. PHASE-3 §6.1.1과 FloodPredictionResult 객체는 여기서 무엇을 도출해야 하는지 정한다.
| 출력 필드 | 수심 격자에서 도출하는 방법 |
|---|---|
inundationArea.estimatedArea (km²) | 최대 수심이 침수 판정 임계값을 넘는 격자 면적의 합. 임계값은 구현 선택이며 반드시 공표해야 한다(예: 0.05 m). |
inundationArea.maxDepth (m) | 모든 격자와 시간 단계에 걸친 최대 수심 |
inundationArea.averageDepth (m) | 침수 격자의 최대 수심 평균 |
inundationArea.geoJson | 침수 격자를 감싸는 폴리곤. 소비자가 그릴 수 있는 허용 오차로 단순화 |
inundationArea.affectedPopulation, affectedBuildings | 침수 폴리곤과 인구·건물 레이어의 중첩 |
v1.0에는 두 가지 한계가 있다. 릴리스 노트는 수평 해상도 1 m보다 세밀한 라이다 기반 DEM에 의존하는 침수 모델은 v1.0 범위 밖이며, 폭풍해일 결합은 v1.1 로드맵 항목이라고 밝힌다. 실무적으로 보면 ENE-033 v1.0 시스템은 1 m 이상의 DEM에서 하천 침수와 도시 침수를 모델링할 수 있지만, 해일과 하천 유량이 함께 일으키는 해안 홍수는 표준의 모델 정의에 들어 있지 않다. coastal-flood 이벤트 유형은 존재하므로 발령 기관이 외부 해일 모델을 써서 해안 홍수 경보를 낼 수는 있다. 다만 이 경우 침수 범위가 ENE-033 모델에서 나온 것이 아님을 경보 메타데이터에 밝혀야 한다.
해상도는 계산 시간과 맞물린다. 2차원에서 격자 크기를 절반으로 줄이면 격자 수가 네 배가 되고, 양해법에서는 Courant 조건 때문에 시간 간격도 절반이 되므로 계산 시간은 대략 여덟 배로 늘어난다. 요청 필드 resolution(coarse / medium / fine)이 운영상 중요한 이유다. 세밀한 실행은 그것이 지원해야 할 선행 시간 안에 끝나지 않을 수 있다.
PHASE-3 §6.1.1은 데이터 기반 모델을 세 층으로 설명한다. 입력층은 과거 및 예측 강수(과거 24시간과 미래 예측), 주요 지점의 하천 수위, 댐 저수량과 방류량, 지형(고도, 경사, 토지 이용), 배수 시설 현황을 받는다. 은닉층은 시계열 패턴을 학습하는 LSTM, 공간 패턴을 인식하는 CNN, 주요 요인에 가중치를 주는 어텐션 메커니즘을 결합한다. 출력층은 홍수 발생 확률(0–100 %), GeoJSON 형식의 예상 침수 범위, 최고 침수 심도, 첨두 시각을 낸다.
명세에는 "10개 주요 지점", "과거 30년 이상"의 홍수 데이터 같은 값이 나온다. 이것은 합리적인 구성의 예시로 읽어야 하며 요구 사항이 아니다. 작은 유역에는 쓸 만한 관측소가 세 곳뿐일 수 있고, 최근에 도시화된 유역은 대표성 있는 기록이 10년 남짓일 수 있다.
§6.1.2의 학습 데이터 범주는 역사적 홍수 사례, 기상 데이터(강수량, 풍속, 기압), 수문 데이터(수위, 유량), 지형 데이터(DEM), 인프라 데이터(배수 시설, 제방)다. 범주마다 약점이 있으며, 어떤 구현이든 다음 세 가지 함정은 명시적으로 다루어야 한다.
모델은 학습 기간 동안 성립했던 강우와 하천 반응의 관계를 배운다. 그런데 이 관계는 유역이 도시화될 때(불투수면 증가, 빠른 유출), 댐이나 제방이 건설될 때, 하도를 준설할 때, 또는 강우 기후 자체가 바뀔 때 달라진다. 이런 변화를 가로질러 학습한 모델은 서로 다른 두 하천의 평균을 배운다. 대응책으로는 마지막 주요 구조 변화 이후 기간만으로 학습하는 방법, 변화를 기술하는 입력(예: 연도별 불투수 비율)을 모델에 명시적으로 주는 방법, 알려진 개입이 있을 때마다 다시 검증하는 방법이 있다.
위험 수위를 넘는 홍수는 정의상 드물다. 시간 단위 데이터에서 "홍수" 범주는 전체 표본의 극히 일부일 수 있다. 전체 오차를 최소화하도록 학습한 모델은 거의 항상 "홍수 없음"을 예측하면서도 좋은 점수를 받는다. 대응책으로는 고유량 표본에 손실 가중치를 더 주는 방법, 전체 정확도 대신 사건 기반 지표(5.6절)로 평가하는 방법, 1D/2D 모델로 물리적으로 모의한 극한 사건을 학습 자료에 보태는 방법이 있다. 마지막 방법은 AI 모델이 물리 모델의 편향까지 물려받게 되므로 신중히 써야 한다.
재현 예측은 과거를 다시 돌려 모델을 시험하는 것이다. 누설은 재현 예측이 실시간에는 알 수 없었던 정보를 모델에 줄 때 생긴다. 흔한 경우는 다음과 같다. 당시 실제로 발표된 강우 예측 대신 관측 강우를 "예측" 입력으로 쓰는 경우, 시계열을 무작위로 나누어 같은 사건의 인접 시간이 학습용과 시험용에 함께 들어가는 경우, 전체 기록으로 계산한 통계로 입력을 정규화하는 경우, 실시간 시스템은 원자료를 보는데 품질 관리와 수정이 끝난 관측 자료를 쓰는 경우다. 어느 경우든 재현 예측은 실제 운영보다 좋아 보이게 된다. 안전한 방법은 사건 단위나 연도 단위로 자료를 나누고, 발표 시점에 실제로 가용했던 예측 입력을 그대로 보관하는 것이다.
AI 모델은 앙상블의 한 구성원이지 물리 모델의 대체물이 아니다. 빠른 속도와 물리 모델이 담지 못하는 지역 반응을 학습하는 능력이 장점이지만, 학습 자료의 범위를 넘는 외삽에 약하다. 가장 위험한 홍수는 바로 그 범위 밖에 있다.
| 모델 계열 | 장점 | 대표적 약점 | 주요 ENE-033 출력 |
|---|---|---|---|
| 1D Saint-Venant | 물리 기반 하도 추적. 댐 방류와 지류를 명시적으로 처리 | 측량 단면과 보정이 필요. 홍수터 세부 표현 불가 | RiverLevelData.forecast |
| 2D 천수 방정식 | 공간적 침수 범위와 수심 | 계산 비용. DEM 품질이 정확도를 제한 | GeoJSON을 포함한 inundationArea |
| AI (LSTM + CNN + 어텐션) | 빠름. 자료에서 지역 반응을 학습 | 경험하지 못한 극한으로의 외삽에 약함. 체계 변화에 민감 | 확률, 첨두 시각, 침수 범위 |
| 통계 모델 | 단순하고 투명한 기준선 | 새로운 조건을 표현하지 못함 | 임계값 초과 확률 |
PHASE-3 §6.3은 세 구성원, 즉 AI 모델, 수문 모델, 통계 모델로 이루어진 EnsemblePrediction을 정의한다. 각 구성원은 완전한 FloodPredictionResult를 반환한다. 그리고 두 가지 요약을 요구한다. 가중치 없는 ensemble 블록(평균 확률, 중앙값 확률, 신뢰도, 불확실성)과 모델 성능에 기반한 가중치를 쓰는 weighted 블록(확률, 침수 면적, 첨두 시각)이다.
interface EnsemblePrediction {
models: {
ai: FloodPredictionResult;
hydrological: FloodPredictionResult;
statistical: FloodPredictionResult;
};
ensemble: {
meanProbability: number;
medianProbability: number;
confidence: number;
uncertainty: number;
};
weighted: { // 모델 성능 기반 가중 평균
probability: number;
inundationArea: number;
peakTime: string;
};
}
EnsemblePrediction은 이 장 뒤쪽의 ModelPerformance, AlertCriteria, AlertDistribution과 마찬가지로 PHASE-3 본문에는 정의되어 있지만 참조 SDK의 types.ts에는 들어 있지 않다. 이 객체를 실제로 주고받아야 하는 구현은 명세를 근거로 직접 정의하고, 이후 SDK 릴리스가 이를 추가하면 이름을 맞출 준비를 해 두어야 한다.
"모델 성능 기반 가중치"라는 표현은 방법을 열어 둔다. 설득력 있는 선택은 같은 유역, 같은 계절의 과거 사건에서 검증된 기량으로 구성원별 가중치를 정하되, 가중치를 적용할 대상에 맞는 점수를 쓰는 것이다.
예를 들어 보자(숫자는 예시다). 세 구성원이 각각 72 %, 58 %, 40 %의 홍수 확률을 냈고, 과거 검증에서 가중치가 0.50(AI), 0.35(수문), 0.15(통계)로 정해졌다고 하자. 가중치 없는 평균은 56.7 %, 중앙값은 58 %이지만, 가중 확률은 0.50 × 72 + 0.35 × 58 + 0.15 × 40 = 62.3 %이다. 이 차이는 60 % 임계값을 넘나들기에 충분하다. 가중 방법을 문서화하고 고정해 두어야 하며, 실행마다 조정해서는 안 되는 이유다.
명세가 정하지 않아 구현자가 결정해야 하는 세부 사항이 세 가지 있다.
uncertainty — 명세는 이를 정의하지 않는다. 단순하고 설명 가능한 선택은 구성원 확률의 퍼짐(표준편차 또는 범위)이다. 그 정의를 예측 노드 문서에 밝혀야 한다.weighted.inundationArea는 숫자 하나지만 각 구성원은 완전한 InundationArea 객체를 가진다. estimatedArea 값은 평균할 수 있지만 GeoJSON 폴리곤을 평균하려 해서는 안 된다. 어느 구성원의 범위를, 또는 어떤 대응 규칙으로 만든 범위를 공표하는지 밝혀야 한다.weighted.peakTime — 시각의 평균은 계산할 수 있다(에포크 시간으로 바꾸어 가중한 뒤 되돌린다). 그러나 구성원이 여러 시간씩 어긋나면 평균 시각 하나가 그럴듯한 두 첨두 사이에 떨어질 수 있다. 퍼짐을 함께 보고해야 한다.구성원 셋은 작은 앙상블이다. 모델 구조의 불확실성은 담지만, 몇 시간 이후에는 흔히 가장 큰 오차 원인이 되는 강우 예측 자체의 불확실성은 담지 못한다. 기상 기관이 앙상블 강우 예측을 제공한다면 강우 구성원마다 수문 모델을 한 번씩 돌리는 것이 결과 범위를 훨씬 잘 보여 준다. ENE-033 구조에서는 hydrological 구성원을 이런 내부 앙상블의 요약으로 취급하여 이를 담을 수 있다.
PHASE-3 §6.1.3은 여덟 개 필드를 가진 ModelPerformance를 정의한다.
| 필드 | 의미 | 단위 |
|---|---|---|
accuracy | 전체 예측 중 맞은 비율(정확도) | % |
precision | 홍수로 예측한 것 중 실제로 일어난 비율(정밀도) | % |
recall | 실제 홍수 중 예측한 비율(재현율) | % |
f1Score | 정밀도와 재현율의 조화 평균 | 점수(명세의 목표값은 백분율로 적혀 있다) |
rmse | 연속 변수(보통 수위)의 평균 제곱근 오차 | 변수의 단위(m) |
mae | 같은 변수의 평균 절대 오차 | 변수의 단위(m) |
leadTime | 예측 발표부터 사건까지의 시간 | 시간 |
falseAlarmRate | 허위 경보의 비율(아래 정의 문제 참조) | % |
범주형 지표는 2 × 2 분할표에서 나온다. 정해진 임계값, 지점, 시간 창에 대해 "홍수 / 홍수 아님" 예측 하나하나를 실제 결과와 비교한다.
| 홍수 발생 | 홍수 미발생 | |
|---|---|---|
| 홍수 예측 | a — 적중 | b — 허위 경보 |
| 홍수 아님 예측 | c — 놓침 | d — 정확한 부정 |
탐지 확률(POD) POD = a / (a + c) (= 재현율)
허위 경보 비율(FAR) FAR = b / (a + b) (= 1 − 정밀도)
임계 성공 지수(CSI) CSI = a / (a + b + c)
정확도 ACC = (a + d) / (a + b + c + d)
F1 점수 F1 = 2a / (2a + b + c) (= 2·CSI / (1 + CSI))
허위 탐지 확률 POFD = b / (b + d)
여기에는 이름의 함정이 있다. 예보 검증에서 허위 경보 비율(false alarm ratio, b / (a + b))은 발령한 경보 중 틀린 것의 비율이다. 반면 허위 탐지 확률(POFD, b / (b + d))은 홍수가 아니었던 경우 중 잘못 홍수로 표시한 비율이며, 이것도 영어로 false alarm rate라 불리기도 한다. 희귀 사건에서는 두 값이 자릿수 단위로 달라질 수 있다. ENE-033의 falseAlarmRate는 어느 쪽인지 밝히지 않는다. 구현은 숫자 옆에 공식을 공표해야 한다. 이 책은 허위 경보 비율을 권한다. 주민이 체감하는 "틀린 경보"가 바로 그것이기 때문이다.
한 관측소에서 시간 단위 예측 1,000건, 그중 홍수 시간이 10건인 검증 자료를 예로 들어 보자. 항상 "홍수 없음"이라고 답하는 모델은 정확도 99 %를 얻지만 POD는 0이다. 쓸모 있는 모델이 a = 6, b = 14, c = 4, d = 976을 냈다고 하자. 정확도는 98.2 %로 쓸모없는 모델보다 낮지만 POD는 60 %, FAR은 70 %, CSI는 25 %이다. 정확도는 쉽고 수가 많은 정확한 부정이 좌우한다. 그러므로 보고서는 사건 중심 점수(POD, FAR, CSI)를 앞세우고, 정확도는 기저 발생률과 함께만 인용해야 한다.
같은 예는 높은 POD 목표와 낮은 허위 경보 목표가 서로 당기는 이유도 보여 준다. 더 일찍, 더 자주 경보하면 POD와 FAR이 함께 오른다. 균형점을 어디에 둘지는 모델의 성질이 아니라 경보 기관의 정책 선택이다.
숫자가 의미를 가지려면 검증 단위도 먼저 정해야 한다. 같은 예측이라도 시간별, 관측소별, 사건별, 행정구역별로 세면 전혀 다른 값이 나온다. 사건 기반 집계 — 이 홍수에 앞서 필요한 선행 시간 이상 먼저 경보가 나갔는가, 이 경보에 대응하는 홍수가 있었는가 — 가 주민의 체감에 가장 가깝다. 대외 보고에는 이것을 쓰고, 시간별 점수는 모델 개발용으로 두는 것을 권한다.
PHASE-3은 targetPerformance로 정확도 85, 정밀도 80, 재현율 90, F1 85, 선행 시간 6시간, 허위 경보율 15를 제시한다. 이것은 명세가 밝힌 설계 목표이며 어떤 시스템의 측정 결과도 아니다. 두 가지를 주의해야 한다. 첫째, 정밀도 80 %는 허위 경보 비율 20 %를 뜻하므로, "허위 경보율" 15 % 목표는 다른 양(예: POFD)을 뜻할 때만 앞뒤가 맞는다. 정의를 고정해야 하는 또 하나의 이유다. 둘째, 위의 예가 보여 주듯 희귀 사건 문제에서 정확도 85 % 목표는 말해 주는 것이 적다. 어떤 성능 주장이든 검증 기간, 임계값, 사건 수, 사용한 공식을 함께 밝혀야 한다.
RMSE와 MAE는 첨두 수위 같은 연속량에 대해 범주형 점수를 보완한다. RMSE는 큰 오차에 더 큰 벌점을 주므로 크게 놓친 홍수 한 건에 더 민감하고, MAE는 더 강건하다. 정확도가 오해를 부르는 것과 같은 이유로, 두 값 모두 전체 기간과 별도로 고유량 기간에 대해 계산해야 한다.
경보 발령 기관의 첫 번째 일은 예측을 경보로 만들지 판단하는 것이다. PHASE-3 §7.1은 두 부분으로 된 AlertCriteria를 정의한다.
interface AlertCriteria {
automatic: {
riskLevel: RiskLevel; // LEVEL-2 이상
probability: number; // 60 % 이상
leadTime: number; // 최소 선행 시간 (시간)
affectedPopulation: number; // 최소 영향 인구
};
manualReview: {
uncertaintyThreshold: number;
criticalInfrastructure: boolean;
historicalSignificance: boolean;
};
}
명세는 두 부분을 어떻게 결합하는지 말하지 않는다. 필드 이름과 들어맞고 이 책이 권하는 해석은 다음과 같다. automatic 조건을 모두 충족하고 manualReview 조건이 하나도 걸리지 않을 때만 자동 발령한다. 조건이 하나라도 걸리면 — 앙상블 불확실성이 임계값을 넘거나, 영향 지역에 핵심 기반 시설(병원, 변전소, 지하철 출입구 등)이 있거나, 사건이 역사적으로 중대할 때(예: 관측 이래 최대 홍수를 넘을 때) — 당직자에게 넘긴다. 사람의 검토는 자동 논리가 가장 믿기 어려운 경우를 위한 안전장치이며, 경보를 낼 법적 권한을 그 권한을 가진 사람에게 남겨 둔다.
확률 60 %와 LEVEL-2 임계값은 명세에 적힌 참조 기본값이며, 특정 유역에 대해 검증된 값이 아니다. 경보 기관은 지역 검증 결과와, 놓침과 허위 경보에 매기는 비용에 따라 자체 임계값을 정해야 한다. 또한 ENE-033의 LEVEL 단계는 홍수통제소가 발령하는 공식 홍수특보(홍수주의보·홍수경보)와 같은 것이 아니다. 둘을 같은 것으로 보지 말고 대응표로 연결해야 하는 이유는 제1장에서 설명했다.
참조 TypeScript SDK는 이 논리의 단순한 형태를 구현한 보조 함수 네 개를 제공한다.
| 함수 | 참조 구현의 규칙 |
|---|---|
calculateRiskScore(p, r, d, dr) | 0.3 × 강수 + 0.4 × 하천 수위 + 0.2 × 댐 + 0.1 × 배수 (각 하위 점수 0–100) |
getRiskLevelFromScore(s) | 80 이상 → LEVEL-4, 60 이상 → LEVEL-3, 40 이상 → LEVEL-2, 20 이상 → LEVEL-1, 그 밖 → LEVEL-0 |
isEvacuationRequired(level, prob) | 단계 ≥ LEVEL-3 이고 확률 ≥ 60 |
estimateTimeToOverflow(current, danger, rise) | (danger − current) / rise. 상승률 ≤ 0 이거나 현재 수위 ≥ 기준이면 null |
가중치와 구간 경계는 참조 구현의 기본값이며 검증된 임계값이 아니다. SDK는 각 0–100 하위 점수를 어떻게 만드는지도 정하지 않는다. 예컨대 하천 수위 하위 점수는 관심 수위와 위험 수위 사이 거리에서 현재 수위가 차지하는 비율로 만들 수 있다. 어떤 대응 방식을 쓰든 배포 문서에 적어야 한다. 최종 위험 단계는 가중치만큼이나 이 대응 방식에 좌우되기 때문이다.
예시 하위 점수로 계산해 보자. 강수 70, 하천 수위 85, 댐 40, 배수 60이면 21 + 34 + 8 + 6 = 69이고, 이는 LEVEL-3(경계)이다. 앙상블 확률이 62.3 %라면 isEvacuationRequired는 true를 반환한다.
README 예시를 바탕으로 한 계산 예다(README는 5.0 m를 경계 수위로 적었지만 여기서는 위험 수위로 본다). 현재 수위 4.2 m, 위험 수위 5.0 m, 수위 상승률 0.2 m/h이면 estimateTimeToOverflow는 (5.0 − 4.2) / 0.2 = 4시간을 반환한다.
이것은 선형 외삽이다. 상승률이 일정하다고 가정하는데, 홍수의 상승부에서는 그런 경우가 드물다. RiverLevelData.trend에는 acceleration(m/h²)도 있다. 가속도 a가 일정하면 시간 t 뒤의 상승량은 rt + ½at²이다. 예시로 a = 0.02 m/h²를 넣으면 0.8 = 0.2t + 0.01t²이고, 해는 약 3.4시간으로 선형 추정보다 30분 넘게 이르다. 두 방법 모두 하도 추적 예측에 비하면 거칠다. 예측 노드를 쓸 수 없을 때의 대체 수단, 그리고 예측 노드 결과를 점검하는 용도로 쓸모가 있다. 두 가지를 더 짚어 둔다. 함수의 매개변수 이름은 dangerLevel이지만 물리적 월류는 bankHeight에서 일어나므로, "월류까지 시간"이 어느 기준을 뜻하는지 정하고 문서화해야 한다. 또 이 함수는 수위가 내려갈 때와 이미 기준을 넘었을 때 모두 null을 반환하므로, 호출하는 쪽에서 현재 수위를 확인해 둘을 구분해야 한다.
RiskLevel은 값이 'LEVEL-0'부터 'LEVEL-4'까지인 문자열 열거형이다. isEvacuationRequired는 이를 riskLevel >= RiskLevel.LEVEL_3으로 비교한다. JavaScript에서 이것은 사전식 문자열 비교다. 다섯 값이 같은 접두어를 가지고 숫자 한 자리만 다르기 때문에 우연히 맞는 답이 나올 뿐이다. 'LEVEL-10' 같은 단계가 추가되거나(이 값은 'LEVEL-2'보다 앞에 정렬된다), 값이 현지화되거나, 대소문자가 다른 값이 들어오면 아무 경고 없이 틀린다. 숫자 대응표를 쓰면 이 의존성이 사라진다.
import { RiskLevel } from './types';
const LEVEL_RANK: Record<RiskLevel, number> = {
[RiskLevel.LEVEL_0]: 0,
[RiskLevel.LEVEL_1]: 1,
[RiskLevel.LEVEL_2]: 2,
[RiskLevel.LEVEL_3]: 3,
[RiskLevel.LEVEL_4]: 4,
};
// 참조 구현 기본값. 유역별로 지역 검증 결과에 따라 정한다.
const WEIGHTS = { precipitation: 0.3, river: 0.4, dam: 0.2, drainage: 0.1 };
const CUTOFFS: [number, RiskLevel][] = [
[80, RiskLevel.LEVEL_4],
[60, RiskLevel.LEVEL_3],
[40, RiskLevel.LEVEL_2],
[20, RiskLevel.LEVEL_1],
];
export function riskScore(s: { precipitation: number; river: number;
dam: number; drainage: number }): number {
return s.precipitation * WEIGHTS.precipitation + s.river * WEIGHTS.river
+ s.dam * WEIGHTS.dam + s.drainage * WEIGHTS.drainage;
}
export function levelFromScore(score: number): RiskLevel {
for (const [min, level] of CUTOFFS) if (score >= min) return level;
return RiskLevel.LEVEL_0;
}
export function atLeast(level: RiskLevel, min: RiskLevel): boolean {
return LEVEL_RANK[level] >= LEVEL_RANK[min];
}
export function evacuationRequired(level: RiskLevel, probability: number): boolean {
return atLeast(level, RiskLevel.LEVEL_3) && probability >= 60;
}
// 예: riskScore({precipitation:70, river:85, dam:40, drainage:60}) === 69
// levelFromScore(69) === RiskLevel.LEVEL_3
AlertCriteria.automatic.riskLevel 검사에도 같은 atLeast 함수를 써야 한다. 그래야 자동 발령 규칙과 대피 규칙이 서로 어긋나지 않는다.
자동 발령 규칙과 대피 규칙은 모두 확률 60 %를 쓰지만 적용 단계가 다르다(LEVEL-2 대 LEVEL-3). 두 값을 하나의 설정 원천에 두어야 한다. 기관이 한쪽만 올리고 다른 쪽을 잊으면, 경보조차 내지 않을 사건에 대피를 권고하는 시스템이 될 수 있다.
FloodAlert가 발령되면 PHASE-3 §7.3은 그 전달을 AlertDistribution으로 기술한다. 이 객체는 세 부분으로 되어 있다. 어떤 채널을 쓰는지, 우선순위 집단별 수신자가 누구인지, 메시지가 어떻게 되었는지다.
| 채널 플래그 | 매체 | 실무적 성격 |
|---|---|---|
sms | 등록 번호로 보내는 문자 메시지 | 대상 지정 가능. 가입자 명단 필요. 부하가 몰리면 대기열이 생김 |
push | 앱 푸시 알림 | 풍부한 내용과 링크. 앱 사용자에게만 도달 |
email | 이메일 | 공무원과 협력 기관에는 적합하지만 주민에게는 너무 느림 |
broadcast | 셀 브로드캐스트(명세 표기: 재난 문자, CBS) | 가입자 명단 없이 대상 기지국 안의 호환 휴대전화 전체에 도달 |
siren | 옥외 사이렌 | 휴대전화 없이 작동. 그 자체로는 담는 정보가 적음 |
radio, tv | 라디오 방송, TV 자막 | 넓은 도달 범위. 방송사 협조에 의존 |
social | SNS | 확산과 후속 소식 전달용. 주 채널은 아님 |
수신자는 highRisk(고위험 지역 주민), vulnerable(취약 계층), firstResponders(재난 대응 인력), publicOfficials(공무원), general(일반 주민)로 나뉜다. 이 구분에는 순서가 담겨 있다. 대응 인력과 공무원은 주민보다 먼저 움직일 수 있도록 일찍 경보를 받아야 한다. 취약 계층 — 거동이 불편한 사람, 요양 시설, 한국어를 읽지 못하는 사람 — 에게는 더 이른 통보와 그들에게 맞는 채널이 모두 필요하다. FloodAlert.instructions가 ko와 en 문구를 따로 가지는 이유이기도 하다.
status 블록은 sent(발송), delivered(전달), failed(실패), confirmed(확인) 건수를 센다. 명세는 이들 사이의 관계를 정하지 않는다. 지켜야 할 합리적인 불변 조건은 전달 + 실패 ≤ 발송(나머지는 아직 대기 중)과 확인 ≤ 전달이다. 건수는 합계만이 아니라 채널별, 수신 집단별로 보관해야 한다. 공무원에게는 모두 도달했지만 셀 브로드캐스트 채널에서 실패한 경보는 주민 경보로서는 실패한 것이기 때문이다. 셀 브로드캐스트와 사이렌은 보통 수신자별 확인을 주지 않으므로, 이들의 delivered는 "통신망이 접수함"이라는 뜻일 수밖에 없다. 이를 수신 확인처럼 보이게 두지 말고 그 의미를 기록해 두어야 한다.
갱신과 해제도 첫 메시지만큼 신경 써야 한다. 격상, 연장, 철회되는 경보는 원래 alertId를 참조로 다시 써서 소비자가 서로 충돌하는 두 메시지를 보여 주지 않고 앞선 메시지를 대체하게 해야 한다. 이것이 CAP 1.2 메시지 유형에 어떻게 대응하는지는 제3장에서 다루었다. README의 "5분 이내" 전달 수치도 명세의 다른 성능 수치와 마찬가지로 설계 목표이지 보장이 아니다. status 기록으로 채널별 발령–전달 시간을 실측하고, 5.6절의 검증 점수와 함께 보고해야 한다.
inundationArea와 GeoJSON이 된다. 1 m 미만 라이다 DEM과 폭풍해일 결합은 v1.0 범위 밖이다.EnsemblePrediction은 AI, 수문, 통계 구성원을 결합한다. 가중치는 문서화되고 검증된 기량에서 나와야 하며, 정의되지 않은 uncertainty 필드는 정의를 밝혀야 한다.제6장은 프로토콜에서 배포로 넘어간다. 이 역할들을 실제 기반 시설에 어떻게 배치하고, 보호하고, 국가·지자체 시스템과 연결하는지를 다룬다.