홍수 예측의 품질은 입력되는 관측값의 품질을 넘지 못한다. 이 장은 WIA-ENE-033 PHASE 1이 정의하는 네 가지 관측 객체 PrecipitationData, RiverLevelData, DamStatusData, DrainageData를 필드 단위로 살펴보고, 각 필드가 나타내는 수문학적 의미를 설명하며, 공개된 스키마가 해석의 여지를 남겨 두어 구현자가 명시적으로 메워야 하는 지점을 짚는다.
제1장에서는 물이 구름에서 범람원에 이르는 경로를 따라갔다. 비가 내리고, 그 일부가 유출되며, 유출수가 하도를 따라 추적되고, 관측소에서 수위가 오르고, 마침내 물이 하도 밖으로 넘친다. 이 사슬의 각 고리는 서로 다른 기관이 운영하는 서로 다른 계측 장비로 관측된다. 우량계와 기상 레이더는 기상 기관이, 수위 관측소는 하천 관리 기관이, 저수지는 댐 운영자가, 배수 펌프장과 우수관로는 지방자치단체가 운영한다. WIA-ENE-033은 이 네 가지 출처에 각각 별도의 객체를 부여하여, 제5장에서 다룰 예측 노드가 제공자마다 다른 사설 포맷을 해독하지 않고도 관측값을 결합할 수 있게 한다. 이 관측값을 소비하는 예측 요청·결과 객체는 제3장의 주제이며, 이 장은 관측 객체 자체에 집중한다.
PHASE 1(데이터 포맷)은 정규 구조를 JSON Schema 2020-12로 표현하며 해당 PHASE가 유효한 동안 안정적으로 유지한다고 밝힌다. 그러나 공개된 자료에서 이 구조는 TypeScript 인터페이스로만 제시된다. PHASE 1 문서 자체(§5.3~§5.6)와 참조 SDK 파일 api/typescript/src/types.ts가 모두 그렇다. v1.0에는 기계가 읽을 수 있는 JSON Schema 파일이 함께 공개되어 있지 않다. 따라서 페이로드를 검증하려는 구현자는 인터페이스로부터 스키마를 직접 도출해야 한다. 어려운 작업은 아니지만 의식적으로 수행해야 한다. TypeScript 인터페이스는 값의 범위, 단위, 시간대에 대해 아무것도 말하지 않으며, 그 정보는 주석에만 들어 있기 때문이다.
PHASE 1은 데이터 모델의 틀을 이루는 네 가지 참조를 제시한다.
ENE-033은 ISO 19156 모델을 그대로 재현하지 않는다. ENE-033의 객체는 의도적으로 평탄화된 "상태 봉투(state envelope)"이다. 하나의 JSON 문서에 한 관측소의 최신값, 기준값, 짧은 예측, 그리고 도출된 상태가 함께 담긴다. 대시보드와 경보 로직에는 편리하지만, ISO 19156이 분리해 두는 개념들 — 대상 객체(하천 구간), 채취 지점(관측소), 관측 속성(수위), 절차(센서), 결과 시각 — 이 한 레코드에 합쳐진다는 뜻이기도 하다. 데이터를 WaterML 2.0으로 교환해야 할 때 이를 다시 분리하는 방법은 2.7절에서 다룬다.
PHASE 1 인터페이스의 모든 수치 필드는 주석으로 단위를 명시한다. 수위는 미터, 유량은 초당 세제곱미터, 강우 깊이는 밀리미터, 강우 강도는 시간당 밀리미터, 저수량은 백만 세제곱미터, 비율은 퍼센트이다. 즉 단위는 표준이 고정하며 페이로드에 실려 전송되지 않는다. 그 귀결은 단순하고 엄격하다. 생산자는 레코드를 내보내기 전에 반드시 ENE-033 단위로 변환해야 한다. 유량을 시간당 세제곱미터로, 저수량을 천 세제곱미터 단위로 보고하는 피드를 구제해 줄 unit 속성은 존재하지 않는다.
단위가 계약의 일부이므로, 도출된 JSON Schema에도 단위를 기록해야 한다. description이나 사용자 정의 주석 키워드를 쓰면 된다. 아래 조각은 구현자가 RiverLevelData의 current 블록을 표현하는 한 가지 예시이며, 표준이 공개한 파일이 아니다.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.org/schemas/ene033/river-current.json",
"title": "RiverLevelData.current (도출 예시)",
"type": "object",
"required": ["waterLevel", "flowRate", "velocity", "timestamp"],
"properties": {
"waterLevel": { "type": "number", "description": "수위(m), levels.*와 동일한 기준면" },
"flowRate": { "type": "number", "minimum": 0, "description": "유량(m3/s)" },
"velocity": { "type": "number", "description": "평균 유속(m/s)" },
"timestamp": { "type": "string", "format": "date-time" }
},
"additionalProperties": false
}
이 조각의 두 가지 선택은 설명이 필요하다. 첫째, 유량에 붙인 minimum: 0은 감조 구간이 아닌 하천을 전제로 한다. 하구 부근에서는 밀물 때 흐름이 역전될 수 있으므로, 그런 곳의 생산자는 이 제약을 빼고 음수가 상류 방향 흐름을 뜻한다는 것을 프로필에 밝혀야 한다. 둘째, additionalProperties: false는 지역적 선택일 뿐이다. PHASE 1 부속서 E는 새 선택 필드의 추가를 호환성을 깨지 않는 변경으로 취급하므로, 모르는 필드를 거부하는 엄격한 소비자는 첫 번째 추가 개정에서 바로 고장 난다. 소비자는 대개 모르는 속성을 허용하고 무시하는 편이 옳다.
네 객체는 네 종류의 계측 장비와 일대일로 대응한다. 따라서 유역 예측 노드는 여러 건의 RiverLevelData(관측소마다 하나), 몇 건의 DamStatusData(저수지마다 하나), 한 묶음의 DrainageData(시설마다 하나), 그리고 예측 대상 지역을 덮는 강수 데이터를 받는다. 이들이 하나의 예측으로 결합될 때, 제3장에서 보듯 FloodPredictionResult.contributingFactors는 하천과 댐은 배열로, 강수와 배수는 단일 객체로 담는다. 저장 구조를 설계할 때 기억해 둘 만한 세부 사항이다.
강우는 거의 모든 홍수 모형의 구동 입력이다. PrecipitationData는 네 부분으로 이루어진다. 이미 측정된 값(observed), 예상되는 값(forecast), 기상 레이더 영상에 대한 선택적 참조(radar), 그리고 사상의 통계적 특성(statistics)이다.
| 필드 | 형 | 단위 | 의미 |
|---|---|---|---|
observed.current | number | mm/h | 현재 강우 강도 |
observed.last1h … last24h | number | mm | 지난 1·3·6·24시간 누적 강우량 |
forecast.next1h … next24h | number | 명시 없음(mm로 해석) | 향후 1·3·6·12·24시간 예상 강우량 |
forecast.hourly | number[] | mm/h | 시간별 예측 강우 강도 |
radar.imageUrl | string | — | 레이더 영상 산출물 링크 |
radar.coverage | number | km | 레이더 산출물의 관측 범위 |
radar.lastUpdate | Timestamp | — | 레이더 산출물의 시각 |
statistics.returnPeriod | number | 년 | 관측 강우의 재현 주기 |
statistics.percentile | number | % | 기후값 분포에서 관측값의 백분위수 |
statistics.isExtreme | boolean | — | 생산자가 판정한 극한 사상 여부 |
강우 데이터에서 가장 흔한 오류는 강도와 누적량을 혼동하는 것이다. 강도는 비율(시간당 밀리미터)이고 누적량은 깊이(밀리미터)이다. 강우 1 mm는 제곱미터당 1리터이므로 두 값 모두 부피로 환산할 수 있지만, 답하는 질문이 다르다. 강도는 일정 강도에 맞춰 설계된 우수관로가 수 분 안에 역류·과부하 상태가 될지를 배수 기술자에게 알려 준다. 누적량은 유역이 얼마나 많은 물을 받았는지, 그리고 토양 수분과 함께 그중 얼마가 유출될지를 하천 예보관에게 알려 준다.
두 값은 관련되어 있지만 서로 대체할 수 없다. 시간당 30 mm로 세 시간 동안 고르게 내린 비는 90 mm를 쌓으므로, current가 30일 때 last3h는 90이 된다. 반면 시간당 60 mm의 강도로 10분 동안 쏟아지고 그친 대류성 강우는 current를 거의 0으로, last1h를 약 10 mm로 남긴다. 강도 필드만 읽는 시스템은 관로망이 아직 그 집중호우를 배수하고 있는 바로 그 순간에 아무 일도 없다고 결론 내리게 된다. 그러므로 구현체는 두 필드를 서로 대조하여 타당성을 검증해야 한다. 어떤 누적량도 음수일 수 없고, 긴 구간의 누적량은 그 안에 포함된 짧은 구간의 누적량보다 작을 수 없다(last1h <= last3h <= last6h <= last24h).
2.1절의 관측 모델에 비추어 보면, PrecipitationData에는 다른 관측 객체가 어떤 형태로든 가지고 있는 세 가지가 빠져 있다.
monitorRainfall(regionId) 메서드가 반환형을 any로 선언하고 있다는 사실도 모니터링용 형태가 아직 확정되지 않았음을 보여 준다. 우량계별 또는 레이더 격자별로 강우를 공개하는 생산자는 각 레코드를 최소한 식별자, Location, 시각으로 감싸고, 그 래퍼를 상호운용 프로필에 문서화해야 한다.last1h부터 last24h까지의 누적량은 어느 순간에 끝나는지 객체 안에 적혀 있지 않다. 이 시각이 없으면 소비자는 방금 도착한 레코드와 큐에서 한 시간 동안 머문 레코드를 구별할 수 없다.hourly에만 mm/h를 적었고, next1h … next24h에는 단위 주석이 없다. last*와의 유추로 읽으면 이들은 밀리미터 단위의 누적량이며, 이 책은 그 해석을 따른다. hourly의 첫 원소가 무엇을 뜻하는지도 정의되어 있지 않다. 자연스러운 해석은 "관측 시각부터 시작하는 한 시간"이며, 생산자는 이를 명시해야 한다.제1장은 재현 주기를 연 초과 확률의 역수로 소개했다. 생산자는 statistics.returnPeriod로 관측 강우가 얼마나 드문지를 표현한다. 그런데 중요한 맥락 하나가 스키마에 없다. 재현 주기가 어느 지속 시간에 대한 것인지이다. 한 호우가 1시간 최대 강우로는 100년 빈도이면서 24시간 총량으로는 평범할 수 있고, 그 반대도 가능하다. 생산자는 유역에 가장 관련 있는 지속 시간(도시 배수에는 짧은 지속 시간, 큰 하천에는 긴 지속 시간)으로 재현 주기를 산정하고 어떤 지속 시간을 썼는지 밝혀야 한다. percentile도 같은 모호성을 가지며, 추가로 기후값의 기준 기간에 따라 달라진다. isExtreme은 생산자의 판정이므로, 소비자는 두 생산자가 같은 기준을 쓴다고 가정해서는 안 된다.
수위 관측소는 수면의 높이인 수위를 직접 측정한다. 초당 통과하는 물의 부피인 유량은 보통 연속 측정하지 않는다. 가끔 실시한 유량 측정으로 보정한 관측소 고유의 관계식, 즉 수위-유량 관계 곡선으로 수위에서 계산한다. RiverLevelData.current는 둘 다(waterLevel, 미터; flowRate, m³/s)와 평균 velocity(m/s)를 담는다. 스키마는 유량이 측정값인지 환산값인지 말하지 않는다. 게다가 수위-유량 관계 곡선은 하도가 넘치거나 하상이 변동할 수 있는 홍수 시에 가장 신뢰도가 낮다. 생산자 프로필에 달리 적혀 있지 않다면 소비자는 flowRate를 추정값으로 다루어야 한다.
| 필드 | 형 | 단위 | 의미 |
|---|---|---|---|
riverId, name | string | — | 하천 식별자(예: HAN-001)와 표시 이름 |
stationId | string | — | 관측소 식별자 |
location | Coordinates | 도 | 관측소 위치 |
current.waterLevel | number | m | 현재 수위 |
current.flowRate | number | m³/s | 현재 유량 |
current.velocity | number | m/s | 평균 유속 |
current.timestamp | Timestamp | — | 현재값의 관측 시각 |
levels.normal / attention / warning / danger | number | m | 이 관측소의 기준 수위 사다리 |
levels.bankHeight | number | m | 제방(둑마루) 높이 |
forecast.peak, peakTime | number, Timestamp | m | 최고 예상 수위와 그 시각 |
forecast.hourly | number[] | m | 시간별 예측 수위 |
trend.rateOfRise | number | m/h | 현재 수위 변화율(하강 시 음수) |
trend.direction | enum | — | rising | stable | falling |
trend.acceleration | number | m/h² | 상승률의 변화 |
status.condition | enum | — | normal | attention | warning | danger |
status.overflowRisk | number | % | 생산자가 산정한 월류 확률(0~100) |
status.estimatedTimeToOverflow | number(선택) | h | 월류 조건에 도달하기까지의 시간 |
levels 블록은 숫자를 판단으로 바꾸어 주는 부분이다. 오름차순의 네 기준값과 물리적 둑마루 높이, 곧 평상시·주의·경계·위험·제방 높이를 담는다. 기준 수위 사다리는 관측소마다 다르다. 수위 5 m가 어떤 지점에서는 무해하고 다른 지점에서는 치명적일 수 있다. 그래서 사다리는 다른 곳에서 조회하지 않고 레코드와 함께 전달된다. RiverLevelData 한 건만 받은 소비자도 추가 질의 없이 그 관측소의 상태를 판단할 수 있다.
이 설계에서 세 가지 규칙이 나온다. 기준값은 엄격한 오름차순이어야 한다(normal < attention < warning < danger <= bankHeight). 이를 어기는 레코드는 손상된 것이다. 기준값과 현재 수위는 같은 수직 기준면을 써야 한다(2.7.1절). 그리고 status.condition은 현재 수위가 도달한 가장 높은 기준값이어야 하며, 소비자가 이를 다시 계산해 확인할 수 있어야 한다. 상태 열거형이 danger에서 끝난다는 점에도 주의해야 한다. "제방 높이 초과"를 나타내는 별도 값이 없다. 월류는 current.waterLevel과 levels.bankHeight를 직접 비교해 감지해야 한다.
이 관측소 수준의 이름들은 예측 결과가 쓰는 5단계 위험 단계 LEVEL-0~LEVEL-4와 같은 척도가 아니며, 어느 쪽도 한국 홍수통제소가 발령하는 공식 홍수특보 용어의 동의어가 아니다. 척도 사이의 관계는 제3장에서 다루는 매핑 결정의 문제이다. 관측소 레코드 안에서 네 이름은 오직 "이 관측소의 몇 번째 기준값을 넘었는가"만을 뜻한다.
trend 블록은 상황이 얼마나 빨리 변하는지를 요약한다. rateOfRise는 시간당 미터 단위의 수위 1차 도함수, acceleration은 m/h² 단위의 2차 도함수, direction은 그 부호를 범주로 읽은 값이다. 위험 기준보다 0.6 m 아래에 있으면서 시간당 0.3 m씩 오르는 관측소는, 같은 수위이지만 하강 중인 관측소와 전혀 다른 상황이다. 실제 운영의 경보 규칙 가운데 상당수는 절대 수위만큼이나 상승률을 기준으로 표현된다.
status.estimatedTimeToOverflow는 선택 항목이며, 스키마에서 한 가지 중요한 점이 정의되어 있지 않다. 어느 수위를 "월류"로 볼 것인가이다. 참조 SDK의 보조 함수 estimateTimeToOverflow(currentLevel, dangerLevel, rateOfRise)는 위험 기준까지 남은 거리를 현재 상승률로 나누고, 하천이 상승 중이 아니거나 이미 위험 기준을 넘었으면 값을 돌려주지 않는다. 제방 높이를 기준으로 삼거나 수리 모형 예측에서 도달 시각을 읽는 생산자는 같은 상황에 대해 다른 숫자를 공개하게 된다. 계산법은 제5장에서 다룬다. 여기서의 요구는 각 생산자가 정의를 프로필에 밝히고, 소비자가 이를 확인하지 않은 채 생산자 사이에서 이 필드를 비교하지 않는 것이다.
다음 레코드는 예시이다. 관측소, 값, 시각은 내부적으로 일관되도록 지어낸 것이며 실제 관측소를 기술하지 않는다.
{
"riverId": "HAN-001",
"name": "예시 하천 (가상)",
"stationId": "HAN-001-ST03",
"location": { "latitude": 37.5170, "longitude": 126.9580 },
"current": {
"waterLevel": 4.10,
"flowRate": 2850,
"velocity": 2.1,
"timestamp": "2026-07-15T09:00:00Z"
},
"levels": {
"normal": 2.00,
"attention": 3.00,
"warning": 4.00,
"danger": 5.50,
"bankHeight": 6.80
},
"forecast": {
"peak": 5.80,
"peakTime": "2026-07-15T17:00:00Z",
"hourly": [4.36, 4.63, 4.91, 5.20, 5.46, 5.66, 5.78, 5.80, 5.72, 5.58]
},
"trend": {
"rateOfRise": 0.25,
"direction": "rising",
"acceleration": 0.01
},
"status": {
"condition": "warning",
"overflowRisk": 70,
"estimatedTimeToOverflow": 5.6
}
}
위의 규칙으로 이 레코드를 점검해 보면 필드들이 어떻게 맞물리는지 알 수 있다. 현재 수위 4.10 m는 경계 기준(4.00 m)보다 높고 위험 기준(5.50 m)보다 낮으므로 condition은 warning이다. 예측 최고 수위 5.80 m는 시간별 배열의 최댓값이며 여덟 번째 원소에 있다. 첫 원소를 10:00Z에 끝나는 한 시간으로 보면 여덟 번째 원소는 17:00Z에 끝나므로 peakTime과 일치한다. 월류까지 5.6시간은 SDK의 선형 추정 (5.50 − 4.10) / 0.25이다. 약한 양의 가속도를 반영한 시간별 예측은 다섯 번째와 여섯 번째 원소 사이에서 5.50 m를 넘으므로 그보다 조금 이르다. 두 숫자 모두 근거가 있으며, 이 레코드가 모호하지 않으려면 생산자 프로필이 estimatedTimeToOverflow의 산정 방법을 밝혀야 한다. 최고 수위는 제방 높이보다 1 m 낮으므로, 이 예측은 위험 수위에 이르지만 월류는 예상되지 않는 홍수를 기술한다. overflowRisk는 생산자 자신의 확률이어서 다른 필드로는 검증할 수 없다.
저수지는 홍수 방어 시설이면서 동시에 홍수의 원천이 된다. 호우 전과 호우 중에는 유입량의 일부를 저류하여 하류의 첨두를 낮출 수 있다. 저수량이 한계에 가까워지면 운영자는 여수로로 물을 방류해야 하고, 그 방류수는 하천을 따라 내려가 하류 지류가 이미 보태고 있는 유량에 더해진다. 큰 댐 아래의 관측소에서는 계획 방류량이 향후 수 시간 동안 가장 중요한 단일 정보인 경우가 많다. DamStatusData가 현재 상태뿐 아니라 dischargePlan을 담는 이유가 여기에 있다.
| 필드 | 형 | 단위 | 의미 |
|---|---|---|---|
damId, name | string | — | 댐 식별자와 표시 이름 |
type | enum | — | multi-purpose | flood-control | hydropower |
waterLevel.current | number | m | 현재 저수위 |
waterLevel.normal | number | m | 상시 만수위 |
waterLevel.flood | number | m | 홍수기 제한 수위 |
waterLevel.design | number | m | 설계 홍수위 |
storage.current / total / effective | number | 백만 m³ | 현재·총·유효 저수량 |
storage.percentage | number | % | 저수율 |
flow.inflow | number | m³/s | 저수지 유입량 |
flow.outflow | number | m³/s | 총 방류량 |
flow.discharge | number | m³/s | 여수로 방류량 |
flow.powerGeneration | number | m³/s | 발전 방류량 |
dischargePlan.scheduled | boolean | — | 방류 계획 여부 |
dischargePlan.startTime, duration, maxDischarge | Timestamp, number, number | —, h, m³/s | 계획 방류의 시점과 규모 |
dischargePlan.affectedArea | string[] | — | 방류의 영향을 받는 하류 지역 |
operational.gateStatus | enum | — | open | closed | partial |
operational.gateOpening | number | % | 수문 개도율 |
operational.emergencyMode | boolean | — | 비상 운영 여부 |
세 기준 수위는 저수지의 운영 범위를 나타낸다. 상시 만수위는 평상시 저류의 상한이다. 홍수기 제한 수위는 홍수기 동안 더 낮게 유지되어, 그 수위와 상위 수위 사이의 공간을 홍수 조절에 쓸 수 있게 한다. 설계 홍수위는 구조물이 설계 홍수 때 도달하도록 설계된 수위이다. 홍수기에 waterLevel.current가 flood에 얼마나 가까운지는 남은 완충 능력을 직접 보여 주는 척도이다.
저수량과 유량은 단순한 수지 관계로 연결된다. 어떤 구간 동안 저수량의 변화는 유입량에서 방류량을 뺀 값에 구간 길이를 곱한 것과 같다(홍수 시간 규모에서는 작은 증발·침투는 무시한다). 순유입 1,000 m³/s가 한 시간 지속되면 360만 m³, ENE-033의 저수량 단위로 3.6이 늘어난다. 소비자는 이 관계로 연속 레코드의 타당성을 점검할 수 있다. 유입량과 방류량이 거의 같은데 한 시간 사이에 저수량이 2,000만 m³ 늘었다면 어느 필드가 틀렸거나 단위가 잘못된 것이다. 백만 세제곱미터 단위는 오류의 단골 원천이다. 운영자에 따라 저수량을 천 세제곱미터로, 또는 억 세제곱미터 단위로 공표하기 때문이다. 변환은 레코드를 내보내기 전에 끝나야 한다.
유량·저수량 블록의 두 가지 측면은 스키마가 정의하지 않으므로 생산자 프로필에 밝혀야 한다. 유량 필드 사이의 관계는 암시되어 있을 뿐 적혀 있지 않다. 자연스러운 해석은 outflow가 여수로 방류 discharge, 발전 방류 powerGeneration, 그 밖의 방류구를 모두 포함한 총 방류량이며, 따라서 outflow는 나머지 두 값의 합 이상이라는 것이다. storage.percentage의 분모가 총 저수량인지 유효 저수량인지도 정해져 있지 않다. 소비자는 이런 관계를 레코드 거부 사유가 아니라 경고로 점검해야 한다.
dischargePlan은 댐 레코드에서 앞을 내다보는 부분이다. scheduled가 참이면 선택 필드들이 방류 시작 시각, 지속 시간, 최대 방류량을 기술한다. 하류 예측 노드에게 이 값들은 경계 조건이다. 방류수는 하도를 따라 추적되어, 거리와 유량에 따라 달라지는 도달 시간 뒤에 각 하류 관측소에 도착한다. affectedArea는 자유 문자열 목록이다. 기계가 쓸 수 있게 하려면 생산자는 지명 대신 Location.regionId에 쓰는 것과 같은 지역 식별자(2.6절)로 채워야 한다.
operational 블록은 수문 상태를 보고한다. gateOpening은 퍼센트이지만, 스키마는 댐 전체에 대해 하나의 수문 상태만 기술하는 반면 많은 댐은 여러 여수로 수문을 독립적으로 운영한다. 이 점이 중요한 경우에는 퍼센트의 뜻을 프로필에서 정의해야 한다. 예를 들어 전체 수문 용량 가운데 현재 열린 비율로 정의할 수 있다. emergencyMode는 하류 소비자가 눈에 띄게 표시해야 하는 플래그이다. 비상 운영은 일반적으로 평상시 방류 규칙이 중단되었음을 뜻하기 때문이다.
도시에서는 하천이 제방을 넘지 않고도 많은 홍수가 일어난다. 집중호우가 우수관로의 용량을 넘거나, 하천 수위가 높아 하천수가 도시로 역류하지 못하도록 배수 문을 닫아야 할 때, 제방 안쪽에 내린 비는 갈 곳이 없다. 하천 수위가 내려갈 때까지 펌프로 퍼내거나 저류지에 담아 두어야 한다. 이것이 내수 침수(제1장의 도시형 홍수)이며, DrainageData는 이에 맞서는 시설을 기술하는 객체이다. 유형 열거형은 pump-station, sewer, detention-basin의 세 가지 시설을 다룬다.
| 필드 | 형 | 단위 | 의미 |
|---|---|---|---|
facilityId, name, type | string, string, enum | — | 시설의 식별과 종류 |
capacity.design | number | m³/s | 설계 배수 용량 |
capacity.current | number | m³/s | 현재 가용 용량 |
capacity.utilization | number | % | 용량 사용률 |
pumps.total / active / standby / failed | number | 대 | 펌프 대수(펌프장만) |
pumps.efficiency | number | % | 펌프 효율 |
waterLevel.current / capacity / available | number | m | 저류지 수위, 최고 수위, 남은 여유고(저류지) |
operational.status | enum | — | active | standby | maintenance | failure |
operational.lastMaintenance, nextMaintenance | Timestamp | — | 유지보수 일정 |
operational.alerts | string[] | — | 현재 시설 경보 |
performance.flowRate | number | m³/s | 실제 배수량 |
performance.energyUsage | number | kWh | 에너지 사용량 |
performance.reliability | number | % | 시설 가동률 |
용량 블록은 시설이 무엇을 하도록 설계되었는지와 지금 무엇을 할 수 있는지를 구별한다. 일정 유량으로 설계된 펌프장이라도 한 대는 수리 중이고 다른 한 대는 대기 중이라면 현재 용량은 더 낮다. 홍수 예측에 중요한 것은 설계값이 아니라 이 차이이다. 침수 모형은 보호 구역이 실제로 얼마나 많은 물을 빼낼 수 있는지 알아야 하기 때문이다. pumps 대수도 같은 추론을 돕는다. 일관된 레코드에서는 active + standby + failed가 total과 같고, failed가 늘어나는 것은 현재 용량이 더 떨어질 것이라는 조기 신호이다.
혼동하기 쉬운 두 양이 있다. capacity.utilization은 용량 가운데 사용 중인 비율이고, performance.flowRate는 실제로 배수된 유량이다. 스키마는 사용률의 분모가 설계 용량인지 현재 용량인지 말하지 않는다. 펌프 절반이 고장 난 채 현재 용량을 모두 쓰고 있는 시설은 현재 용량 기준으로 100%, 설계 용량 기준으로는 약 50%를 보인다. 두 해석은 여유 용량에 대해 정반대의 결론으로 이어진다. 생산자는 어떤 분모를 쓰는지 밝혀야 한다.
저류지의 선택 블록 waterLevel은 저류 상태를 높이로 표현한다. 현재 수위, 최고 수위, 남은 여유고이며, 여유고는 자연스럽게 capacity − current로 읽힌다. 이는 남은 저류 부피와 같지 않다. 부피는 저류지의 형상에 달려 있으므로, 침수 모형이 부피를 필요로 한다면 저류지의 수위-저류량 관계로 환산해야 한다. 킬로와트시 단위의 energyUsage는 누적 기간이 명시되어 있지 않다. 운영자는 대개 하루 단위나 사상 시작 이후 누적으로 보고하므로, 프로필에서 어느 쪽인지 밝혀야 한다.
operational.status 열거형은 PHASE 1과 SDK 모두에서 active, standby, maintenance, failure의 네 값을 가진다. README의 간략한 데이터 소스 요약은 maintenance가 빠진 세 값짜리 pumpStatus를 쓰고 부하를 퍼센트로 표현한다. 그 요약은 PHASE 1 구조보다 앞선 개요이므로 스키마로 사용해서는 안 된다. 홍수기에 maintenance 상태인 시설은 운영상 용량 감소와 같으므로, 소비자는 이를 중립 상태가 아니라 용량 감소로 다루어야 한다.
네 관측 객체는 모두 Coordinates로 자신의 위치를 나타낸다. 십진 도 단위의 latitude와 longitude를 이름 붙은 필드로 가진 객체이다. 표준은 좌표 참조계를 명시하지 않는다. GPS 위치와 웹 지도가 보통 WGS 84를 쓰고 GeoJSON(RFC 7946)이 이를 요구하므로, 생산자는 WGS 84를 쓰고 국가 평면 좌표는 공개 전에 변환해야 한다. 예측 요청과 결과는 더 풍부한 Location 타입을 쓰며, 여기에는 regionId, name, 선택 항목인 address와 boundary 기하가 더해진다.
두 표현의 경계에서 흔한 버그가 생긴다. Coordinates는 필드에 이름을 붙이므로 순서가 상관없다. GeoJSON 위치는 경도, 위도 순서의 맨 배열이다. {latitude, longitude}를 읽은 순서대로 GeoJSON 배열에 복사하는 코드는 서울 부근 관측소의 "위도"를 126.9°로 만든다. 위도는 90°를 넘을 수 없으므로 이 경우는 범위 검사로 잡히지만, 두 값이 모두 90보다 작은 지역 — 유럽, 아프리카, 남아메리카의 상당 부분 — 에서는 뒤바뀐 좌표도 완전히 합법적이어서 그저 엉뚱한 곳에 찍힌다. 위치를 아는 관측소로 작성한 단위 시험만이 믿을 만한 방어책이다. SDK의 GeoJSON 타입도 단순화되어 있다. 선택 항목 properties를 가진 기하 객체이지만, RFC 7946에서 properties는 기하를 감싸는 Feature에 속한다. 표준 도구가 경계를 읽게 하려면 생산자는 올바른 Feature 또는 FeatureCollection 객체를 내보내야 한다.
Timestamp 타입은 ISO 8601 문자열이라고만 정의된다. ISO 8601은 오프셋 없는 지역 시각을 허용하는데, 데이터가 시간대나 일광 절약 시간 경계를 넘는 순간 모호해진다. 이 책의 모든 예시가 따르는 안전한 관례는 Z 지정자를 붙인 UTC로 내보내고(예: 2026-07-15T09:00:00Z) 지역 시각 변환은 표시할 때만 하는 것이다. 소비자는 오프셋 없는 시각을 추측으로 해석하지 말고 거부해야 한다.
README와 CLI의 예시는 식별자의 패턴을 보여 준다. 지역 ID는 REG-SEOUL-001, 하천 ID는 HAN-001 같은 형식이다. 관측소·댐·시설 식별자에는 공개된 패턴이 없다. 표준은 이들 가운데 어느 것에 대해서도 등록부를 운영하지 않으므로, 두 기관이 각자 같은 문자열을 부여할 수 있다. 기관 경계를 넘는 데이터라면 식별자를 전역적으로 유일하게 만들어야 한다. 예를 들어 발급 기관의 이름공간을 접두어로 붙인다. 또한 식별자는 안정적이어야 한다. 새 횡단면으로 이설된 관측소는 기준 수위와 수위-유량 관계 곡선이 바뀌므로 새 stationId를 받아야 한다.
| 식별자 | 사용 위치 | 예시 패턴 | 권고 |
|---|---|---|---|
regionId | Location, 경보·대시보드 객체 | REG-SEOUL-001 | dischargePlan.affectedArea에도 재사용 |
riverId | RiverLevelData | HAN-001 | 하천 또는 구간마다 하나, 소속 관측소가 공유 |
stationId | RiverLevelData | 공개되지 않음 | 이름공간 접두어, 이설 시 새 ID |
damId | DamStatusData | 공개되지 않음 | 이름공간 접두어 |
facilityId | DrainageData | 공개되지 않음 | 이름공간 접두어, 시설마다 하나 |
수위는 무언가로부터의 높이이다. 수위 관측소는 흔히 관측소 자체의 영점(수위표 영점)으로부터의 높이를 보고하는 반면, 댐 수위와 제방 둑마루는 보통 평균 해수면에 연계된 국가 높이 기준면에 대한 표고로 표현된다. 둘의 차이는 수 미터 이상일 수 있다. ENE-033에는 기준면 필드가 없으므로, 유일한 보호 장치는 내부 일관성이다. 한 레코드 안에서 current.waterLevel, 모든 levels 기준값, forecast 값은 같은 기준면을 써야 한다. 여러 관측소의 레코드를 결합할 때 — 예를 들어 종단 수면 형상을 그리거나 수리 모형에 입력할 때 — 에는 먼저 공통 표고 기준면으로 변환해야 한다. 생산자는 각 관측소의 기준면과 영점 표고를 프로필에 공개해야 한다.
홍수 경보는 시간에 민감하며, 수십 개 출처의 관측값을 결합한 예측은 그 시계들이 맞는 만큼만 일관된다. PHASE 1 부속서 E는 stratum-2 서버에 대한 NTPv4 동기화가 해당 PHASE에서 표현하는 대부분의 기한에 충분하다고 적고, 결정론적 연동이 필요한 곳에는 PTP를 권고한다. 관측 데이터에 대해서는 실무적 귀결이 있다. 원격 관측소 함체 안의 기록계는 동기화되어야 하고 시계 상태가 감시되어야 한다. 수 분씩 틀어지는 기록계는 계산된 상승률과 홍수파의 겉보기 도달 시각을 소리 없이 어긋나게 만든다.
네 객체의 수치 필드는 선택 블록을 제외하면 인터페이스에서 모두 필수이다. 그래서 우량계가 오프라인인 생산자는 0을 내보내고 싶은 유혹을 받는다. 강우에서 이는 위험하다. 0은 "비가 오지 않음"으로 읽혀, 정전이나 통신 두절이 바로 그 호우 때문에 일어났을 수 있는 순간에 예측을 낮춘다. 원칙은 측정되지 않은 값을 결코 숫자로 대체하지 않는 것이다. 레코드 전체를 쓸 수 없다면 보내지 말아야 하며, 모니터링 대시보드의 센서 상태(제4장)가 해당 관측소를 오프라인으로 집계해야 한다. 일부 필드만 쓸 수 없다면 생산자는 확장으로 선택적 품질 블록을 추가할 수 있다. 부속서 E는 선택 필드의 추가를 호환성을 깨지 않는 변경으로 본다. 다만 ENE-033 v1.0은 품질 플래그를 정의하지 않으므로, 그러한 확장은 생산자의 선택이며 상호운용 프로필에 선언해야 한다.
단위 오류는 첫날에 드러나는 일이 드물다. 센서가 교체되거나, 기록계가 재설정되거나, 데이터 제공자가 피드를 바꿀 때 나타난다. 최선의 방어는 이 장에서 설명한 관계들을 수집 단계의 타당성 점검으로 코드화하는 것이다. 아래 함수는 TypeScript로 작성한 예시이다. PHASE 1에 정의된 필드만 사용하며, 예외를 던지지 않고 경고를 반환하므로 의심스러운 레코드를 버리는 대신 격리할 수 있다.
import { RiverLevelData, TrendDirection } from '@wia/ene-033';
// 관측소 레코드 한 건에 대한 수집 단계 점검 (예시)
export function checkRiverRecord(r: RiverLevelData): string[] {
const w: string[] = [];
const L = r.levels;
if (!(L.normal < L.attention && L.attention < L.warning &&
L.warning < L.danger && L.danger <= L.bankHeight)) {
w.push('기준 수위가 오름차순이 아님');
}
if (r.current.flowRate < 0) {
w.push('유량이 음수');
}
if (!/(Z|[+-]\d\d:\d\d)$/.test(r.current.timestamp)) {
w.push('시각에 UTC 오프셋이 없음');
}
const h = r.current.waterLevel;
const expected =
h >= L.danger ? 'danger' :
h >= L.warning ? 'warning' :
h >= L.attention ? 'attention' : 'normal';
if (r.status.condition !== expected) {
w.push(`condition=${r.status.condition}, 수위로 본 상태=${expected}`);
}
if (h >= L.bankHeight) {
w.push('수위가 제방 높이 이상 (해당 열거값 없음)');
}
const dir = r.trend.rateOfRise > 0 ? TrendDirection.RISING
: r.trend.rateOfRise < 0 ? TrendDirection.FALLING
: TrendDirection.STABLE;
if (Math.abs(r.trend.rateOfRise) > 0.01 && r.trend.direction !== dir) {
w.push('추세 방향이 rateOfRise와 모순');
}
if (r.forecast.hourly.length > 0 &&
Math.max(...r.forecast.hourly) > r.forecast.peak + 1e-6) {
w.push('시간별 예측이 명시된 최고 수위를 초과');
}
return w;
}
2.3.5절의 예시에 이 함수를 적용하면 빈 목록이 반환된다. 방향 점검에 둔 작은 불감대(0.01 m/h) 자체도 구현상의 선택이다. 수위가 사실상 평탄한 관측소도 센서 잡음 때문에 아주 작은 0이 아닌 상승률을 보고하며, 그때는 stable이 정직한 기술이다.
PHASE 1 문서는 열거형을 인라인 문자열 리터럴(예: 'rising' | 'stable' | 'falling')로 적고, SDK는 TypeScript 열거형 RiverLevelStatus, TrendDirection, DamType, GateStatus, DrainageFacilityType, OperationalStatus를 선언한다. 이 열거형들의 문자열 값은 명세의 리터럴과 똑같으므로 둘은 전송 수준에서 호환된다. 어느 쪽으로 생성한 JSON 문서든 다른 쪽을 통과한다. 네 관측 객체의 필드 이름과 중첩 구조도 두 출처에서 동일하다. 눈에 띄는 차이는 다른 곳에 있다. SDK는 강우 모니터링 응답을 any로 선언하며, README의 요약 블록 DataSources는 다른 이름(예: warningLevel, dangerLevel)을 쓰므로 데이터 모델이 아니라 비공식 개요로 읽어야 한다.
국가 수문 기관과 국제 자료 교환 사업은 시계열을 OGC WaterML 2.0으로 공개하는 경우가 많다. WaterML도 함께 공급해야 하는 ENE-033 생산자, 또는 WaterML을 받아 ENE-033을 내보내는 소비자에게는 평탄화된 봉투와 관측 모델 사이의 매핑이 필요하다. 아래 표는 관측소 레코드에 대한 대응 관계이며, 강우와 저수지 데이터에도 같은 접근을 적용할 수 있다.
| WaterML 2.0 / 관측 모델 개념 | ENE-033 출처 | 비고 |
|---|---|---|
| 관측 지점(채취 객체) | stationId, name, location | 관측소마다 관측 지점 하나 |
| 채취 대상 객체(수체) | riverId | 관측소가 대표하는 하천 또는 구간 |
| 관측 속성 | 필드 이름에 암시 | 수위(waterLevel), 유량(flowRate), 유속을 별도 시계열로 |
| 단일 시각-값 결과를 갖는 관측 | current.* + current.timestamp | 현상 시각 = current.timestamp |
| 측정 시계열(예측) | forecast.hourly | ENE-033이 암시로만 두는 시작 시각과 1시간 간격을 명시해야 함, 결과 시각 = 예측 발표 시각 |
| 측정 단위 | ENE-033이 고정 | 명시적으로 기록, 예: m, m3/s, mm |
| 보간 유형 | 없음 | 수위는 순간값, 누적 강우량은 직전 구간의 합계 |
| 품질 / 한정자 | 없음 | 생산자의 품질 확장이 있으면 사용 |
| 기준 수위 | levels.* | 핵심 시계열 모델에 전용 요소가 없음, 관측 지점 메타데이터나 별도 문서로 공개 |
| 도출된 상태 | trend.*, status.* | 관측값이 아니므로 측정 시계열로 내보내지 않음 |
마지막 행은 근본적인 설계 차이를 보여 준다. WaterML은 측정되거나 예측된 것을 담는다. ENE-033의 관측소 레코드는 여기에 해석 — 상태, 월류 위험, 월류까지 시간 — 을 더하며, 이는 생산자가 계산한 값이다. 반대 방향, 즉 WaterML에서 ENE-033으로 변환할 때에는 변환기가 이 도출 필드를 계산해야 하고, 그 정의에 대한 책임도 변환기가 진다. 매핑은 또한 유효한 WaterML을 만들기 위해 ENE-033 레코드 밖에서 보충해야 할 정보가 무엇인지 드러낸다. 예측 시계열의 시작 시각, 보간 유형, 기준면이다. 이것들은 바로 이 장이 생산자의 상호운용 프로필에서 메우라고 권고한 빈틈이다.
관측 객체를 갖추었으니, 다음 장에서는 이를 소비하는 객체들로 넘어간다. 예측 요청, 위험 단계와 침수 추정을 담은 예측 결과, 그리고 결정을 대중에게 전달하는 경보 메시지이다.