📦 목차

제5장: 통신 프로토콜과 실시간 데이터 흐름 (PHASE 3)

제4장은 WIA-ENE-057 플랫폼이 무엇을 제공하는지, 즉 리소스와 위험 점수, 경보와 보고서를 설명했다. 이 장은 바이트가 어떻게 움직이는지를 다룬다. 사막화 모니터링 네트워크는 데이터를 만들어 내는 쪽이 열악한 환경에 놓여 있다는 점에서 다른 데이터 시스템과 다르다. 사헬 지대의 토양 센서는 태양광 패널과 배터리 하나로 버틴다. 현장 조사원은 휴대전화 신호가 한 칸 잡히는 곳에 오토바이를 세워 두고 조사 결과를 올린다. 위성 파이프라인은 16일 주기로 수백 메가바이트의 래스터를 밀어 넣는다. 반대편에서는 정부 부처의 대시보드가 실시간 화면을 원하고, 복원 사업 파트너는 임계값을 넘었을 때 서명된 알림을 받기를 원하며, GIS 분석가는 같은 데이터를 공개 지리공간 서비스로 데스크톱 도구에 불러오기를 원한다.

표준의 PHASE 3(프로토콜 명세)는 이 모든 참여자가 서로 대화할 수 있게 하는 규약을 정한다. 전송·보안 계층, 실시간 웹소켓(WebSocket) 스트림, 센서용 MQTT 토픽, 래스터 수집 요청, OGC 및 GeoJSON 상호운용, 이벤트와 웹훅 전달, 대량 업로드, 캐싱, 그리고 링크가 끊겨도 시스템 전체를 안정적으로 유지하는 재시도·버전 규칙이 그것이다. 이 장은 명세의 절 순서를 따르되, 명세 예시가 느슨하거나 서로 어긋나는 곳도 짚는다. 바로 그런 곳에서 둘 다 적합한 구현체가 서로 연동에 실패하기 때문이다. 동반 시뮬레이터의 세 번째 탭(「3. 프로토콜」)을 열면 이 장에서 설명하는 API·웹소켓 메시지 형태를 직접 확인할 수 있다.

5.1 프로토콜 스택과 설계 원칙

다섯 계층, 여러 채널

명세는 다섯 계층의 스택을 제시한다. 애플리케이션 계층(RESTful API), 전송 계층(HTTPS와 WSS), 데이터 형식 계층(JSON과 GeoJSON), 인증 계층(OAuth 2.0과 JWT 액세스 토큰), 네트워크 계층(TCP/IP)이다. 실제로 애플리케이션 계층은 하나의 API가 아니라 하위 계층을 공유하는 네 개의 채널이다. 요청·응답 방식의 REST, 갱신을 밀어 주는 웹소켓, 자원이 제한된 기기를 위한 MQTT, GIS 클라이언트를 위한 OGC 웹 서비스가 그것이다. 이들을 가르는 차이보다 공통점이 더 중요하다. 모든 채널은 locationId를 키로 하는 같은 PHASE 1 레코드를 실어 나르고, 모든 채널은 TLS 위에서 동작하며, 모든 채널은 같은 인가 서버가 발급한 자격 증명으로 인증한다.

 생산자(Producers)              플랫폼(WIA-ENE-057)                   소비자(Consumers)
 ---------                      ----------------------                 ---------
 soil / weather sensors --MQTT (TLS)--> broker --+
 field apps (ODK)       --HTTPS POST----------->  |
 satellite pipeline     --HTTPS multipart------>  +--> validation --> record store
 partner systems        --HTTPS bulk upload---->  |        |               |
                                                  |        v               v
                                                  |   event bus ----> webhooks  --> partner servers
                                                  |        |                     
                                                  |        +--------> WSS stream --> dashboards
                                                  +-----------------> REST / OGC --> GIS, reports

 모든 화살표에 공통으로 쓰이는 계층:
   Application : REST resources, WebSocket messages, MQTT topics, OGC operations
   Transport   : HTTPS / WSS / MQTT over TLS (TLS 1.3 필수, 5.8절)
   Data format : JSON records, GeoJSON features, GeoTIFF / NetCDF rasters
   Identity    : OAuth 2.0 access tokens (JWT), device credentials
   Network     : TCP/IP
그림 5-1. 프로토콜 스택과 종단 간 데이터 흐름. 왼쪽의 생산자는 MQTT나 HTTPS로 플랫폼에 도달하고, 오른쪽의 소비자는 끌어오기(REST, OGC) 또는 밀어 주기(웹소켓, 웹훅)로 데이터를 받는다.

그림 5-1은 왼쪽에서 오른쪽으로 한 번 따라가 볼 만하다. 센서 측정값은 MQTT 브로커로, 현장 조사 양식과 위성 산출물은 HTTPS로, 파트너 기관의 보관 자료는 분할 대량 업로드로 들어온다. 이들은 모두 제3장에서 설명한 동일한 검증 단계를 거친다. 따라서 REST로 들어왔다면 거부되었을 레코드(NDVI 1.5, 위도 95)는 MQTT로 오든 대량 파일 안에 섞여 오든 똑같이 거부된다. 통과한 레코드는 레코드 저장소에 들어가며 이벤트를 발생시키고, 이벤트 버스는 이를 웹훅과 웹소켓 스트림으로 퍼뜨린다. REST와 OGC 엔드포인트는 저장된 상태를 요청이 올 때마다 제공한다.

다섯 가지 원칙

PHASE 3 §1.2는 다섯 가지 원칙을 든다. 흔한 API 조언처럼 읽히지만, 이 분야에서는 원칙마다 구체적인 결과가 따른다.

원칙명세의 문구토지 모니터링 플랫폼에서의 의미
무상태 통신요청마다 처리에 필요한 정보를 모두 담는다어느 서버 복제본이든 응답할 수 있고, 다른 노드로 재접속한 현장 기기도 세션을 복구할 필요가 없다
멱등성같은 요청을 반복해도 결과가 같다불안정한 링크에서 재시도해도 측정값이 중복되면 안 된다. 쓰기는 레코드 id로, 이벤트는 eventId로 중복을 제거한다
버전 관리URL 경로에 API 버전을 넣는다구형 펌웨어가 설치된 게이트웨이는 계속 /v1을 호출하고, 새 클라이언트는 다음 버전으로 옮겨 간다
콘텐츠 협상여러 응답 형식을 지원한다같은 위치를 WIA JSON 레코드로도, 지도 도구용 GeoJSON 피처로도 돌려줄 수 있다
오류 복원력점진적 기능 저하와 재시도백오프, 회로 차단기, 저장 후 전달(store-and-forward)이 장애 중에도 원격 관측소를 쓸모 있게 유지한다(5.7절)

