# Kong ↔ Azure APIM 전환 검증 도구 동일 기능의 API를 Kong 게이트웨이와 Azure APIM 양쪽으로 호출해, HTTP 상태 코드와 응답 본문이 일치하는지 API 단위로 판정합니다. Java(JRE 17 이상)만 설치되어 있으면 폐쇄망 Windows에서도 별도 설치 없이 동작합니다. > 이 도구는 원래 PowerShell(`run.ps1`)로 구현되어 있었으나, 이제 **Java jar(`ApiDiff.jar`) 실행 방식만 사용**합니다. `run.ps1`/`run.bat`은 더 이상 유지되지 않습니다. ## 실행 ``` apiDiff.bat 더블클릭 ``` 또는 콘솔에서 직접: ``` java -jar ApiDiff.jar java -jar ApiDiff.jar --port 9000 --no-browser java -jar ApiDiff.jar --config config-prod.json ``` | 옵션 | 기본값 | 설명 | |------|--------|------| | `--port` | `8080` | 사용 중이면 최대 24개 다음 포트까지 자동으로 찾습니다 | | `--config` | `config.json` | 시작 시 선택될 설정 파일 | | `--no-browser` | — | 브라우저 자동 실행 안 함 | | `--debug` | — | 콘솔에 로그 출력 ([debug 로그](#debug-로그) 참고) | `apiDiff.bat`은 인자 없이 `java -jar ApiDiff.jar`만 실행합니다. 옵션을 주려면 콘솔에서 `java -jar ApiDiff.jar ...` 형태로 직접 실행하세요. 기동되면 기본 브라우저가 자동으로 열립니다(미지원 환경이면 콘솔에 URL만 출력됩니다). 종료는 콘솔에서 `Ctrl+C`. > `index.html`을 파일로 직접 열면 동작하지 않습니다. 브라우저는 `Host` 헤더 지정과 자체서명 인증서 요청을 차단하므로, 실제 HTTP 호출은 `ApiDiff.jar`가 띄우는 내장 서버가 대행합니다. ## 설정 파일 선택 `ApiDiff.jar`(또는 `apiDiff.bat`) 바로 옆의 `*.json` 파일이 모두 툴바의 **설정 파일** 드롭다운에 나열됩니다. 환경별로 파일을 나눠 두고 화면에서 전환하면 됩니다. ``` config.json ← 기본 (개발) config-prod.json ← 운영 config-stg.json ← 스테이징 apis/search.json ← 그룹 파일 (하위 폴더라 목록에 안 나옴) ``` - 기본 선택은 `--config` 값 → `config.json` → 이름순 첫 파일 순으로 결정됩니다. - 파일을 새로 추가해도 **서버 재시작 없이** `다시 읽기`만 누르면 목록에 나타납니다. - 설정을 바꾸면 실행 결과와 변수 입력값이 초기화됩니다. 변수 값과 요청 간격은 **설정 파일별로** 따로 기억되므로, 개발 토큰이 운영 설정 입력란에 남지 않습니다. - `config.json`이 아예 없는 상태로 처음 실행하면 샘플 파일이 자동 생성됩니다. 이미 파일이 있으면 그대로 읽습니다. ## 구성 | 파일 | 역할 | |------|------| | `ApiDiff.jar` | 내장 UI 서버 + HTTP 프록시 (Java, 외부 라이브러리는 Gson뿐) | | `apiDiff.bat` | 더블클릭 런처 — `java -jar ApiDiff.jar` 실행 | | `config.json` | 최상위 설정 — 변수, 게이트웨이 주소, 그룹 파일 include | | `apis/*.json` | 서비스별 API 그룹 파일 | UI(`index.html`/`style.css`/`app.js`/`messages.js`)는 jar 내부 리소스로 번들되어 별도 파일로 배포되지 않습니다. 화면에 표시되는 문구만 바꾸고 싶다면 jar 소스의 `ApiDiff/ApiDiff/src/main/resources/static/messages.js`를 수정한 뒤 다시 빌드하면 됩니다 — 로직(`app.js`)과 스타일(`style.css`)을 건드릴 필요가 없습니다. ## config.json ```jsonc { "timeoutMs": 30000, // 요청 타임아웃 (기본 30초) "requestIntervalMs": 0, // API 간 요청 간격 (기본 0 = 대기 없음) "debug": false, // 콘솔 로그 (기본 false, --debug 가 우선) "compareHeaders": [], // 판정에 반영할 응답 헤더 (기본 [] = 미반영) "compareMode": "dual", // "dual"(기본) 또는 "single" // "compareTargets": ["kong", "apim"], // dual 모드가 비교할 targets 키 2개 (기본값이 바로 이 값) // "singleTarget": "kong", // compareMode 가 "single" 일 때 필수 "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": ["[^<]*"] } ] } ``` ### body — application/x-www-form-urlencoded `Content-Type`이 `application/x-www-form-urlencoded`일 때, 즉 curl로 치면 ``` curl --data-urlencode 'start=1234' --data-urlencode 'end=1234' ... ``` 형태로 보내야 하는 요청은 `body`를 **객체**로 쓰면 됩니다. 헤더의 `Content-Type` 값이 `application/x-www-form-urlencoded`로 지정되어 있으면, 객체는 JSON이 아니라 `key=value&key2=value2` 형태로 각 값이 URL-인코딩되어 직렬화됩니다. ```jsonc { "name": "기간 조회", "method": "POST", "path": "/giop/rest/etc/Range", "headers": { "Content-Type": "application/x-www-form-urlencoded" }, "body": { "start": "1234", "end": "1234" } // 실제 전송 본문: start=1234&end=1234 } ``` - `Content-Type`은 `targets.*.headers` / `api.headers` / `api.targetHeaders.` 어디에 있어도 됩니다(우선순위는 [헤더 우선순위](#api-그룹)와 동일). - `{{VAR}}` 변수는 URL-인코딩되기 **전에** 치환되므로, 값에 `&`나 공백이 있어도 정상적으로 이스케이프됩니다. - `Content-Type`이 `application/x-www-form-urlencoded`가 아니면(생략 포함), 객체 `body`는 기존과 동일하게 **JSON 문자열로 직렬화**됩니다. - 이미 인코딩된 문자열을 직접 쓰고 싶다면 `body`를 객체 대신 문자열로 적으면 됩니다 — 이 경우 그대로 전송되며 별도 인코딩은 하지 않습니다. `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 규칙 - 경로는 `ApiDiff.jar` 기준 **상대 경로**입니다. 절대 경로(`C:\...`)는 거부됩니다. - 도구 폴더 바깥(`../`)을 가리키면 거부됩니다. - **중첩 include는 지원하지 않습니다.** 그룹 파일 안의 `include`는 오류로 처리됩니다 (순환 참조 원천 차단). - 파일이 없거나 JSON이 깨졌거나 `apis`가 없으면 **어느 파일이 문제인지 이름과 함께** 화면에 표시되고 설정 전체가 로드되지 않습니다. - 그룹 이름이 중복되면 어느 두 파일이 충돌하는지 표시하며 거부합니다. - 그룹 파일을 `apis/` 하위에 두는 이유: 상단의 **설정 파일 선택 목록**은 `ApiDiff.jar` 바로 옆의 `*.json`만 훑습니다. 하위 폴더에 두면 그룹 파일이 설정 파일로 잘못 나열되지 않습니다. **헤더 우선순위**: `targets.*.headers` → `api.headers` → `api.targetHeaders.` 순으로 키 단위 덮어쓰기. 최종 값이 빈 문자열이면 해당 헤더는 전송하지 않습니다. ## 테스트 실행 | 범위 | 조작 | 동작 | |------|------|------| | **전체** | 툴바 `전체 실행` | 모든 그룹의 모든 API를 위에서부터 순차 실행 | | **그룹** | 그룹 헤더의 `그룹 실행` | 그 그룹의 API만 실행. 다른 그룹의 기존 결과는 유지 | | **개별** | API 행의 `실행` | 그 API 하나만 실행 | - 실행은 항상 **순차**입니다. 진행 상황이 툴바에 `3 / 12 — 경로검색` 형태로 표시됩니다. - 한 API의 Kong·APIM 두 호출은 **동시에** 나갑니다. 양쪽이 3초씩 걸리는 API는 6초가 아니라 약 3초에 끝납니다. - 실행이 끝나면 `완료 (12/12건 · OK 10 · Fail 1 · ERROR 1)` 요약이 남습니다. - **중지** 버튼을 누르면 **진행 중인 요청까지 즉시 취소**하고 멈춥니다. 이미 나온 결과는 보존되며 `중지됨`으로 표시됩니다. ### 목록 컬럼 | 컬럼 | 내용 | |------|------| | API | 이름. `ignorePatterns`가 있으면 개수가 함께 표시됩니다 | | **Host** | 실제로 전송되는 `Host` 헤더 값 | | 메서드 | GET / POST | | 경로 | `path` | | 판정 | OK / Fail / ERROR / 실행 중 / 미실행 | **Host 컬럼**은 최종적으로 머지된 헤더에서 `Host`를 뽑아 **변수 치환 후**의 값을 보여줍니다. 머지 우선순위는 다른 헤더와 같습니다. ``` targets..headers → api.headers → api.targetHeaders. (뒤쪽이 이김) ``` - Kong과 APIM의 Host가 **같으면 한 줄**로 표시합니다. - **다르면 `kong.local → apim.local`** 처럼 양쪽을 모두 보여주고 빨간색으로 강조합니다. 좌우 Host가 갈리는 것은 대개 설정 실수입니다. - 단일 타깃 모드에서는 해당 타깃의 Host만 표시합니다. - 변수를 아직 입력하지 않았으면 치환되지 않은 `{{VHOST}}` 원문이 그대로 보입니다. - 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` 등은 필연적으로 달라지기 때문입니다. 화면에는 양쪽 전부 표시하고, 값이 다른 항목만 노란색으로 강조합니다. **특정 헤더만 판정에 넣고 싶으면** [응답 헤더 판정](#응답-헤더-판정-compareheaders)을 쓰세요. - 본문 비교는 **기본이 엄격 일치**입니다. 공백 차이도 Fail입니다. - `ignorePatterns`를 지정한 API만 해당 정규식을 치환한 뒤 비교합니다. 자세한 사용법은 아래 [ignorePatterns](#ignorepatterns) 참고. - 본문이 16MB를 넘으면 읽기를 중단하고 **수신한 부분의 SHA-256으로만** 비교합니다. 이 경우 `ignorePatterns`는 적용되지 않으며 화면에 경고가 표시됩니다. ### 응답 헤더 판정 (compareHeaders) 지정한 헤더만 골라 좌우 값이 같은지 판정에 반영합니다. 지정하지 않으면 **기존과 완전히 동일하게** 동작합니다. ```jsonc // config.json — 전역 기본값 { "compareHeaders": ["Content-Type", "Cache-Control"] } ``` ```jsonc // apis/search.json — API별 조정 { "name": "경로검색", "method": "GET", "path": "/giop/rest/etc/RouteSearch.xml", "compareHeaders": ["X-Custom"], // 전역 목록에 추가 "ignoreHeaders": ["Cache-Control"] // 전역 목록에서 제외 } ``` **판정 대상 = 전역 `compareHeaders` ∪ API `compareHeaders` − API `ignoreHeaders`** `ignoreHeaders`가 마지막에 적용되므로, 같은 헤더를 `compareHeaders`와 `ignoreHeaders` 양쪽에 쓰면 **제외가 이깁니다.** | 규칙 | 동작 | |------|------| | 헤더 **이름** | 대소문자 무시. `content-type`으로 내려와도 `Content-Type` 지정과 매칭됩니다 | | 헤더 **값** | **문자열 완전 일치**. 정규화·부분 일치 없음 | | 양쪽 모두 없음 | 일치로 간주 | | 한쪽만 있음 | 불일치 | | 목록이 빔 | 헤더 판정 자체를 수행하지 않음 (기존 동작) | 값이 완전 일치라는 점을 특히 주의하세요. | Kong | APIM | 판정 | |------|------|------| | `text/xml` | `text/xml` | OK | | `text/xml` | `text/xml; charset=utf-8` | **Fail** | | `no-cache` | `max-age=60` | **Fail** | 불일치하면 판정이 `Fail`이 되고 사유에 헤더 이름이 나옵니다 — `응답 헤더 불일치 (Content-Type)`. 상세 화면의 응답 헤더 표에서 판정 대상 행은 이름 뒤에 `[판정]`이 붙고 굵게 표시됩니다. 그중 값이 다른 행은 **빨간색**이 되어, 단순히 값이 다를 뿐인 노란색 행과 구분됩니다. 표 제목도 `응답 헤더 (판정 대상 2개 · 불일치 1개)` 형태로 실제 상태를 알려줍니다. ## dual 모드의 대상 이름 변경 (compareTargets) `dual` 모드는 기본적으로 `targets.kong`과 `targets.apim` 두 키를 비교합니다. 검증 대상 시스템이 Kong/Azure APIM이 아니라면(예: Nginx ↔ Envoy 전환처럼 대상 자체가 바뀌는 경우), `targets`의 키 이름을 자유롭게 짓고 `compareTargets`로 어느 두 키를 비교할지 지정하면 됩니다. ```jsonc { "compareMode": "dual", "compareTargets": ["nginx", "envoy"], // targets 의 키 2개. 순서가 좌/우 표시 순서 "targets": { "nginx": { "label": "Nginx", "baseUrl": "https://...", "headers": { "Host": "{{VHOST}}" } }, "envoy": { "label": "Envoy", "baseUrl": "https://...", "headers": { "Host": "{{VHOST}}" } } } } ``` - `compareTargets`를 생략하면 기존과 동일하게 `["kong", "apim"]`을 사용합니다 — 기존 설정 파일은 전혀 수정할 필요가 없습니다. - 지정할 경우 **정확히 2개의 문자열**이어야 하며, 두 키 모두 `targets`에 존재해야 합니다. 그렇지 않으면 설정 오류로 거부됩니다. - `targets..label`을 생략하면 화면에는 `` 값 자체가 표시됩니다(예: `Kong`처럼 고정된 이름이 아니라 실제로 지정한 키 이름). ## 단일 타깃 모드 (compareMode) Kong과 APIM 양쪽을 호출해 서로 비교하는 대신, **한 곳만 호출해 설정에 적어둔 기대값과 비교**할 수 있습니다. 상대 게이트웨이가 아직 없거나, 특정 응답이 고정값이어야 함을 검증할 때 씁니다. 모드는 **설정 파일 단위**로 정합니다. 툴바의 설정 파일 드롭다운으로 갈아탑니다. ```jsonc // config-single.json { "compareMode": "single", // 생략하면 "dual" (= 기존 동작) "singleTarget": "kong", // targets 의 키. "apim" 도 가능 "targets": { "kong": { "label": "Kong", "baseUrl": "https://...", "headers": { "Host": "{{VHOST}}" } } }, "include": ["apis/search.json"] } ``` - `compareMode`가 `"single"`이면 `singleTarget`이 **필수**입니다. 빠뜨리면 설정 오류로 거부됩니다 — 조용히 `kong`으로 넘어가면 의도하지 않은 대상을 검증하게 되기 때문입니다. - 단일 모드에서는 `targets`에 대상 하나만 두어도 됩니다. - 상대 타깃을 향한 요청은 코드 경로 자체에 존재하지 않으므로 **실수로도 호출되지 않습니다.** ### expected — 기대값 API마다 `expected` 블록으로 기대값을 적습니다. ```jsonc { "name": "경로검색", "method": "GET", "path": "/giop/rest/etc/RouteSearch.xml", "ignorePatterns": ["[^<]*"], "expected": { "status": 200, "headers": { "Content-Type": "text/xml" }, "body": "OK" // 짧으면 인라인 // "bodyFile": "expected/RouteSearch.xml" // 길면 파일로 } } ``` | 필드 | 생략 시 | 설명 | |------|---------|------| | `status` | 상태 코드 비교 생략 | 숫자 | | `headers` | 헤더 비교 생략 | 명시한 헤더만 완전 일치 검사. 이름은 대소문자 무시 | | `body` | 본문 비교 생략 | **문자열만** 허용. 객체는 지원하지 않습니다 | | `bodyFile` | — | 도구 디렉터리 기준 **상대 경로**. `body`가 함께 있으면 `body`가 이깁니다(경고 표시) | - `status`/`headers`/`body` 중 **하나도 지정하지 않으면** 비교할 것이 없으므로 `ERROR`가 됩니다. - `expected` 자체가 없는 API는 `ERROR / 기대값(expected)이 정의되지 않았습니다`로 표시됩니다. 다른 API 실행에는 영향이 없습니다. - `expected.body` 비교에도 **`ignorePatterns`가 양쪽에 동일하게 적용됩니다.** 위 예처럼 매 호출 달라지는 `` 값을 마스킹할 수 있습니다. - 단일 모드에서는 전역 `compareHeaders`를 **적용하지 않습니다.** 비교 상대가 되는 응답이 없으므로 `expected.headers`만 씁니다. **`bodyFile` 제약** - 도구 디렉터리 밖을 가리키는 경로(`../`)와 절대 경로(`C:\...`)는 **거부**됩니다. - 최대 4MB. - **UTF-8로 저장하세요.** UTF-8 BOM은 자동으로 제거하지만, UTF-16 파일은 명확한 오류로 거부합니다. - **줄바꿈은 정규화하지 않습니다.** 실제 응답이 LF인데 기대값 파일이 CRLF면 모든 줄이 다르게 나옵니다. 기대값 파일은 **LF로 저장**하세요. - 경로가 잘못되었거나 파일이 없으면 **그 API만** `ERROR`가 되고, 나머지 목록은 정상적으로 뜹니다. 상세 화면은 왼쪽이 실제 응답, 오른쪽이 기대값입니다. 기대값 쪽에는 요청 정보가 없으므로 그 영역이 생략되고, `bodyFile`을 썼으면 출처 파일명이 표시됩니다. ## debug 로그 `/proxy` 요청이 실제로 들어오고 끝났는지 콘솔에서 확인하는 용도입니다. ``` java -jar ApiDiff.jar --debug ``` 설정 파일로도 켤 수 있습니다. ```jsonc { "debug": true } ``` - **`--debug` 옵션이 우선입니다.** 옵션을 주면 설정이 `false`여도 켜집니다. - 설정으로 켜는 방식은 브라우저가 `/config`를 읽은 **이후부터** 적용됩니다. 처음부터 남기려면 `--debug`를 쓰세요. | 카테고리 | 내용 | |----------|------| | `RECV` | `/proxy` 요청 접수 — 순번, 호출할 메서드/URL | | `DONE` | `/proxy` 응답 완료 | | `ERROR` | `/proxy` 처리 중 예외 발생 | 출력 예: ``` [RECV] #1 POST /proxy -> GET https://10.0.0.1:10001/giop/rest/etc/RouteSearch.xml [DONE] #1 /proxy 200 ``` 요청/응답 헤더 전문이나 구간별 소요 시간(hdr/body 분리, SLOW 경고 등)은 출력하지 않는 **최소 로그**입니다. 요청이 어디서 멈췄는지 더 깊이 보려면 `timeoutMs`를 낮춰 문제 지점을 좁히거나(아래 [실행 중 멈춘 것 같을 때](#실행-중-멈춘-것-같을-때) 참고), OS 레벨 네트워크 도구(예: `netstat`, 프록시 로그)를 함께 활용하세요. **비밀값 마스킹** - `variables`에서 `"secret": true`로 표시한 값은 로그에 그대로 남지 않고 마스킹됩니다. - 요청/응답 헤더 자체를 로그에 남기지 않으므로, `Authorization` 등 민감 헤더 값은 애초에 콘솔에 출력되지 않습니다. **주의** - Windows 콘솔에서 **마우스로 텍스트를 선택하면(QuickEdit) 콘솔 출력이 멈추고, 서버가 멈춘 것처럼 보입니다.** `Esc`로 선택을 해제하세요. 자주 겪는다면 콘솔 속성에서 QuickEdit를 끄십시오. ## 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)>[^<]*` | | 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이 자꾸 떠서" 넓은 패턴을 추가하는 것은 검증을 무의미하게 만듭니다. 적용된 패턴은 항상 결과 화면에 표시되므로 나중에 무엇을 제외했는지 확인할 수 있습니다. ## 실행 중 멈춘 것 같을 때 이 도구에는 **어떤 경우에도 요청이 무한정 매달려 있지 않도록** 3중 상한이 걸려 있습니다. | 상한 | 값 | 담당 | |------|-----|------| | 요청 전체 | `timeoutMs` (+ 마지막 읽기 1회 대기) | 서버가 연결·요청 전송·응답 대기·본문 읽기 전 구간을 감시 | | 개별 읽기 | `min(timeoutMs, 15000)` | 응답이 아주 느리게 흘러오는 경우를 끊음 | | 브라우저 | `timeoutMs + 5000` | 서버가 아예 답을 못 주는 경우의 최후 방어선 | 어느 쪽이 먼저 걸리든 그 항목은 `ERROR`로 확정되고 **다음 API로 진행합니다.** 한 API가 실패해도 전체 실행은 끝까지 갑니다. 그래도 진행이 멈춘 것처럼 보이면 순서대로 확인하세요. 1. **콘솔에서 텍스트를 선택하고 있지 않은지** — Windows 콘솔에서 드래그하면 출력이 멈추고 서버도 함께 멈춥니다. `Esc`를 누르세요. 2. **`--debug`로 다시 실행** — `RECV`/`DONE`/`ERROR` 로그로 요청이 실제로 들어오고 끝났는지 확인합니다 (구간별 시간까지는 나오지 않습니다). 3. **`timeoutMs`를 낮춰서 확인** — 5000 정도로 두면 문제 지점이 빨리 드러납니다. 4. **`/ping` 으로 서버 생존 확인** — 브라우저에서 `http://localhost:8080/ping`을 열어 `{"ok":true}`가 즉시 오는지 봅니다. 한 API가 멈춰 있어도 이 응답은 즉시 와야 정상입니다. ## 알려진 제약 - 자체서명·만료 인증서를 무조건 허용합니다(`curl -k` 상당). 검증 도구 용도로만 사용하세요. - 내장 서버는 전체 요청(정적 파일, `/config`, `/proxy` 등)을 최대 8개 스레드로 동시 처리합니다. 응답 시간 표시는 각 호출별로 개별 측정됩니다. - 본문이 1200행을 넘으면 라인 diff 대신 첫 차이 위치와 그 주변 문자열만 표시합니다. - 응답 본문은 16MB까지만 읽습니다. 그 이상은 읽기를 중단하고 수신한 부분의 SHA-256으로만 비교하며, `ignorePatterns`는 적용되지 않습니다. - `expected.body`는 문자열만 지원합니다. 객체를 쓰면 설정 오류로 거부됩니다. - 기대값 파일(`expected.bodyFile`)의 줄바꿈은 정규화하지 않습니다. LF로 저장하세요. - debug 로그는 요청 접수(`RECV`)·완료(`DONE`)·오류(`ERROR`)만 남기는 최소 로그입니다. 헤더 전문이나 구간별 소요 시간은 출력하지 않습니다. - include는 1단계만 해석합니다. 그룹 파일이 또 다른 파일을 include할 수 없습니다. - 그룹 파일은 `apis`만 담습니다. 그룹 파일에 `variables`나 `targets`를 써도 무시됩니다. - 요청 간격은 API 사이에만 적용되며, 한 API의 Kong·APIM 두 호출 사이에는 적용되지 않습니다. - 이미지·바이너리 응답은 8MB까지만 화면에 표시합니다. 그 이상은 SHA-256으로만 비교합니다. - 설정 파일 이름에 제어 문자가 들어간 요청은 400이 아닌 500으로 응답합니다. 파일은 서빙되지 않으므로 동작·보안에는 영향이 없습니다. - Java 17 이상 런타임이 필요합니다. `apiDiff.bat`은 인자를 전달하지 않으므로, 옵션이 필요하면 콘솔에서 `java -jar ApiDiff.jar ...`를 직접 실행하세요.