·

[JSON] (6) JSON 스키마를 운영에 적용하기: 호스팅·번들링·재사용

JSON 스키마를 실제 운영 환경에서 어떻게 관리하는지 설명합니다. `$id` 기반 URL 설계, HTTP 호스팅, 버전 관리, 불변성 전략, `$ref` 재사용, 번들링 구조까지 실무 흐름으로 정리합니다.

밝은 베이지색 배경 위에 “JSON”과 “(6) JSON 스키마를 운영에 적용하기”라는 제목이 배치된 프레젠테이션 스타일 이미지. 오른쪽에는 파란색 보드 형태의 그래픽 안에 JSON 예시 코드가 표시되어 있으며, 이름, 나이, 학생 여부, 기술 스택(JavaScript, Python, SQL), 주소(city, country) 정보가 포함되어 있다. 보드 양옆에는 파란 중괄호 장식이 있으며, 좌측 상단에는 “made with DALL-E 3” 문구가 작게 적혀 있다. 하단 중앙에는 작은 로고 아이콘이 배치되어 있다.

만들어 둔 스키마가 어디에 있냐고 물으면 한참을 더듬는 경우가 많습니다. 누군가의 노트북 로컬 폴더에 있다는 답이 돌아오기도 하고, 슬랙에 한 번 올라간 첨부 파일이 전부이기도 합니다. 스키마 자체는 잘 짜여 있는데, 다른 팀이 가져다 쓰려면 매번 “그 파일 좀 다시 보내 주세요”라는 메시지를 보내야 합니다.

스키마를 만드는 것과 운영하는 것은 다른 문제입니다. 스키마가 어디에 있는지, 어떻게 가져오는지, 언제 갱신되는지가 정해져 있지 않으면 그 스키마는 자산이 되지 못합니다. 같은 이름의 스키마가 v1과 v2로 둘 다 돌아가고 있는데 어느 시스템이 어느 버전을 쓰는지 추적되지 않는 상태도 흔합니다.

6편에서는 스키마를 조직 안에서 자산으로 운영하는 방법을 다룹니다. HTTP로 스키마를 식별·접근하는 구조부터 시작해서, 호스팅 워크플로, 번들링을 설명해 보겠습니다.


1. HTTP로 스키마를 식별하기

스키마 운영의 토대는 식별자(identifier)입니다. JSON 스키마의 $id 키워드는 URI 표준(RFC 3986)을 따르는 식별자를 받습니다. 이 식별자를 HTTPS URL로 지정하면 이름 충돌 방지, 도구의 자동 참조, 경로 기반 버전 관리가 한꺼번에 해결됩니다.

1) $id URL 설계

<https://schemas.example.com/v2/user.json>
└─────────────┬────────────┘└──┘ └───┬───┘
            도메인             버전   리소스

$id의 URL은 실제 스키마를 호스팅할 위치와 일치시키는 것이 운영상 가장 깔끔합니다. 흔히 쓰이는 구조는 도메인/버전/리소스 패턴입니다.

{
  "$id": "<https://schemas.example.com/v2/user.json>",
  // 도메인: 스키마 소유 조직
  // v2: 호환성이 깨지는 변경이 있을 때 올림
  // user.json: 리소스 이름
  "$schema": "<https://json-schema.org/draft/2020-12/schema>",
  "type": "object",
  "properties": {
    "userId": { "type": "string", "format": "uuid" },
    "email": { "type": "string", "format": "email" }
  },
  "required": ["userId", "email"]
}

다른 스키마에서 이 스키마를 참조할 때는 $ref에 동일한 URL을 적습니다.

{
  "$id": "<https://schemas.example.com/v2/order.json>",
  "$schema": "<https://json-schema.org/draft/2020-12/schema>",
  "type": "object",
  "properties": {
    // $ref가 user.json의 $id URL을 가리킨다
    // 밸리데이터는 이 URL을 따라가서 스키마를 가져온다
    "buyer": { "$ref": "<https://schemas.example.com/v2/user.json>" },
    "items": {
      "type": "array",
      "items": { "$ref": "<https://schemas.example.com/v2/product.json>" }
    },
    "totalAmount": { "type": "number", "minimum": 0 }
  },
  "required": ["buyer", "items", "totalAmount"]
}