멱등성에는 주의가 필요하다. HTTP는 GET, PUT, DELETE를 멱등 메서드로 정의하지만 POST는 그렇지 않으며, 이 표준의 수집 요청은 대부분 POST다. 식생 레코드를 보낸 뒤 시간 초과를 겪은 클라이언트는 서버가 그것을 저장했는지 알 수 없다. 안전한 설계는 레코드 식별자를 멱등 키로 삼는 것이다. 서버는 VEG-20251225-SAHEL-001을 담은 두 번째 POST를 새 행이 아니라 갱신(upsert) 또는 무동작으로 처리한다. 제7장의 수집 파이프라인에서 이 문제를 다시 다룬다.

명세는 버전을 고르는 방법 두 가지를 제시한다. URL 경로(/v1/…, /v2/…)와 Accept 헤더의 벤더 미디어 타입(application/vnd.wia-ene-057.v1+json)이다. 둘 다 지원해도 되지만 주된 방식은 URL 경로여야 한다. 경로는 로그와 프록시, 캐시에서 그대로 보이지만, 미디어 타입 방식은 중간 장비가 헤더를 고쳐 쓰는 순간 쉽게 사라진다.

5.2 웹소켓 스트리밍

연결 수명 주기

대시보드와 운영 콘솔은 폴링 없이 갱신을 받아야 한다. 표준은 WSS 위의 /v1/stream 엔드포인트를 제공한다. 명세에 쓰인 호스트명(api.wia-ene-057.org 등)은 예시일 뿐이며 각 구현체가 자체 주소를 공개한다. 웹소켓 프로토콜 자체는 RFC 6455가 정의한다. 클라이언트가 Upgrade: websocket 헤더를 붙인 HTTP GET을 보내고, 서버가 101 Switching Protocols로 응답하면, 그 뒤로는 양쪽이 프레임을 주고받는다. WIA-ENE-057은 그 위에 작은 JSON 메시지 프로토콜을 얹는다.

 Client                                   Server (wss://.../v1/stream)
   |  HTTP GET /v1/stream  Upgrade: websocket   |
   |------------------------------------------->|
   |<-------------------------------------------|  101 Switching Protocols
   |  {"type":"auth","token":"..."}             |
   |-------------------------------------------> |  토큰·스코프 검증
   |  {"type":"subscribe", subscriptionId,      |
   |    locations, dataTypes, filters}          |
   |-------------------------------------------> |  구독 등록
   |                                            |
   |<-------------------------------------------|  {"type":"data", messageId, payload}
   |<-------------------------------------------|  {"type":"data", ...}
   |  {"type":"ping"}                           |
   |------------------------------------------->|
   |<-------------------------------------------|  {"type":"pong"}
   |                                            |
   x  네트워크 단절(예: 셀룰러 전환)            |
   |  백오프 지연 후 재연결                     |
   |-------------------------------------------> |  새 소켓
   |  auth  -> subscribe (same subscriptionId)  |
   |------------------------------------------->|
   |  GET /api/v1/monitoring/... ?since=<마지막 |
   |  수신 시각>   (REST로 공백 메우기)         |
   |------------------------------------------->|
그림 5-2. 웹소켓 시퀀스: 핸드셰이크, 인증, 구독, 데이터, 하트비트, 그리고 재연결 경로.

인증은 소켓 안에서 이루어진다. 첫 메시지는 액세스 토큰을 담은 {"type":"auth"}다. 브라우저의 웹소켓 API가 핸드셰이크에 Authorization 헤더를 붙일 수 없기 때문에 생긴 설계다. 여기에는 명세가 드러내 말하지 않은 결과가 따른다. 서버는 열려 있지만 인증되지 않은 소켓을 신뢰할 수 없는 것으로 취급해 아무것도 보내지 말아야 하며, 짧은 기한 안에 유효한 auth 메시지가 오지 않으면 연결을 닫아야 한다. RFC 6455는 이런 경우를 위해 종료 코드 1008(정책 위반)을 두고 있다. 토큰을 쿼리 문자열에 넣어서는 안 된다. 쿼리 문자열은 접근 로그에 그대로 남는다.

메시지 유형

{ "type": "auth", "version": "1.0.0", "token": "eyJhbGciOiJSUzI1NiIs..." }

{
  "type": "subscribe",
  "subscriptionId": "SUB-20251225-001",
  "locations": ["SAHEL-REGION-001"],
  "dataTypes": ["vegetation", "soil", "alerts"],
  "filters": { "minSeverity": "medium", "metrics": ["ndvi", "soilMoisture"] }
}

{
  "type": "data",
  "messageId": "msg-20251225-001",
  "timestamp": "2025-12-25T10:00:00Z",
  "subscriptionId": "SUB-20251225-001",
  "dataType": "vegetation",
  "locationId": "SAHEL-REGION-001",
  "payload": { "ndvi": 0.35, "evi": 0.28, "change": -0.02, "trend": "declining" }
}
유형방향주요 필드비고
auth클라이언트 → 서버version, token반드시 첫 메시지여야 한다
subscribe클라이언트 → 서버subscriptionId, locations, dataTypes, filters.minSeverity, filters.metrics토큰 권한 밖의 위치는 서버가 거부해야 한다
data서버 → 클라이언트messageId, subscriptionId, dataType, locationId, payloadPHASE 3가 정의한 범용 봉투
vegetation-update서버 → 클라이언트locationId, dataPHASE 2 §9에 나오는 유형별 변형
ping / pong양방향timestamp애플리케이션 수준 하트비트로, RFC 6455 제어 프레임을 보완한다

이 스트림을 설명하는 명세 문서가 둘인데, 세부 내용이 일치하지 않는다. PHASE 3는 locations 배열로 구독하고 payload를 담은 범용 data 봉투로 전달한다. PHASE 2 §9는 locationIds로 구독하고 data 객체를 담은 유형별 vegetation-update 메시지를 보여 준다. 구현체는 한 가지 형태를 정본으로 정해 프로파일 매니페스트에 선언하고, 입력은 너그럽게 받아야 한다. subscribe에서 locations와 locationIds를 모두 받아들이고, 클라이언트는 type으로 분기하되 모르는 유형은 무시한다. 이렇게 관대한 수신 측이 있어야 이미 배포된 대시보드를 깨뜨리지 않고 메시지 집합을 넓혀 갈 수 있다.

데이터 손실 없는 재연결

웹소켓은 살아 있는 파이프이지 큐가 아니다. 클라이언트가 끊겨 있는 동안 서버가 보낸 것은 사라진다. 따라서 견고한 클라이언트는 재연결할 때마다 세 가지를 한다. 무작위성을 섞어 지수적으로 늘어나는 지연을 기다린 뒤 다시 시도하고, auth와 subscribe를 다시 보내며(서버 로그가 이어지도록 같은 subscriptionId를 재사용한다), 마지막으로 처리한 시각 이후의 레코드를 REST API로 요청해 공백을 메운다. 공백 메우기와 실시간 스트림은 겹칠 수 있으므로 클라이언트는 messageId 또는 레코드 id로 중복을 제거한다.

// Reconnecting stream client (sketch). Values such as the URL are illustrative.
type Msg = { type: string; messageId?: string; timestamp?: string; [k: string]: unknown };

export function openStream(url: string, getToken: () => Promise<string>,
                           onData: (m: Msg) => void) {
  let attempt = 0;
  let lastSeen: string | undefined;            // newest timestamp processed
  const seen = new Set<string>();              // recent messageIds (bounded in practice)

  const connect = async () => {
    const ws = new WebSocket(url);
    let heartbeat: ReturnType<typeof setInterval> | undefined;

    ws.onopen = async () => {
      ws.send(JSON.stringify({ type: "auth", version: "1.0.0", token: await getToken() }));
      ws.send(JSON.stringify({
        type: "subscribe", subscriptionId: "SUB-20251225-001",
        locations: ["SAHEL-REGION-001"], dataTypes: ["vegetation", "soil", "alerts"],
      }));
      attempt = 0;
      heartbeat = setInterval(() =>
        ws.send(JSON.stringify({ type: "ping", timestamp: new Date().toISOString() })), 30_000);
      if (lastSeen) void backfillSince(lastSeen);  // REST gap fill, see text
    };

    ws.onmessage = (ev) => {
      const m: Msg = JSON.parse(String(ev.data));
      if (m.type === "pong") return;
      if (m.messageId && seen.has(m.messageId)) return;   // duplicate after reconnect
      if (m.messageId) seen.add(m.messageId);
      if (m.timestamp && (!lastSeen || m.timestamp > lastSeen)) lastSeen = m.timestamp;
      onData(m);
    };

    ws.onclose = () => {
      clearInterval(heartbeat);
      const delay = Math.min(60_000, 1_000 * 2 ** attempt++) * (0.5 + Math.random() / 2);
      setTimeout(connect, delay);
    };
  };
  void connect();
}

declare function backfillSince(isoTimestamp: string): Promise<void>;

이 스케치는 상태(마지막 수신 시각, 최근 메시지 ID)를 메모리에 둔다. 대시보드라면 그래도 된다. 그러나 스트림 데이터를 다른 시스템으로 넘기는 서비스는 두 값을 모두 영속 저장해야 한다. 그렇지 않으면 프로세스가 재시작될 때 공백이 다시 생긴다.

5.3 MQTT 센서 데이터 수집

현장 기기에 MQTT를 쓰는 이유

토양 수분 센서, 우량계, 기상 관측 마스트는 작은 메시지를 자주 보내고 대부분의 시간을 절전 상태로 보낸다. 측정값 하나마다 TLS 핸드셰이크를 새로 하는 HTTPS는 이런 기기에 부담이 크다. MQTT는 OASIS가 표준화한 발행·구독 프로토콜(3.1.1판은 ISO/IEC 20922로도 발행되었고, 5.0판이 있다)로, 브로커와 오래 유지되는 연결 하나를 쓰고 프레임이 간결하며 구독자에게 퍼뜨리는 일은 브로커가 맡는다. 명세는 3.1.1과 5.0을 모두 나열한다. 5.0판에는 메시지 만료, 확인 응답의 사유 코드처럼 현장에서 쓸모 있는 기능이 추가되었다.

토픽 구조

토픽은 wia-ene-057/{locationId}/{sensorType}/{metric} 패턴을 따른다. 위치를 맨 앞에 두었기 때문에 구독자는 와일드카드 하나로 한 현장의 모든 데이터를 받을 수 있고, 접근 권한도 위치 단위의 하위 트리로 부여할 수 있다.

토픽 또는 필터의미일반적인 QoS
wia-ene-057/SAHEL-REGION-001/soil/moisture한 현장의 토양 수분 측정값1
wia-ene-057/SAHEL-REGION-001/weather/rainfall우량계 누적값1
wia-ene-057/SAHEL-REGION-001/vegetation/ndvi지상 관측 또는 파생 NDVI0 또는 1
wia-ene-057/SAHEL-REGION-001/#구독 필터: 해당 현장의 모든 지표(# = 다단계 와일드카드)구독자가 선택
wia-ene-057/+/soil/moisture구독 필터: 모든 현장의 토양 수분(+ = 단일 단계)구독자가 선택

브로커는 기기가 자기 위치 아래에만 발행하도록 강제해야 한다. SENSOR-SOIL-001의 자격 증명으로 wia-ene-057/#에 쓸 수 있다면, 그 기기 하나로 네트워크 전체 현장의 측정값을 위조할 수 있다.

기기 메시지

Topic:  wia-ene-057/SAHEL-REGION-001/soil/moisture      (QoS 1)

{
  "deviceId": "SENSOR-SOIL-001",
  "locationId": "SAHEL-REGION-001",
  "timestamp": "2025-12-25T10:00:00Z",
  "sensorType": "soil-moisture",
  "readings": { "moisture": 12.5, "temperature": 28.5, "depth": "0-10cm" },
  "metadata": { "battery": 85, "signalStrength": -65, "firmware": "1.2.3" }
}

페이로드는 deviceId, locationId, timestamp, readings, 그리고 기기 metadata(배터리 잔량 백분율, dBm 단위 신호 세기, 펌웨어 버전)를 담는다. 메타데이터는 장식이 아니다. 배터리가 일정 수준 아래로 떨어지면서 토양 수분 계열이 함께 내려간다면 토양이 마른 것이 아니라 센서가 고장 나고 있을 가능성이 크다. 보정 버그가 있는 펌웨어가 만든 측정값을 모두 찾아낼 수 있는 방법도 펌웨어 버전뿐이다. 수집 서비스는 메타데이터를 떼어 버리지 말고 측정값과 함께 보관해야 한다. 또 locationId가 토픽과 본문에 모두 나타나므로, 두 값이 다른 메시지는 수집 서비스가 거부해야 한다.

QoS 선택

MQTT는 세 가지 전달 수준을 제공하며, 명세는 이를 메시지 부류에 대응시킨다. QoS 0(최대 한 번)은 일상적인 원격 측정에, QoS 1(최소 한 번)은 중요한 측정값에, QoS 2(정확히 한 번)는 중대 경보에 쓴다. 이 수준들을 쓰는 방식은 MQTT의 두 가지 성질이 결정한다. 첫째, QoS는 종단 간이 아니라 구간마다(기기→브로커, 브로커→구독자) 따로 적용된다. 둘째, QoS 1은 설계상 중복 전달이 일어날 수 있다. 그래서 실무 규칙은 다음과 같다. 레코드로 이어지는 측정값에는 QoS 1을 쓰고, 소비 측은 deviceId와 timestamp의 조합으로 멱등하게 만들며, QoS 2는 추가 왕복에 드는 배터리 비용을 감수할 만한 드문 메시지에만 쓴다. MQTT 계층의 "정확히 한 번"이 애플리케이션의 중복 제거를 대신해 주지는 않는다.

엣지에서의 버퍼링

원격 게이트웨이는 몇 시간에서 며칠씩 연결이 끊긴다. 게이트웨이는 모든 측정값을 먼저 로컬 저장소에 기록하고 그 버퍼에서 발행해야 하며, 브로커가 확인 응답을 보낸 항목만 지워야 한다. 링크가 돌아오면 밀린 데이터를 시각 순서대로 속도를 제한해 발행하고, 오래 끊겼던 경우에는 대량 업로드(5.7절)로 묶어 보낸다. 측정값은 원래의 측정 시각을 유지해야 하며, 플랫폼이 도착 시각을 대신 찍어서는 절대 안 된다. 뒤에 나오는 보안 절에서 시각 동기화를 다루는 이유도 여기에 있다.

5.4 위성 래스터 수집

위성 산출물은 레코드가 아니라 래스터로 들어온다. 명세는 수집 요청 하나를 정의한다. /api/v1/ingest/satellite로 보내는 multipart/form-data 형식의 HTTPS POST로, metadata라는 이름의 JSON 파트와 GeoTIFF를 담은 파일 파트로 구성된다. 대체 컨테이너로는 NetCDF가 명시되어 있으며, 다차원 기후 자료나 토양 수분 산출물에 적합하다.

POST /api/v1/ingest/satellite HTTP/1.1
Authorization: Bearer <access-token>
Content-Type: multipart/form-data; boundary=wia-boundary

--wia-boundary
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{
  "locationId": "SAHEL-REGION-001",
  "satellite": "MODIS",
  "instrument": "MODIS-Terra",
  "product": "MOD13Q1",
  "acquisitionDate": "2025-12-25T10:30:00Z",
  "resolution": "250m",
  "bands": ["NDVI", "EVI"],
  "cloudCover": 5.2,
  "crs": "EPSG:4326"
}
--wia-boundary
Content-Disposition: form-data; name="file"; filename="SAHEL-001-20251225.tif"
Content-Type: image/tiff

<binary GeoTIFF bytes>
--wia-boundary--

메타데이터 필드는 래스터를 데이터 모델의 나머지 부분과 연결한다. locationId는 등록된 현장에 래스터를 묶고, satellite, instrument, product는 출처를 밝히며, bands는 들어 있는 레이어를 나열하고, cloudCover는 품질 필터링에 쓰이며, crs는 파일의 좌표 참조계를 밝힌다. 다음 세 가지는 틀리기 쉽다.

큰 래스터를 약한 링크에서 요청 하나로 밀어 넣어서는 안 된다. 파일을 객체 저장소에 먼저 올려 두고 참조만 제출하거나, 5.7절의 분할 대량 업로드를 청크별 체크섬과 함께 쓴다.

5.5 OGC와 GeoJSON 상호운용

세 가지 OGC 서비스

QGIS나 ArcGIS 같은 GIS 클라이언트는 개방형 지리공간 컨소시엄(OGC)의 서비스 표준을 기본으로 지원한다. 명세는 그중 세 가지를 제공한다. 렌더링된 지도 이미지를 위한 WMS 1.3.0(예: desertification-risk 레이어), 벡터 피처(속성이 붙은 모니터링 지점과 폴리곤)를 위한 WFS 2.0.0, 그리고 커버리지, 즉 래스터 값 자체(ndvi-coverage)를 위한 WCS 2.0.1이다. 모든 서비스는 GetCapabilities에도 응답하며, 클라이언트는 이를 먼저 호출해 사용할 수 있는 레이어와 좌표계, 형식을 알아낸다.

# WMS 1.3.0, CRS EPSG:4326: BBOX is minLat,minLon,maxLat,maxLon
GET /wms?service=WMS&version=1.3.0&request=GetMap&layers=desertification-risk
        &styles=&crs=EPSG:4326&bbox=14.0,-5.0,15.0,-3.0
        &width=800&height=600&format=image/png

# The same window with CRS:84 (longitude first): minLon,minLat,maxLon,maxLat
GET /wms?service=WMS&version=1.3.0&request=GetMap&layers=desertification-risk
        &styles=&crs=CRS:84&bbox=-5.0,14.0,-3.0,15.0
        &width=800&height=600&format=image/png

# WFS 2.0.0 GetFeature (cql_filter is a server vendor parameter, see text)
GET /wfs?service=WFS&version=2.0.0&request=GetFeature
        &typeNames=wia-ene-057:vegetation-monitoring
        &outputFormat=application/json
        &cql_filter=locationId='SAHEL-REGION-001'

# WCS 2.0.1 GetCoverage, trimming by named axes
GET /wcs?service=WCS&version=2.0.1&request=GetCoverage&coverageId=ndvi-coverage
        &subset=Lat(14.0,15.0)&subset=Long(-5.0,-3.0)&format=image/tiff

이 요청들에 대해 세 가지를 덧붙인다. 명세는 WFS 매개변수를 WFS 1.x 표기인 typeName으로 적었지만, WFS 2.0.0에서는 위 예시처럼 typeNames가 정식 이름이다. 관대한 서버는 둘 다 받아들인다. WFS 예시의 cql_filter는 WFS 2.0 표준의 일부가 아니라 일부 서버(GeoServer 등)가 지원하는 벤더 매개변수다. 표준 방식은 OGC 필터 인코딩의 FILTER 매개변수다. cql_filter에 의존하는 구현체는 이를 프로파일에 밝혀야 한다. 마지막으로 WCS 요청은 Lat, Long이라는 축 이름으로 범위를 자르는데, 이 이름은 커버리지마다 설명 문서에서 정의되므로 클라이언트는 하드코딩하지 말고 DescribeCoverage에서 읽어 와야 한다.

GeoJSON

웹 클라이언트에는 위치를 GeoJSON(RFC 7946)으로 돌려준다. 피처는 지오메트리와 자유 형식의 properties로 이루어지며, WIA 필드는 properties에 들어간다. RFC 7946은 좌표 순서를 경도, 위도로, 좌표계를 WGS 84로 고정했고 예전의 crs 멤버를 없앴다. 따라서 명세 예시에 있는 crs 블록은 필요 없으며, 엄격한 파서 중에는 이를 무시하거나 거부하는 것도 있다. 폴리곤 링은 닫혀 있어야 하고(첫 좌표를 마지막에 반복), 외곽 링은 반시계 방향으로 감는 것이 권고된다.

{
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "id": "SAHEL-REGION-001",
    "geometry": {
      "type": "Polygon",
      "coordinates": [[
        [-4.0, 14.0], [-3.0, 14.0], [-3.0, 15.0], [-4.0, 15.0], [-4.0, 14.0]
      ]]
    },
    "properties": {
      "locationId": "SAHEL-REGION-001",
      "areaUnit": "hectares",
      "ndvi": 0.35,
      "lastUpdate": "2025-12-25T10:00:00Z"
    }
  }]
}

