# Kong ↔ Azure APIM 전환 검증 도구
동일 기능의 API를 Kong 게이트웨이와 Azure APIM 양쪽으로 호출해, HTTP 상태 코드와 응답 본문이 일치하는지 API 단위로 판정합니다. 폐쇄망 Windows에서 추가 설치 없이 동작합니다.
## 실행
```
run.bat 더블클릭
```
또는
```
powershell -ExecutionPolicy Bypass -File .\run.ps1
powershell -ExecutionPolicy Bypass -File .\run.ps1 -Port 9000 -NoBrowser
powershell -ExecutionPolicy Bypass -File .\run.ps1 -Config config-prod.json
```
| 옵션 | 기본값 | 설명 |
|------|--------|------|
| `-Port` | `8080` | 사용 중이면 최대 24개 다음 포트까지 자동으로 찾습니다 |
| `-Config` | `config.json` | 시작 시 선택될 설정 파일 |
| `-NoBrowser` | — | 브라우저 자동 실행 안 함 |
기동되면 기본 브라우저가 자동으로 열립니다. 종료는 콘솔에서 `Ctrl+C`.
> `index.html`을 파일로 직접 열면 동작하지 않습니다. 브라우저는 `Host` 헤더 지정과 자체서명 인증서 요청을 차단하므로, 실제 HTTP 호출은 `run.ps1`이 대행합니다.
## 설정 파일 선택
`run.ps1` 바로 옆의 `*.json` 파일이 모두 툴바의 **설정 파일** 드롭다운에 나열됩니다. 환경별로 파일을 나눠 두고 화면에서 전환하면 됩니다.
```
config.json ← 기본 (개발)
config-prod.json ← 운영
config-stg.json ← 스테이징
apis/search.json ← 그룹 파일 (하위 폴더라 목록에 안 나옴)
```
- 기본 선택은 `-Config` 값 → `config.json` → 이름순 첫 파일 순으로 결정됩니다.
- 파일을 새로 추가해도 **서버 재시작 없이** `다시 읽기`만 누르면 목록에 나타납니다.
- 설정을 바꾸면 실행 결과와 변수 입력값이 초기화됩니다. 변수 값과 요청 간격은 **설정 파일별로** 따로 기억되므로, 개발 토큰이 운영 설정 입력란에 남지 않습니다.
## 구성
| 파일 | 역할 |
|------|------|
| `run.ps1` | 로컬 UI 서버 + HTTP 프록시 (PowerShell 5.1 / 7.x) |
| `index.html` | UI 전체 (외부 JS/CSS 없이 인라인) |
| `config.json` | 최상위 설정 — 변수, 게이트웨이 주소, 그룹 파일 include |
| `apis/*.json` | 서비스별 API 그룹 파일 |
| `run.bat` | ExecutionPolicy 우회 런처 |
## config.json
```jsonc
{
"timeoutMs": 30000, // 요청 타임아웃 (기본 30초)
"requestIntervalMs": 0, // API 간 요청 간격 (기본 0 = 대기 없음)
"include": [ // 그룹 파일 (아래 "API 그룹" 참고)
"apis/search.json",
"apis/rp.json"
],
"variables": { // UI 상단 입력란으로 노출됨
"VHOST": { "label": "가상 호스트명", "default": "test.dev.gis.kt.com" },
"KONG_TOKEN": { "label": "Kong 토큰", "default": "", "secret": true }
// "VHOST": "test.dev.gis.kt.com" ← 문자열 축약형도 가능
},
"targets": {
"kong": {
"label": "Kong",
"baseUrl": "https://221.148.247.196:10001",
"headers": { "Host": "{{VHOST}}", "Authorization": "bearer {{KONG_TOKEN}}" }
},
"apim": {
"label": "Azure APIM",
"baseUrl": "https://20.249.202.204:10001",
"headers": { "Host": "{{VHOST}}" }
}
},
"groups": [ // 파일 안에 직접 그룹을 둘 수도 있음
{ "name": "etc", "label": "기타", "apis": [] }
]
}
```
`variables`, `targets`, `timeoutMs`, `requestIntervalMs`는 **최상위 설정 파일에만** 둡니다. 그룹 파일에 써도 무시됩니다.
**변수 치환**: 모든 문자열의 `{{VAR}}`가 UI 입력값으로 치환됩니다. 값이 비어 있으면 치환되지 않고 해당 API는 실행 전 오류로 표시됩니다. 입력값은 브라우저 `sessionStorage`에만 보관되며 파일에 기록되지 않습니다.
## API 그룹
API는 항상 그룹에 속합니다. 그룹은 화면에서 접고 펼 수 있고, 그룹 단위로 실행하며, 그룹별 OK/Fail/ERROR 집계가 따로 표시됩니다.
그룹을 정의하는 방법은 세 가지이고 **섞어 쓸 수 있습니다.**
| 방법 | 위치 | 용도 |
|------|------|------|
| `include` | 최상위 설정 | 서비스별 파일 분리 (권장) |
| `groups` | 최상위 설정 또는 그룹 파일 | 파일 하나에 여러 그룹 |
| `apis` | 최상위 설정 | 그룹 없이 쓰던 기존 설정 — `default` 그룹으로 자동 변환 |
그룹이 화면에 나오는 순서는 **최상위 `apis` → 최상위 `groups` → `include` 나열 순서**입니다.
### 그룹 파일 (권장)
`apis/` 폴더에 서비스별로 하나씩 만듭니다.
```jsonc
// apis/search.json
{
"name": "search", // 필수. 그룹 식별자 (전체에서 유일해야 함)
"label": "검색 서비스", // 선택. 화면 표시명 (없으면 name 사용)
"apis": [
{
"name": "경로검색",
"method": "GET", // GET | POST
"path": "/giop/rest/etc/RouteSearch.xml",
"enabled": true, // false면 목록에서 아예 제외
// 이하 전부 선택 항목
"query": { "x": "127.0", "y": "37.5" },
"headers": { "X-Common": "v" }, // 양쪽 공통, targets 헤더를 덮어씀
"targetHeaders": { // 한쪽에만 필요한 헤더
"apim": { "Ocp-Apim-Subscription-Key": "{{APIM_KEY}}" }
},
"paths": { "apim": "/v2/RouteSearch.xml" }, // 전환하며 경로가 바뀐 경우
"body": "{ \"q\": 1 }", // POST 본문 (객체로 쓰면 JSON 직렬화)
"ignorePatterns": ["[^<]*"]
}
]
}
```
`name`을 생략하면 **파일 이름**이 그룹 이름이 됩니다 (`apis/rp.json` → `rp`).
한 파일에 여러 그룹을 넣으려면 `groups` 배열을 씁니다.
```jsonc
// apis/legacy.json
{
"groups": [
{ "name": "legacy-a", "label": "레거시 A", "apis": [ /* ... */ ] },
{ "name": "legacy-b", "label": "레거시 B", "apis": [ /* ... */ ] }
]
}
```
### 그룹 추가 절차
1. `apis/` 폴더에 `<서비스명>.json` 생성
2. `name`, `label`, `apis` 작성
3. `config.json`의 `include` 배열에 경로 추가
4. 화면에서 **다시 읽기** — 서버 재시작 불필요
### include 규칙
- 경로는 `run.ps1` 기준 **상대 경로**입니다. 절대 경로(`C:\...`)는 거부됩니다.
- 도구 폴더 바깥(`../`)을 가리키면 거부됩니다.
- **중첩 include는 지원하지 않습니다.** 그룹 파일 안의 `include`는 오류로 처리됩니다 (순환 참조 원천 차단).
- 파일이 없거나 JSON이 깨졌거나 `apis`가 없으면 **어느 파일이 문제인지 이름과 함께** 화면에 표시되고 설정 전체가 로드되지 않습니다.
- 그룹 이름이 중복되면 어느 두 파일이 충돌하는지 표시하며 거부합니다.
- 그룹 파일을 `apis/` 하위에 두는 이유: 상단의 **설정 파일 선택 목록**은 `run.ps1` 바로 옆의 `*.json`만 훑습니다. 하위 폴더에 두면 그룹 파일이 설정 파일로 잘못 나열되지 않습니다.
**헤더 우선순위**: `targets.*.headers` → `api.headers` → `api.targetHeaders.` 순으로 키 단위 덮어쓰기. 최종 값이 빈 문자열이면 해당 헤더는 전송하지 않습니다.
## 테스트 실행
| 범위 | 조작 | 동작 |
|------|------|------|
| **전체** | 툴바 `전체 실행` | 모든 그룹의 모든 API를 위에서부터 순차 실행 |
| **그룹** | 그룹 헤더의 `그룹 실행` | 그 그룹의 API만 실행. 다른 그룹의 기존 결과는 유지 |
| **개별** | API 행의 `실행` | 그 API 하나만 실행 |
- 실행은 항상 **순차**입니다. 진행 상황이 툴바에 `3 / 12 — 경로검색` 형태로 표시됩니다.
- 실행이 끝나면 `완료 (12/12건 · OK 10 · Fail 1 · ERROR 1)` 요약이 남습니다.
- **중지** 버튼을 누르면 진행 중인 요청이 끝난 뒤 멈춥니다. 이미 나온 결과는 보존되며 `중지됨`으로 표시됩니다.
- Fail이나 ERROR가 난 항목은 상세가 자동으로 펼쳐집니다.
- 설정 파일을 바꾸면 결과는 초기화됩니다. 그룹 실행은 다른 그룹 결과를 지우지 않습니다.
### 요청 간격
연속 호출이 게이트웨이의 rate limit에 걸리거나 백엔드에 부담을 주는 경우, API 사이에 대기 시간을 둘 수 있습니다.
- 툴바의 **요청 간격** 입력란(ms)에서 조절합니다.
- 초기값은 설정 파일의 `requestIntervalMs`이고, 화면에서 바꾼 값은 **설정 파일별로** 세션에 기억됩니다.
- 간격은 **API와 API 사이에만** 적용됩니다. 첫 API 전에는 대기하지 않고, 개별 실행은 대기 없이 즉시 시작합니다.
- 한 API의 Kong 호출과 APIM 호출 사이에는 간격이 적용되지 않습니다. 두 호출은 한 묶음입니다.
- `0`이면 대기 없이 연속 실행합니다.
- 대기 중에는 진행 표시가 `대기 500ms (4/12)`로 바뀝니다.
예: API 20개를 1초 간격으로 돌리면 최소 19초가 걸립니다. 긴 실행은 **중지**로 언제든 끊을 수 있습니다.
## 판정 규칙
| 결과 | 조건 |
|------|------|
| **OK** | HTTP 상태 코드가 같고 응답 본문이 완전히 동일 |
| **Fail** | 상태 코드 또는 본문이 다름 (라인 diff 표시) |
| **ERROR** | 한쪽이라도 연결 실패·타임아웃, 또는 설정 오류 |
판정 방식은 응답의 `Content-Type`에 따라 자동으로 갈립니다.
| 응답 종류 | Content-Type 예 | 비교 방식 | 화면 표시 |
|-----------|-----------------|-----------|-----------|
| 텍스트 | `application/xml`, `application/json`, `text/*`, `image/svg+xml` | 문자열 비교 + 라인 diff | 본문 그대로, 차이는 diff |
| 이미지 | `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/bmp` | **바이트 단위 SHA-256** | **이미지로 렌더링** |
| 기타 바이너리 | `application/pdf`, `application/octet-stream` 등 | 바이트 단위 SHA-256 | 형식·크기·해시만 표시 |
- **이미지 응답**은 좌우 컬럼에 실제 이미지로 나란히 표시되어 눈으로 대조할 수 있습니다. 투명 PNG도 보이도록 체커보드 배경을 깔았습니다. 이미지 위에는 MIME 타입·바이트 크기·SHA-256 앞 16자리가 함께 표시됩니다.
- **`image/svg+xml`은 일부러 텍스트로 취급합니다.** SVG는 텍스트 포맷이고, 해시 한 줄보다 라인 diff가 훨씬 유용하기 때문입니다.
- `Content-Type`이 없는 응답은 텍스트로 취급합니다.
- 한쪽은 이미지, 한쪽은 텍스트로 돌아오면 `Fail`이며 어느 쪽이 어느 형식인지 표시합니다. 전환 과정에서 오류 페이지(HTML)가 이미지 대신 반환되는 경우를 잡습니다.
- **`ignorePatterns`는 바이너리에 적용되지 않습니다.** 지정되어 있으면 "적용되지 않았음" 경고를 띄우고 바이트 단위로 비교합니다.
- 본문이 8MB를 넘으면 인라인 표시만 생략하고, **비교는 SHA-256으로 정상 수행**됩니다.
- 응답 헤더는 **판정에 반영하지 않습니다.** 게이트웨이가 다르면 `Date`, `Server`, `X-Kong-Upstream-Latency`, `Request-Context` 등은 필연적으로 달라지기 때문입니다. 화면에는 양쪽 전부 표시하고, 값이 다른 항목만 노란색으로 강조합니다.
- 본문 비교는 **기본이 엄격 일치**입니다. 공백 차이도 Fail입니다.
- `ignorePatterns`를 지정한 API만 해당 정규식을 치환한 뒤 비교합니다. 자세한 사용법은 아래 [ignorePatterns](#ignorepatterns) 참고.
## ignorePatterns
`transactionID`처럼 요청마다 달라지는 값 때문에 본문이 절대 같아질 수 없는 경우, 그 부분만 비교에서 제외합니다.
```json
{
"name": "경로검색",
"method": "GET",
"path": "/giop/rest/etc/RouteSearch.xml",
"ignorePatterns": [
"[^<]*",
"[^<]*",
"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}"
]
}
```
### 여러 개를 쓸 때
**배열에 나열한 순서대로, 위에서부터 차례로 적용됩니다.** 각 패턴은 자기 순번의 고유 토큰으로 치환되므로, 서로 다른 필드가 같은 토큰으로 뭉개져 진짜 차이를 놓치는 일은 없습니다.
```
원본 602XHJs-1t-aaa2026-08-13T11:30:38
비교값 602
```
이 덕분에 아래처럼 **필드 순서가 뒤바뀐 경우도 Fail로 잡힙니다.**
```
Kong 12 ->
APIM 21 -> ← 다름
```
**패턴끼리 겹치면 앞선 것이 이깁니다.** 넓은 패턴을 위에 두면 아래 패턴이 매칭될 대상 자체가 사라집니다.
```json
// 나쁜 예 — 1번은 영원히 매칭되지 않음
[".*", "[^<]*"]
```
좁은 패턴을 먼저, 넓은 패턴을 나중에 두세요.
### 결과 화면의 매칭 통계
패턴이 하나라도 있으면 결과 상세에 패턴별 매칭 횟수 표가 나옵니다.
| 토큰 | 패턴 | Kong | APIM |
|------|------|------|------|
| `` | `[^<]*` | 1회 | 1회 |
| `` | `[^<]*` | 1회 | 0회 |
두 가지 경고를 자동으로 띄웁니다.
- **양쪽 모두 0회 매칭** — 정규식 오타일 가능성이 높습니다. 패턴은 조용히 무시되므로 이 표가 유일한 단서입니다.
- **양쪽 매칭 횟수가 다름** — 한쪽에만 있는 필드라는 뜻이고, 전환 누락일 수 있습니다. 위 예에서 APIM 응답에는 ``가 아예 없습니다.
### 작성 요령
| 대상 | 패턴 |
|------|------|
| XML 엘리먼트 | `[^<]*` |
| 여러 엘리먼트 한 번에 | `<(transactionId\|reqId\|seq)>[^<]*\\1>` |
| XML 속성 | `id="[^"]*"` |
| JSON 문자열 속성 | `"transactionId"\\s*:\\s*"[^"]*"` |
| JSON 숫자 속성 | `"elapsed"\\s*:\\s*\\d+` |
| ISO 타임스탬프 | `\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}` |
| UUID | `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}` |
- **`.*`를 쓰지 마세요.** 탐욕적 매칭이라 같은 태그가 두 번 나오면 그 사이 내용을 전부 삼킵니다. XML은 `[^<]*`, JSON 문자열은 `[^"]*`를 쓰세요.
- **JSON 이스케이프**: 백슬래시는 `\\`, 큰따옴표는 `\"`로 씁니다. 정규식의 `\d`는 설정 파일에서 `\\d`가 됩니다.
- 정규식 문법이 잘못되면 조용히 넘어가지 않고 해당 API가 **ERROR**로 표시되며 몇 번째 패턴이 문제인지 알려줍니다.
- 패턴은 API별로 지정합니다. 전역 패턴은 없습니다. 비워 두거나 생략하면 엄격 비교가 유지됩니다.
- **바이너리 응답(이미지·PDF 등)에는 적용되지 않습니다.** 지정되어 있으면 경고를 띄우고 바이트 단위로 비교합니다.
### 남용하지 마세요
`ignorePatterns`는 **진짜 불일치를 숨길 수 있는 기능**입니다. 게이트웨이만 바뀌고 백엔드가 같다면 본문은 원래 완전히 같아야 정상입니다. 매 요청 달라지는 값(트랜잭션 ID, 타임스탬프, 서명)에만 쓰고, "Fail이 자꾸 떠서" 넓은 패턴을 추가하는 것은 검증을 무의미하게 만듭니다. 적용된 패턴은 항상 결과 화면에 표시되므로 나중에 무엇을 제외했는지 확인할 수 있습니다.
## 알려진 제약
- 자체서명·만료 인증서를 무조건 허용합니다(`curl -k` 상당). 검증 도구 용도로만 사용하세요.
- 서버는 요청을 한 번에 하나씩 처리합니다. Kong/APIM 호출은 순차 실행되며, 응답 시간 표시는 각 호출별로 개별 측정되므로 영향받지 않습니다.
- 본문이 1200행을 넘으면 라인 diff 대신 첫 차이 위치와 그 주변 문자열만 표시합니다.
- include는 1단계만 해석합니다. 그룹 파일이 또 다른 파일을 include할 수 없습니다.
- 그룹 파일은 `apis`만 담습니다. 그룹 파일에 `variables`나 `targets`를 써도 무시됩니다.
- 요청 간격은 API 사이에만 적용되며, 한 API의 Kong·APIM 두 호출 사이에는 적용되지 않습니다.
- 이미지·바이너리 응답은 8MB까지만 화면에 표시합니다. 그 이상은 SHA-256으로만 비교합니다.
- 설정 파일 이름에 제어 문자가 들어간 요청은 400이 아닌 500으로 응답합니다. 파일은 서빙되지 않으므로 동작·보안에는 영향이 없습니다.
- `run.ps1`의 콘솔 메시지는 영문입니다. PowerShell 5.1이 BOM 없는 UTF-8 스크립트의 한글을 오독하기 때문이며, UI 한국어 표기에는 영향이 없습니다.