밸리데이터가 order.json을 처리할 때 $ref의 URL로 HTTP 요청을 보내 user.jsonproduct.json을 가져옵니다. 이 URL이 실제로 접근 가능한 주소이면 별도 설정 없이 참조가 동작합니다.

2) 식별자와 접근 주소는 다르다

$idhttps://schemas.example.com/v2/user.json을 적었다고 해서 밸리데이터가 항상 그 URL로 HTTP 요청을 보내는 것은 아닙니다. $id는 스키마의 고유 이름이지, 네트워크 접근을 보장하는 약속이 아닙니다.

{
  "$id": "<https://schemas.example.com/v2/order.json>",
  "type": "object",
  "properties": {
    "buyer": {
      "$ref": "<https://schemas.example.com/v2/user.json>"
    }
  }
}

이 스키마를 밸리데이터에 넣으면 $ref의 URL을 처리해야 합니다. 이때 두 가지 경로가 있습니다. 운영 환경에서는 해당 URL이 실제로 접근 가능한 호스팅 주소이므로, 밸리데이터가 HTTP GET 요청을 보내 스키마를 가져옵니다. 로컬 개발이나 CI처럼 네트워크가 차단된 환경에서는 스키마를 미리 등록해 두면, 밸리데이터가 $id 값을 키로 매칭해서 로컬에서 찾습니다. 스키마 자체는 동일하고, 밸리데이터가 참조를 해소하는 방식만 달라집니다.

운영 환경에서는 $id URL과 호스팅 위치를 일치시켜서 어떤 도구든 URL만으로 스키마를 가져올 수 있게 하고, 로컬이나 CI에서는 미리 등록하는 구성이 일반적입니다.


2. 호스팅 워크플로우: GitHub + Cloudflare Pages

$id URL로 스키마를 식별하기로 했으면, 그 URL에서 실제로 스키마를 내려줄 호스팅이 필요합니다. 작은 조직이 빠르게 시작할 수 있는 조합으로 GitHub 저장소와 Cloudflare Pages를 예로 들겠습니다.

1) 저장소 구조

스키마 전용 저장소를 하나 만들고, URL 경로와 디렉터리 구조를 일치시킵니다. $idhttps://schemas.example.com/v2/user.json이면, 저장소에서도 같은 경로에 파일이 있어야 합니다.

schemas-repo/
├── v2/
│   ├── user.json          ← $id: <https://schemas.example.com/v2/user.json>
│   ├── order.json         ← $id: <https://schemas.example.com/v2/order.json>
│   └── product.json       ← $id: <https://schemas.example.com/v2/product.json>
├── v1/
│   └── user.json          ← 이전 버전. URL이 유지되므로 기존 참조가 깨지지 않는다
├── _headers               ← Cloudflare Pages가 읽는 응답 헤더 설정
└── index.html             ← 어떤 스키마가 호스팅되어 있는지 사람이 볼 수 있는 랜딩 페이지

$id URL의 경로와 파일 시스템 경로가 일치하므로, URL만 보고도 저장소에서 해당 파일을 찾을 수 있고, 새 스키마를 추가할 때도 규칙이 명확합니다.

2) 응답 헤더 설정

저장소 루트의 _headers 파일에 다음을 적으면, Cloudflare Pages가 모든 .json 파일 응답에 이 헤더를 붙입니다.