축 순서의 함정

이 장 안에서만도 같은 장소가 세 가지 방식으로 표기되며, 이를 섞는 것이 지리공간 시스템에서 가장 흔한 상호운용 버그다. 명세의 WMS 1.3.0 요청은 crs=EPSG:4326과 bbox=14.0,-5.0,15.0,-3.0을 쓴다. 이것은 옳다. WMS 1.3.0에서 EPSG:4326은 좌표계 정의의 축 순서를 따르며, 그 순서는 위도가 먼저다. 그런데 같은 요청을 WMS 1.1.1로 보내거나 CRS:84를 쓰면 경도가 먼저다. 위도 우선 상자를 경도 우선 서비스에 보낸 클라이언트는 아무 오류 없이 엉뚱한 지역의 지도나 빈 지도를 받는다.

맥락순서SAHEL-REGION-001 지점(위도 14.5, 경도 −4.0)
WIA JSON coordinates 객체이름 있는 필드{"latitude": 14.5, "longitude": -4.0}
GeoJSON(RFC 7946)경도, 위도[-4.0, 14.5]
WMS 1.3.0, CRS=EPSG:4326위도, 경도bbox minLat,minLon,maxLat,maxLon
WMS 1.3.0, CRS=CRS:84; WMS 1.1.1 SRS=EPSG:4326경도, 위도bbox minLon,minLat,maxLon,maxLat
WCS 2.0.1 subset축 이름으로 지정subset=Lat(…)&subset=Long(…)
WKT, 다수의 GIS 라이브러리대개 x, y = 경도, 위도POINT(-4.0 14.5)

모든 CI 파이프라인에 넣어 둘 만한 간단한 점검이 있다. 예시 지점을 각 출력 경로로 변환해 본 뒤, 그 결과가 사헬 띠(대략 북위 10~20도) 안에 떨어지는지 확인하는 것이다. 위도와 경도가 뒤바뀌면(경도 14.5, 위도 −4.0) 이 점은 적도 남쪽의 중앙아프리카로 옮겨 간다. WIA 레코드 형식의 이름 있는 필드가 가장 안전한 표현이며, PHASE 1이 이를 쓰는 이유 중 하나도 여기에 있다.

5.6 이벤트, 웹훅, 서명 검증

이벤트 유형과 봉투

플랫폼 내부의 변경은 이벤트로 발행된다. PHASE 3 §6.1은 다섯 가지 유형을 정의한다. data.created, data.updated, data.deleted, alert.triggered, assessment.completed이다. 모든 이벤트는 하나의 봉투 형식을 쓴다.

{
  "eventId": "evt-20251225-001",
  "eventType": "data.created",
  "timestamp": "2025-12-25T10:00:00Z",
  "source": "wia-ene-057-monitoring",
  "version": "1.0.0",
  "data": {
    "resourceType": "vegetation-monitoring",
    "resourceId": "VEG-20251225-SAHEL-001",
    "locationId": "SAHEL-REGION-001",
    "action": "created",
    "payload": { "ndvi": 0.35, "evi": 0.28, "timestamp": "2025-12-25T10:00:00Z" }
  },
  "metadata": { "organizationId": "org-12345", "correlationId": "corr-abc123" }
}