/*.json
  Content-Type: application/schema+json
  Cache-Control: public, max-age=86400, immutable

application/schema+json은 JSON 스키마 명세가 정의한 미디어 타입입니다. 명세상 권장(SHOULD)이지 필수(MUST)는 아니며, 대부분의 도구는 application/json으로도 동작합니다. 다만 Hyperjump처럼 미디어 타입을 엄격하게 검사하는 밸리데이터가 있으므로, 스키마 전용 호스팅이라면 application/schema+json으로 설정해 두는 쪽이 안전합니다.

Cache-Control: immutable은 한 번 발행된 URL의 내용이 바뀌지 않는다는 약속입니다. 스키마 내용이 바뀔 때는 기존 URL을 수정하는 대신 새 버전 경로(/v3/user.json)를 발행하고, 기존 경로(/v2/user.json)는 그대로 유지합니다.

3) 배포와 도메인

Cloudflare Pages에 저장소를 연결하면 git push마다 자동 배포가 일어납니다. 정적 파일 호스팅이므로 빌드 명령 없이 디렉터리 그대로 배포됩니다. HTTPS 인증서도 자동 발급·갱신됩니다.

마지막으로 schemas.example.com 같은 커스텀 도메인을 연결합니다. 이 도메인이 스키마 $id의 일부가 되므로, 한 번 정한 뒤에 바꾸면 그 URL을 $ref로 참조하는 모든 스키마가 깨집니다.

배포 후 URL이 실제로 동작하는지 확인하는 방법은 간단합니다.

# 스키마가 올바른 Content-Type과 함께 내려오는지 확인
curl -I <https://schemas.example.com/v2/user.json>

# 기대하는 응답:
# HTTP/2 200
# content-type: application/schema+json
# cache-control: public, max-age=86400, immutable

이 구조가 잡히면 git push 한 번으로 스키마가 발행되고, 어떤 도구든 URL만으로 스키마를 가져올 수 있는 상태가 됩니다.

같은 목적으로 쓸 수 있는 다른 호스팅 옵션도 있습니다.

옵션비용적합한 규모
GitHub Pages공개 저장소 무료작은 팀, 공개 스키마
Cloudflare Pages무료 플랜으로 충분, 트래픽 비용 없음작은 팀에서 중간 규모
AWS S3 + CloudFront트래픽·요청 단위 과금대규모 조직, AWS 인프라와 통합
자체 호스팅서버·운영 비용 전부 부담보안·통제가 핵심인 환경

3. 운영의 네 가지 원칙

호스팅이 갖춰지면 운영 규칙을 정해야 합니다. 네 가지 원칙이 있습니다.

원칙핵심 질문수단
불변성(immutability)발행된 스키마가 변하지 않는가기존 URL 수정 금지, 새 버전 경로로 발행
버전 관리호환성이 깨지는 변경을 구분할 수 있는가URL 경로에 버전 포함, 시맨틱 버저닝 적용
변경 기록(CHANGELOG)누가 언제 무엇을 왜 바꿨는지 알 수 있는가Git 이력 + 사람이 읽을 수 있는 변경 기록
검색과 발견다른 팀이 이 스키마를 찾을 수 있는가카탈로그 페이지, title·description 어노테이션 활용

1) 불변성(immutability)

한 번 발행된 URL의 스키마는 수정하지 않습니다. /v2/user.json을 참조하는 시스템이 10개 있는데 그 파일을 덮어쓰면, 10개 시스템이 예고 없이 다른 스키마를 받게 됩니다. 검증을 통과하던 데이터가 갑자기 실패하거나, 반대로 걸러야 할 데이터가 통과하는 상황이 생깁니다.

불변성을 운영 수준에서 강제하는 방법은 두 가지입니다.

# GitHub Actions 예시: 이미 발행된 버전 경로의 파일이 변경되면 실패
- name: 발행된 스키마 변경 차단
  run: |
    # main 브랜치에 이미 존재하는 파일이 수정되었는지 검사
    CHANGED=$(git diff --name-only origin/main -- v1/ v2/)
    if [ -n "$CHANGED" ]; then
      echo "발행된 스키마가 수정되었습니다: $CHANGED"
      echo "기존 URL을 수정하는 대신 새 버전 경로를 만들어 주세요."
      exit 1
    fi

내용을 고쳐야 하면 같은 URL을 덮어쓰는 대신 새 버전 경로(/v3/user.json)로 새 파일을 발행합니다.

2) 버전 관리

스키마 변경의 호환성에 따라 버전을 나눕니다. 버전 체계는 시맨틱 버저닝(Semantic Versioning)을 기준으로 합니다.

변경 종류호환성예시URL 처리
주 버전(Major, v1 → v2)깨짐필수 필드 제거, 타입 변경새 URL 발행. 기존 URL은 deprecated와 함께 유지
부 버전(Minor, v1.0 → v1.1)유지선택 필드 추가, enum 값 추가같은 주 버전 URL 안에서 갱신 가능
패치(Patch, v1.1.0 → v1.1.1)유지description 수정, examples 추가같은 주 버전 URL 안에서 갱신 가능

부 버전 변경과 주 버전 변경에서 저장소 구조와 URL이 어떻게 달라지는지 비교하겠습니다.

부 버전 변경: 선택 필드 nickname 추가 (호환 유지)

  schemas-repo/
  └── v2/
      └── user.json   ← 같은 URL에서 갱신
                         기존 데이터는 nickname이 없어도 검증 통과
                         새 데이터는 nickname을 포함할 수 있음

  URL 변화 없음: <https://schemas.example.com/v2/user.json>
주 버전 변경: nickname을 displayName으로 변경 + 필수로 승격 (호환 깨짐)

  schemas-repo/
  ├── v2/
  │   └── user.json   ← 기존 URL 유지, deprecated 붙임
  └── v3/
      └── user.json   ← 새 URL 발행

  기존 URL 유지: <https://schemas.example.com/v2/user.json> (deprecated)
  새 URL 발행:   <https://schemas.example.com/v3/user.json>

핵심은 부 버전·패치는 기존 URL을 그대로 쓰고, 주 버전은 반드시 새 URL을 만든다는 규칙입니다. 기존 URL은 내리지 않고 deprecated 어노테이션을 붙여서 유지합니다. deprecated를 읽는 도구가 이전 안내를 표시할 수 있고, 기존 시스템은 당장 깨지지 않습니다.

3) 변경 기록(CHANGELOG)

Git이 기계 수준의 이력을 자동으로 남기지만, 사람이 읽을 수 있는 변경 기록도 함께 관리하는 것이 좋습니다. 저장소 루트에 CHANGELOG.md를 두고, 버전마다 세 가지를 적습니다.

## v3 (2026-05-20)

### 변경 내용
- `nickname` 필드를 `displayName`으로 이름 변경
- `displayName`을 필수 필드로 승격

### 호환성
- **주 버전 변경 (v2 → v3): 호환되지 않음**
- v2 스키마로 검증하던 데이터 중 `nickname`만 있고
  `displayName`이 없는 데이터는 v3에서 검증 실패

### 마이그레이션
- 기존 데이터의 `nickname` 필드를 `displayName`으로 이름 변경
- `displayName`이 비어 있는 레코드는 값을 채운 뒤 마이그레이션
- v2 URL은 deprecated 상태로 유지. 2026년 8월까지 병행 운영 예정

4) 검색과 발견

호스팅된 스키마를 다른 팀이 찾을 수 없으면 자산이 되지 못합니다. 호스팅 섹션에서 만든 index.html이 카탈로그(catalog) 역할을 합니다. 4편에서 다룬 titledescription 어노테이션이 여기서 쓰입니다. 스키마마다 어노테이션을 잘 적어 두면 카탈로그를 자동으로 생성할 수 있습니다.

// 스키마의 어노테이션이 카탈로그 항목이 된다
{
  "$id": "<https://schemas.example.com/v3/user.json>",
  // title → 카탈로그에서 항목 이름으로 표시
  "title": "사용자 프로필",
  // description → 카탈로그에서 한 줄 설명으로 표시
  "description": "회원가입 및 프로필 관리에 사용되는 사용자 스키마. userId, email, displayName을 필수로 포함한다.",
  "type": "object",
  "properties": { ... }
}

저장소의 모든 스키마에서 $id, title, description을 읽어서 목록을 만드는 스크립트를 CI에 붙이면, 스키마를 발행할 때마다 카탈로그가 자동으로 갱신됩니다.


4. 번들링: 분산된 스키마를 한 파일에 모으기

번들링은 한 스키마가 $ref로 참조하는 외부 스키마를 따라가면서, 연결된 스키마를 전부 한 파일에 모으는 작업입니다. order.jsonuser.json을 참조하고, user.json이 다시 address.json을 참조하면, 번들링 도구가 그 참조 체인을 끝까지 따라가서 전부 $defs 안에 넣습니다. 결과물 한 파일만 있으면 네트워크 요청 없이 검증이 동작합니다.

번들링은 이 외부 참조를 한 파일 안으로 집어넣어서 네트워크 의존성을 제거합니다. $defs는 JSON 스키마에서 서브스키마를 모아 두는 표준 키워드인데, 번들링은 외부 스키마를 이 $defs 안에 넣고 원래의 $id를 그대로 유지시킵니다. 밸리데이터는 $ref URL과 $defs 안의 $id를 매칭해서, 네트워크 요청 없이 참조를 해소합니다.

// order.json
// 밸리데이터가 $ref URL 두 개로 HTTP 요청을 보내야 한다
{
  "$id": "<https://schemas.example.com/v2/order.json>",
  "type": "object",
  "properties": {
    "buyer": { "$ref": "<https://schemas.example.com/v2/user.json>" },
    "items": {
      "type": "array",
      "items": { "$ref": "<https://schemas.example.com/v2/product.json>" }
    }
  }
}

앞에서 만든 order.jsonuser.jsonproduct.json$ref로 참조합니다. 밸리데이터가 이 스키마를 처리하려면 참조 대상 두 개를 HTTP로 가져와야 합니다. 오프라인 환경에서는 검증이 막히고, 외부 URL이 잠시 응답하지 않으면 검증이 멈추고, 외부 스키마가 언제 바뀌는지 통제할 수 없습니다.

번들 전                              번들 후

  order.json                           order.bundled.json
  ┌────────────────┐                   ┌───────────────────────────┐
  │ buyer:         │                   │ buyer:                    │
  │   $ref ────────┼──► user.json      │   $ref ─────────────┐     │
  │                │    (HTTP GET)     │                     │     │
  │ items:         │                   │ items:              │     │
  │   $ref ────────┼──► product.json   │   $ref ──────────┐  │     │
  │                │    (HTTP GET)     │                  │  │     │
  └────────────────┘                   │                  │  │     │
                                       │ $defs:           │  │     │
  user.json       product.json         │ ┌────────────────┼──┼───┐ │
  ┌───────┐       ┌───────┐            │ │                │  │   │ │
  │ 별도   │       │ 별도   │            │ │ user ◄─────────┘  │   │ │
  │ 파일   │       │ 파일   │            │ │                   │   │ │
  └───────┘       └───────┘            │ │ product ◄─────────┘   │ │
                                       │ │                       │ │
  3개 파일, HTTP 요청 2회                 │ └───────────────────────┘ │
                                       │ 1개 파일, HTTP 요청 0회      │
                                       └───────────────────────────┘

sourcemeta/jsonschema CLI로 번들링합니다.

# order.json이 참조하는 모든 외부 스키마를 따라가서 한 파일에 모은다
jsonschema bundle order.json > order.bundled.json

Node.js 환경이라면 Hyperjump 번들러를 쓸 수 있습니다.

import { bundle } from "@hyperjump/json-schema/bundle";

const bundled = await bundle("<https://schemas.example.com/v2/order.json>");

어느 도구를 쓰든 결과물의 구조는 같습니다.

// order.bundled.json
// $ref URL은 그대로인데, 참조 대상이 $defs 안에 들어왔다
// 밸리데이터가 $defs 안의 $id와 $ref URL을 매칭하므로
// 네트워크 요청이 발생하지 않는다
{
  "$id": "<https://schemas.example.com/v2/order.json>",
  "type": "object",
  "properties": {
    "buyer": { "$ref": "<https://schemas.example.com/v2/user.json>" },
    "items": {
      "type": "array",
      "items": { "$ref": "<https://schemas.example.com/v2/product.json>" }
    }
  },
  "$defs": {
    "user": {
      // 원래의 $id가 그대로 유지된다
      "$id": "<https://schemas.example.com/v2/user.json>",
      "type": "object",
      "properties": {
        "userId": { "type": "string", "format": "uuid" },
        "email": { "type": "string", "format": "email" }
      },
      "required": ["userId", "email"]
    },
    "product": {
      "$id": "<https://schemas.example.com/v2/product.json>",
      "type": "object",
      "properties": {
        "productId": { "type": "string" },
        "name": { "type": "string" },
        "price": { "type": "number", "minimum": 0 }
      },
      "required": ["productId", "name", "price"]
    }
  }
}

$ref의 URL은 번들 전과 동일합니다. 달라진 것은 참조 대상이 같은 파일의 $defs 안에 들어왔다는 점뿐입니다. 밸리데이터가 $ref URL과 $defs 안의 $id를 매칭하므로, 외부 네트워크 없이 검증이 동작합니다.

CI 파이프라인에 번들링 단계를 넣으면 스키마를 발행할 때마다 번들 파일이 자동으로 생성됩니다. 다만 번들 파일은 만든 시점의 스냅샷(snapshot, 특정 시점의 사본)이므로, 원본 외부 스키마가 갱신되면 다시 번들링을 돌려야 합니다.

번들링은 호스팅을 대체하는 것이 아니라 보완하는 구조입니다. 호스팅된 스키마가 URL로 누구든 참조할 수 있는 원본이고, 번들은 특정 환경(서비스 패키지, SDK, 오프라인 검증)에 배포하기 위한 스냅샷입니다.


5. 시리즈의 내용이 한 스키마에 모이는 모습

6편까지 다룬 내용들이 실제 스키마 하나에 어떻게 적용되는지 보겠습니다. 시리즈 처음부터 등장한 회원가입 이벤트 스키마를 운영 단계까지 반영한 최종 형태입니다.

{
  // 6편: HTTP URL로 식별하고 호스팅한다
  "$schema": "<https://json-schema.org/draft/2020-12/schema>",
  "$id": "<https://schemas.example.com/v2/signup-event.json>",

  // 4편: 어노테이션으로 의미를 담는다
  "title": "회원가입 이벤트",
  "description": "회원이 가입을 완료한 시점에 발생하는 분석 이벤트. 마케팅 채널 분석과 코호트 분석의 입력 데이터.",
  "examples": [
    {
      "userId": "550e8400-e29b-41d4-a716-446655440000",
      "signedUpAt": "2026-05-20T09:00:00Z",
      "channel": "organic",
      "marketingConsent": true
    }
  ],

  // 5편: 간이 확장으로 조직 고유 메타데이터를 추가한다
  "myorg:classification": "internal",

  // 2편: 구조를 정의한다
  "type": "object",
  "required": ["userId", "signedUpAt", "channel"],
  "additionalProperties": false,

  // 3편: 각 필드의 타입과 형식으로 검증한다
  "properties": {
    "userId": {
      "type": "string",
      "format": "uuid",
      "description": "회원의 고유 식별자. 인증 서비스에서 발급한 UUID."
    },
    "signedUpAt": {
      "type": "string",
      "format": "date-time",
      "description": "가입 완료 시점. UTC, RFC 3339 형식."
    },
    "channel": {
      "type": "string",
      "enum": ["organic", "ad", "referral"],
      "description": "유입 채널. 마케팅 분석의 주 분류 키."
    },
    "marketingConsent": {
      "type": "boolean",
      "default": false,
      "description": "마케팅 정보 수신 동의 여부."
    }
  }
}
시리즈 편이 스키마에서 쓰인 도구
2편 (작성)type, required, properties, additionalProperties
3편 (검증)format, enum으로 값의 형식과 범위 제한
4편 (어노테이션)title, description, examples, default로 의미 전달
5편 (어휘 확장)myorg:classification으로 조직 고유 메타데이터 추가
6편 (운영)$id URL로 식별, 호스팅 인프라에 발행, 버전 관리 적용

이 파일이 호스팅 인프라에 발행되면, 조직 안의 밸리데이터·문서 생성기·코드 생성기·데이터 카탈로그가 같은 URL에서 같은 정의를 가져갑니다.


마무리

6편에서는 스키마를 만든 뒤 자산으로 운영하는 방법을 다뤘습니다. $id와 HTTP URL로 스키마를 식별하고, 정적 호스팅으로 발행하고, 불변성·버전 관리·변경 기록·검색의 네 가지 원칙으로 운영하고, 번들링으로 분산된 스키마를 한 파일에 모아 네트워크 의존성을 제거하는 흐름이었습니다.

스키마는 만드는 것보다 운영하는 것이 더 오래 걸리는 작업입니다. 한 번 발행된 URL은 쉽게 바꿀 수 없고, 그 URL을 참조하는 시스템은 시간이 지날수록 늘어납니다. 운영 규칙을 처음부터 잡아 두는 쪽이, 나중에 수습하는 것보다 비용이 적게 듭니다.


JSON 시리즈

(1) JSON과 JSON 스키마 이해하기

(2) JSON 스키마 작성하기

(3) JSON 스키마로 데이터 검증하기

(4) JSON 스키마에 어노테이션으로 의미와 맥락 담기

(5) JSON 스키마 어휘 확장하기

(6) JSON 스키마를 운영에 적용하기