eventId는 중복 제거의 단위다. 전달은 반복될 수 있어도 이벤트는 반복되지 않는다. correlationId는 상류의 한 동작이 일으킨 모든 일을 하나로 묶는다. 그래서 현장 업로드 한 건이 검증, 레코드 생성, 추세 재계산, 그리고 그로 인해 발생한 경보까지 어떻게 이어졌는지 추적할 수 있다. 명세 예시는 짧은 리소스 식별자를 쓰지만, 위 예시는 이벤트를 저장된 레코드와 곧바로 연결할 수 있도록 PHASE 1 식별자 패턴(VEG-20251225-SAHEL-001)을 썼다. 페이로드는 레코드 전체가 아니라 요약이다. 전체 레코드가 필요한 소비자는 resourceId로 가져온다.

웹훅 등록과 전달

파트너는 대상 URL, 이벤트 유형 목록, 선택적 필터(위치 ID, 심각도), 공유 비밀값을 지정해 웹훅을 등록한다. 그러면 플랫폼은 조건에 맞는 이벤트를 다음 세 헤더와 함께 POST로 보낸다.

헤더예용도
X-WIA-Signaturesha256=<hex>공유 비밀값으로 계산한 원본 요청 본문의 HMAC-SHA256
X-WIA-Eventalert.triggered본문을 파싱하기 전에 라우팅
X-WIA-Deliverydelivery-20251225-001이번 전달 시도를 식별해 지원 문의와 로그에 활용

서명을 올바르게 검증하기

명세의 검증 코드는 방향은 옳지만(HMAC-SHA256과 상수 시간 비교) 실무상 결함이 두 가지 있다. Node의 crypto.timingSafeEqual은 두 버퍼의 길이가 다르면 예외를 던지므로, 형식이 틀리거나 잘린 헤더가 오면 핸들러가 401을 돌려주는 대신 죽어 버린다. 또 코드는 payload가 무엇인지 밝히지 않는다. HMAC은 수신한 바이트 그대로를 대상으로 계산해야 한다. JSON을 파싱해 객체로 넘겨주는 프레임워크를 쓰면 그것을 다시 직렬화하고 싶어지는데, 공백이나 키 순서, 숫자 표기가 조금만 달라도 다이제스트가 달라진다. 아래 수신기는 원본 본문을 유지하고, 길이를 먼저 확인하고, eventId로 중복을 제거하며, 실제 작업 전에 빠르게 응답한다.

// Express receiver for WIA-ENE-057 webhooks (Node.js 18+).
const crypto = require('crypto');
const express = require('express');

const app = express();
const SECRET = process.env.WIA_WEBHOOK_SECRET;     // shared secret set at registration
const processed = new Set();                       // use a persistent store in production

function verifySignature(rawBody, header, secret) {
  if (typeof header !== 'string' || !header.startsWith('sha256=')) return false;
  const expected = Buffer.from(
    'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'), 'utf8');
  const received = Buffer.from(header, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  if (received.length !== expected.length) return false;
  return crypto.timingSafeEqual(received, expected);
}

// express.raw keeps the exact bytes; re-serialising parsed JSON would change them.
app.post('/webhooks/wia-ene-057', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifySignature(req.body, req.get('X-WIA-Signature'), SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString('utf8'));
  if (processed.has(event.eventId)) return res.status(200).end();   // duplicate delivery
  processed.add(event.eventId);

  res.status(202).end();            // acknowledge fast, work asynchronously
  setImmediate(() => handleEvent(req.get('X-WIA-Event'), event));
});

function handleEvent(type, event) { /* enqueue for processing */ }

app.listen(8080);

몇 가지 운영 규칙이 더 필요하다. 수신기는 몇 초 안에 응답하고 무거운 처리는 큐로 넘겨야 한다. 발신 측은 느린 응답을 실패로 보고 재전송하기 때문이다. 중복 이벤트에 2xx를 돌려주는 것은 옳다. 그 이벤트는 이미 처리되었다. 비밀값은 정해진 주기로 교체하며, 교체 기간에는 이전 비밀값과 새 비밀값 중 어느 쪽으로 만든 서명이든 받아들인다. 끝으로, 서명은 본문만 보호한다. 유효한 전달을 가로챈 공격자가 그것을 다시 보내는 재전송 공격은 막지 못한다. eventId 기반 중복 제거가 재전송의 효과를 줄여 주며, 구현체는 한 걸음 더 나아가 서명 대상에 타임스탬프를 넣는 확장을 도입하고 오래된 전달을 거부할 수 있다.

5.7 대량 업로드, 캐싱, 복원력

분할 대량 업로드

과거 보관 자료, 한 철 동안의 현장 조사, 일주일 동안 오프라인이었던 게이트웨이의 밀린 데이터는 단일 요청으로 보내기에 너무 크다. PHASE 3 §7은 시작, 청크 업로드, 완료, 상태 조회의 네 단계 흐름을 정의한다.

POST /api/v1/bulk/upload/initiate
{ "dataType": "vegetation-monitoring", "recordCount": 10000,
  "estimatedSize": 52428800, "format": "json" }

-> { "uploadId": "upload-20251225-001", "chunkSize": 5242880,
     "expiresAt": "2025-12-26T10:00:00Z",
     "uploadUrl": "/api/v1/bulk/upload/upload-20251225-001/chunk" }

POST /api/v1/bulk/upload/upload-20251225-001/chunk     (repeat for 1..10)
X-Chunk-Number: 1
X-Chunk-Total: 10
<5 MiB of bytes>

POST /api/v1/bulk/upload/upload-20251225-001/complete
{ "uploadId": "upload-20251225-001", "chunksReceived": 10,
  "checksums": ["<sha256 of chunk 1>", "<sha256 of chunk 2>", "..."] }

GET /api/v1/bulk/upload/upload-20251225-001/status
-> { "status": "processing",
     "progress": { "totalRecords": 10000, "processed": 7500, "successful": 7450,
                   "failed": 50, "percentage": 75.0 },
     "errors": [ { "recordNumber": 245, "error": "Invalid NDVI value: 1.5" } ] }

명세 예시의 수치는 서로 들어맞으며 동작 원리를 잘 보여 준다. 예상 크기 52,428,800바이트(50 MiB)를 서버가 정한 chunkSize 5,242,880바이트(5 MiB)로 나누면 청크 열 개가 되고, 각 청크에는 X-Chunk-Number와 X-Chunk-Total로 번호를 매긴다. complete 호출은 청크마다 SHA-256 다이제스트를 나열하므로, 서버는 손상되었거나 빠진 청크를 찾아내 파일 전체가 아니라 그 청크만 다시 요청할 수 있다. 50 MiB를 보내는 데 여러 번 시도해야 할 수도 있는 위성 링크에서는 이 성질이 중요하다.

상태 응답은 바이트가 아니라 레코드 단위로 진행률을 알려 준다. 레코드 10,000건 중 7,500건 처리, 7,450건 승인, 50건 거부이며, 오류 목록에는 245번 레코드의 "Invalid NDVI value: 1.5" 같은 항목이 담긴다. PHASE 1의 검증 규칙이 대량 경로에서도 똑같이 적용된 것이다. 부분 실패는 정상적인 일이다. 클라이언트는 오류 목록을 내려받아 해당 레코드를 고치거나 버린 뒤, 배치 전체를 되풀이하지 말고 더 작은 새 업로드로 다시 제출한다. 업로드에는 만료 시각(expiresAt)도 있으므로, 그보다 오래 연결이 끊긴 클라이언트는 처음부터 다시 시작해야 한다.

HTTP 캐싱

모니터링 데이터의 상당 부분은 천천히 바뀐다. 식생 레코드는 16일마다 교체되고, 평가는 새 데이터가 들어올 때 갱신된다. HTTP 캐싱(RFC 9111)은 대역폭이 가장 부족한 바로 그 링크에서 대역폭을 아껴 준다. 명세는 표준 도구를 그대로 쓴다.

매일 아침 현장 목록을 다시 여는 현장용 태블릿이 바뀌지 않은 위치에 대해 304를 받는다면 비용은 거의 들지 않는다. 다만 캐싱은 인가를 존중해야 한다. 호출자의 토큰에 따라 달라지는 응답은 절대 public이 될 수 없다.

재시도, 백오프, 회로 차단기

명세의 재시도 규칙은 지수 백오프로, 지연 = min(최대 지연, 기본 지연 · 2n)이다. 기본 지연이 1초라면 다섯 번의 시도에 1, 2, 4, 8, 16초가 된다. 여기에 두 가지 보완이 표준 관행이다. 첫째, 무작위 지터를 섞는다. 같은 기지국을 잃었던 게이트웨이 백 대가 기지국이 복구되는 순간 한꺼번에 재시도하지 않도록 하기 위해서다. 둘째, 서버의 지시를 따른다. Retry-After(RFC 9110에 따라 초 단위 숫자 또는 HTTP 날짜)가 붙은 429 Too Many Requests나 503 Service Unavailable은 클라이언트 자체 일정보다 우선한다.

회로 차단기는 의존 서비스가 멈췄을 때 양쪽을 모두 보호한다. 닫힘 상태에서는 요청이 정상적으로 흐른다. 감시 기간 안에 실패가 임계값에 이르면 차단기가 열림 상태가 되어 네트워크에 닿지도 않고 즉시 실패를 돌려준다. 재설정 시간이 지나면 반열림 상태가 되어 시험 요청 하나를 허용하고, 성공하면 다시 닫힌다. 명세의 설정값은 failureThreshold 5, timeout 30,000 ms(요청당), resetTimeout 60,000 ms, monitoringPeriod 10,000 ms다.

config: base = 1 s, maxDelay = 60 s, maxAttempts = 5
        failureThreshold = 5, timeout = 30 s, resetTimeout = 60 s, monitoringPeriod = 10 s

function call(request):
    if breaker.state == OPEN:
        if now - breaker.openedAt < resetTimeout: fail fast ("circuit open")
        breaker.state = HALF_OPEN                  # allow one probe
    for attempt in 0 .. maxAttempts-1:
        response = send(request, timeout)
        if response is success:
            breaker.recordSuccess()                # HALF_OPEN -> CLOSED
            return response
        if not retryable(response, request):       # 4xx other than 408/429, or
            return response                        # non-idempotent without a key
        breaker.recordFailure()                    # >= failureThreshold within
        if breaker.state == OPEN: fail fast        # monitoringPeriod -> OPEN
        if response has Retry-After:
            wait(parse(Retry-After))               # seconds or HTTP-date
        else:
            wait(random(0.5, 1.0) * min(maxDelay, base * 2^attempt))
    fail ("retries exhausted")

재시도해도 안전한 것만 재시도해야 한다. GET, PUT, DELETE는 반복해도 된다. POST는 서버가 반복을 알아볼 수 있을 때만 재시도해야 하며, 이 표준에서는 레코드가 자기 id를 갖고 있거나 업로드가 uploadId를 갖고 있을 때가 그렇다. 408과 429를 제외한 클라이언트 오류는 아예 재시도하지 않는다. 잘못된 레코드를 다섯 번 보내 봐야 거부가 다섯 번 돌아올 뿐이다.

폐기 신호

어떤 버전을 퇴역시킬 때 서버는 응답 안에서 이를 알린다. 퇴역 날짜를 담은 Sunset 헤더(RFC 8594), Deprecation 헤더, 그리고 대체 경로를 가리키는 rel="alternate" Link 헤더다. 명세 예시는 Deprecation: true라고 쓰는데, 이는 초기 초안에서 쓰던 형태다. 발행된 RFC 9745는 값을 날짜로 정의하므로 새 구현체는 그 형태로 내보내고, 입력에서는 옛 형태도 받아들이는 것이 좋다. 원격 관측소의 게이트웨이는 API 버전이 꺼진 뒤에도 사람 손길 없이 계속 돌아갈 수 있으므로, 클라이언트는 이 헤더를 기록하고 운영자에게 알려야 한다. 병행 기간을 포함한 버전 관리의 거버넌스 측면은 제8장에서 다룬다.

5.8 프로토콜 계층의 보안

전송 암호화

두 명세 문서가 정한 하한이 서로 다르다. PHASE 3 §5.1은 강한 암호군과 완전 순방향 비밀성(ECDHE 또는 DHE 키 교환, AES-256-GCM 또는 ChaCha20-Poly1305)을 갖춘 TLS 1.2 이상을 요구한다. API-INTERFACE 문서 §7은 전송 구간을 TLS 1.3(RFC 8446)으로 암호화해야 한다(MUST)고 적는다. 더 엄격한 쪽을 요구사항으로 삼아야 한다. 즉 WSS와 TLS 위의 MQTT를 포함한 모든 공개 엔드포인트에 TLS 1.3을 적용한다. TLS 1.3은 구조적으로 순방향 비밀성을 제공하고 약한 암호 선택지를 없앴으므로, PHASE 3의 암호군 목록은 구형 기기 때문에 TLS 1.2를 계속 받아야 하는 경우에만 의미가 있다. 그런 예외가 필요하다면 조용한 기본값으로 두지 말고, 사유를 밝힌 부분 적합(partial)으로 프로파일 매니페스트에 기재해야 한다.

OAuth 2.0과 JWT 액세스 토큰

제4장은 기계 클라이언트가 클라이언트 자격 증명 그랜트로 토큰을 받는 방법을 보여 주었다. 복원 사업 관리자가 웹 대시보드에 로그인하는 경우처럼 사람을 대신해 동작하는 애플리케이션을 위해, PHASE 3 §5.2는 인가 코드 흐름을 설명한다. 요청할 스코프와 state 값을 붙여 /oauth/authorize로 리디렉션하고, 등록된 콜백에서 코드를 받아 /oauth/token에서 교환하면, 액세스 토큰(예시에서는 유효 시간 3,600초)과 리프레시 토큰을 받는다. 명세의 흐름에는 PKCE(RFC 7636)가 빠져 있다. 현행 OAuth 보안 지침인 RFC 9700(OAuth 2.0 보안 모범 사례, BCP)은 코드 흐름을 쓰는 모든 클라이언트에 PKCE를 권고하므로, WIA 배치는 이를 필수로 두어야 한다.

액세스 토큰은 RS256으로 서명된 JWT(RFC 7519)다. 리소스 서버는 인가 서버를 호출하지 않고도 토큰을 검증할 수 있다. 공개된 키 집합에서 kid가 가리키는 공개 키를 찾아 서명을 검증한 다음, 클레임을 확인한다.

Header : { "alg": "RS256", "typ": "JWT", "kid": "wia-ene-057-key-1" }

Payload: {
  "iss": "https://auth.example.org",
  "sub": "user-12345",
  "aud": "https://api.example.org",
  "iat": 1735124400,
  "exp": 1735128000,
  "scope": "monitoring:read restoration:write",
  "organization": "org-12345",
  "role": "project-manager"
}
클레임리소스 서버가 수행하는 확인
iss설정된 발급자와 정확히 일치하는지
aud이 API를 포함하는지. 다른 서비스용으로 발급된 토큰은 거부한다
exp, iat만료되지 않았고 미래에 발급된 것이 아닌지. 작은 시각 오차 허용치를 둔다
sub감사 로그에 남길 사용자 또는 클라이언트 식별
scope엔드포인트가 요구하는 스코프를 포함하는지. 예: 진행 상황 갱신에는 restoration:write
organization, role테넌트 격리와 역할 기반 접근 제어를 위한 사용자 정의 클레임. 호출자가 어느 위치를 읽고 쓸 수 있는지 결정한다

알고리즘은 고정해야 한다. 토큰 헤더가 지정하는 alg를 무엇이든 받아들이는 검증기는 서명이 없거나 잘못 서명된 토큰에 속아 넘어갈 수 있다. 웹소켓의 auth 메시지에도 같은 토큰 검증이 적용된다. 오래 유지되는 스트림 도중 토큰이 만료되면 서버는 소켓을 닫고, 클라이언트가 새 토큰으로 재연결하게 해야 한다.

시각, 저장, 감사

API-INTERFACE 문서는 세 가지 요구사항을 더 둔다. 첫째, 시각은 계층(stratum) 2 이상의 NTPv4(RFC 5905)로 동기화해야 한다. 그래야 토큰 만료 확인이 의미를 갖고, 여러 서버에 걸친 이벤트 순서가 일관되게 유지된다. 믿을 만한 네트워크 시각원이 없는 원격 게이트웨이에서는 GPS 수신기가 정확한 기준을 제공하며, 이런 용도로 흔히 쓰인다. 둘째, 저장 데이터는 AES-256-GCM 이상으로 암호화해야 한다. 셋째, 플랫폼은 투명성 로그 방식의 머클 트리 구조로 변조 탐지가 가능한 감사 로그를 유지해야 한다. 그래야 누가 어떤 레코드를 바꾸었는지의 이력을 몰래 고쳐 쓸 수 없다. 같은 문서는 배치마다 개인정보에 적용되는 규제 체계(GDPR, 한국 개인정보 보호법 등)를 선언하도록 요구한다. 이 분야에서는 주로 현장 조사원이 대상이다. 조사원의 이름과 전화번호, GPS 이동 경로가 조사 제출물과 함께 오가기 때문이다.

영역요구사항 또는 권고근거
전송TLS 1.3 (PHASE 3는 1.2를 허용하므로, 쓴다면 선언된 예외로 처리)API-INTERFACE §7, PHASE 3 §5.1
사용자 인가state를 포함한 인가 코드 흐름, PKCE 추가PHASE 3 §5.2, RFC 7636, RFC 9700
토큰JWT, RS256, kid. iss·aud·exp·scope 검증PHASE 3 §5.3
웹훅원본 본문에 대한 HMAC-SHA256, 상수 시간 비교, eventId 중복 제거PHASE 3 §6.2
시각NTPv4, 계층 2 이상API-INTERFACE §5
저장저장 시 AES-256-GCM, 변조 탐지 가능한 감사 로그API-INTERFACE §7

PHASE 3 끝부분의 준수 목록에 대해 한마디 덧붙인다. 이 목록은 HTTP/1.1(RFC 7230–7235), HTTP/2(RFC 7540), OAuth 2.0(RFC 6749) 등에 체크 표시를 달아 두었다. 이는 스스로 선언한 정합일 뿐 인증이 아니며, 인용된 문서 가운데 몇몇은 이미 대체되었다. HTTP 의미론과 캐싱은 현재 RFC 9110과 RFC 9111, HTTP/1.1 메시지 형식은 RFC 9112, HTTP/2는 RFC 9113이다. 구현체는 자체 적합성 선언에서 현행 문서를 인용해야 한다.

정리

PHASE 3는 데이터 모델과 API를 실제로 동작하는 네트워크로 바꾼다. 앞으로 가져갈 요점은 다음과 같다. 모든 채널은 TLS, OAuth로 발급된 자격 증명, PHASE 1 검증을 공유한다. 웹소켓 스트림에는 소켓 내 인증, 관대한 파싱, 재연결 시 REST 공백 메우기가 필요하다. MQTT 토픽은 위치 기준으로 구성하고, QoS 1과 애플리케이션 수준 중복 제거를 기본으로 삼는다. 래스터 메타데이터는 합성 기간, 투영, 배율을 정직하게 기술해야 한다. 축 순서는 가정하지 말고 시험해야 한다. 웹훅 서명은 원본 바이트를 대상으로, 상수 시간 비교 전에 길이를 먼저 확인해 검증한다. 재시도는 횟수를 제한하고 지터를 섞으며, 서버가 반복임을 알아볼 수 있는 작업에만 적용한다. 제6장은 이 프로토콜들을 바깥으로 넓혀, WIA-ENE-057 플랫폼이 연결되는 지구관측 자료 보관소, 토지 황폐화 보고 절차, GIS 플랫폼, 탄소 등록부를 다룬다.