JSON 비교

입력한 내용은 저장, 기록, 공유되지 않습니다.

두 JSON을 붙여넣으면 추가·삭제·변경된 값을 경로별로 보여줍니다. 키 순서는 무시되므로 실제 차이만 보고됩니다.

사용 방법

  1. 왼쪽에 원본 JSON을 붙여넣습니다.
  2. 오른쪽에 비교할 JSON을 붙여넣습니다.
  3. 차이가 경로와 함께 아래에 표시됩니다.

비교 방식

두 문서를 모두 파싱한 뒤 루트부터 나란히 따라가며 양쪽의 같은 경로에 있는 것을 비교합니다. 객체는 키로 짝을 지으므로 키 순서를 바꿔도 달라지는 것이 없고, 배열은 인덱스로 짝을 짓습니다. 모든 차이는 세 가지 중 하나로 보고됩니다. 추가는 해당 경로가 오른쪽에만 있다는 뜻이고, 삭제는 왼쪽에만 있다는 뜻이며, 변경은 양쪽 모두 그 경로에 값이 있지만 값이 다르다는 뜻입니다.

타입이 바뀐 것도 변경으로 간주되며, 이런 경우가 보이면 자세히 살펴볼 가치가 있습니다. 숫자 42였던 필드가 문자열 "42"로 바뀌면 관대한 클라이언트에서는 대개 계속 동작하다가, 그 값으로 계산하는 하위 처리에서 실패합니다. 객체였던 필드가 객체 하나짜리 배열로 바뀐 경우도 마찬가지인데, 조건에 따라 응답을 조립하는 코드에서 흔히 일어나는 사고입니다.

배열을 인덱스로 비교하는 이유

결과에 가장 큰 영향을 주는 설계 결정이므로 분명히 말해 두겠습니다. 요소 200개짜리 배열의 맨 앞에 요소 하나를 넣으면, 삽입 지점 이후의 모든 요소가 다른 인덱스로 옮겨지므로 인덱스 기반 비교는 변경 200건을 보고합니다. 잘못된 것은 없습니다. 비교 결과가 당신이 생각한 것과 다른 질문에 답하고 있을 뿐입니다.

대안은 최장 공통 부분 수열을 찾아 삽입으로 보고하는 방식으로, 텍스트의 줄 단위 diff가 이렇게 합니다. 하지만 JSON에서는 이것이 딱히 더 낫지 않습니다. 목록 속 두 객체가 언제 같은 객체인지 판단해야 하는데, 그러려면 사용자가 지정하지 않았고 도구도 추측할 수 없는 키가 필요합니다. 잘못 추측하면 확신에 차 있지만 오해를 부르는 답을 내놓게 됩니다. 인덱스 비교는 투박하지만 존재하지 않는 대응 관계를 지어내는 일은 결코 없습니다.

실제로 목록 순서가 바뀐 경우라면 비교하기 전에 양쪽을 식별자 기준으로 정렬하세요.

null, 없는 키, 경로 읽는 법

한쪽에는 값이 null로 존재하고 다른 쪽에는 아예 없는 키는 변경이 아니라 추가 또는 삭제로 보고됩니다. 둘은 서로 다른 상태이고 코드도 다르게 처리하기 때문입니다. 키가 없다는 것은 대개 그 필드가 한 번도 설정되지 않았다는 뜻이고, null은 대개 일부러 '값 없음'으로 설정했다는 뜻입니다. 하나를 다른 하나의 의미로 쓰는 API는 버그를 낳으며, 실제로 어느 쪽을 보내고 있는지 보여 주는 것이 바로 이 도구입니다.

경로는 루트에서 출발하는 길로 읽습니다. user.roles[2]는 user 객체 안에 있는 roles 배열의 세 번째 요소입니다. 이 문법은 JavaScript에서 그 값에 접근할 때 쓰는 것과 일부러 똑같이 맞췄으며, JSONPath 표현식과도 충분히 가까워 JSONPath를 받는 대부분의 도구에 그대로 붙여 넣을 수 있습니다.

진짜 변경만 골라내는 회귀 점검 흐름

JSON 비교의 가장 흔한 용도는 배포가 바뀌지 말아야 할 것을 바꾸지 않았는지 확인하는 일입니다. 현재 버전의 응답을 받아 두고, 후보 버전에서 다시 받아서 비교합니다. 어려운 부분은 비교 자체가 아니라 언제나 잡음입니다. 응답에는 호출할 때마다 정당하게 달라지는 필드가 대개 섞여 있기 때문입니다.

비교하기 전에 그런 필드는 제거하거나 정규화합니다. 자동 생성된 식별자, 타임스탬프, request id, 만료 시각이 담긴 서명 URL, 소요 시간을 알려 주는 값 등입니다. 문서를 순회하면서 이런 필드를 고정된 자리표시자로 바꿔 주는 짧은 스크립트 하나면, 차이 200개짜리 비교가 정말 중요한 하나짜리 비교로 줄어듭니다.

API가 순서를 보장하지 않는 곳에도 같은 처리를 합니다. 태그 목록이 호출할 때마다 다른 순서로 돌아온다면 먼저 양쪽을 안정적인 키로 정렬하십시오. 그렇지 않으면 인덱스 기반 비교가 모든 원소를 변경으로 보고하고, 유일하게 진짜인 차이 하나가 그 속에 묻혀 버립니다.

같음, 동일함, 그리고 동등함

두 문서가 '같다'는 말에는 여러 의미가 있고, 도구는 그중 하나를 골라야 합니다. 바이트 단위 일치는 가장 엄격하지만 가장 쓸모가 적습니다. 서식만 다시 잡아도 깨지기 때문입니다. 공백이나 키 순서와 무관하게 같은 키가 같은 값을 가진다는 구조적 일치가 바로 이 비교가 수행하는 것입니다. 세 번째인 의미적 동등성은 스키마에 달린 문제입니다.

구조적 비교가 껄끄러워지는 지점은 숫자입니다. JSON은 정수와 부동소수점을 구분하지 않아서 파서에게 1, 1.0, 1e0은 모두 같은 값입니다. 생산자가 어느 쪽을 내보내는지는 라이브러리 버전에 따라 달라질 수 있고, 의미상 아무것도 바뀌지 않았는데 표기만 바뀝니다. 끝자리 0이 왕복 변환에서 사라지는 것도 같은 이유입니다.

문자열은 눈으로 보기에 똑같아도 바이트가 다를 수 있습니다. 유니코드는 같은 문자를 한 가지 이상의 방식으로 표현할 수 있기 때문입니다. 악센트가 붙은 글자는 하나의 코드 포인트일 수도 있고, 기본 글자에 결합 기호가 더해진 형태일 수도 있습니다. 비교 전에 NFC로 정규화하면 이런 종류의 거짓 차이가 사라지며, 텍스트가 두 곳 이상에서 들어올 때는 언제나 해 둘 만합니다.

비교가 맞지 않는 도구일 때

두 사례를 비교하면 그 두 사례에 대해서만 알 수 있습니다. 응답이 어떤 규칙을 지키는지 확인하는 것은 다른 일이고, 그 일의 도구는 스키마입니다. 문서에는 JSON Schema, 엔드포인트에는 OpenAPI가 있습니다. 스키마는 id가 정수이고 email이 특정 패턴에 맞는다고 선언하며, 우연히 잡아 둔 한 쌍이 아니라 모든 응답에 대해 성립합니다.

실제로는 둘을 함께 씁니다. 지속적 통합에서의 스키마 검증은 타입이 바뀐 필드나 사라진 필수 속성 같은 구조적 이탈을 잡아내고, 비교는 조사 과정에서 '이 두 페이로드 사이에 무엇이 바뀌었는가'라는 더 좁은 질문에 답합니다.

컨트랙트 테스트는 여기서 한 걸음 더 나아가, 소비자가 자신이 의존하는 내용을 공개하도록 합니다. 그러면 생산자의 파이프라인은 단지 예전 샘플과 다르다는 이유가 아니라, 어떤 변경이 실제 클라이언트를 망가뜨릴 때 실패하게 됩니다.

경로를 읽고 그 자리로 이동하기

여기서 알려 주는 경로는 루트에서 출발하는 길처럼 읽힙니다. user.roles[2]는 user 객체 안 roles 배열의 세 번째 원소입니다. 이는 그 값에 닿기 위해 JavaScript에서 실제로 입력할 표현식과 일부러 같게 맞춘 것이라, 문서를 변수에 담아 두었다면 경로를 그대로 콘솔에 붙여 넣을 수 있습니다.

같은 형태는 대부분의 로그·API 도구가 받아들이는 질의 언어인 JSONPath와도 가깝습니다. JSONPath에서는 표현식이 달러 기호로 시작해 $.user.roles[2]가 됩니다. 명령줄 처리기인 jq는 달러 기호 없이 비슷한 표기를 쓰므로 .user.roles[2]로 파일이나 스트림에서 같은 값을 뽑아냅니다.

아주 큰 문서에서는 비교 결과 자체보다 그 경로가 쓸모 있는 출력입니다. 차이가 어디에 있는지만 알면 jq나 HTTP 클라이언트의 필터로 양쪽에서 그 하위 트리만 끌어올 수 있고, 문서 전체를 훑는 어떤 방식보다 읽기가 빠릅니다.

다 읽을 수 없을 만큼 큰 문서 비교하기

수 메가바이트짜리 응답은 분석 데이터 내보내기나 대량 조회 엔드포인트에서 흔하고, 그것을 통째로 비교하면 아무도 쓸 수 없는 결과가 나옵니다. 생산적인 방법은 비교하기 전에 줄이는 것입니다. 관심 있는 하위 트리를 고르고, 논의 중인 필드만 추려 내고, 컬렉션은 식별자로 정렬합니다.

개수를 세어 보는 것이 유용한 첫 단계입니다. 한쪽에 4,812건이 있고 다른 쪽에 4,811건이 있다면 궁금한 것은 어떤 레코드가 빠졌는가이고, 그것은 문서 비교가 아니라 식별자의 집합 비교입니다. 정렬된 id 목록 두 개와 행 단위 diff면 몇 초 만에 답이 나옵니다.

페이로드가 정말 크고 비교를 반복해서 돌려야 한다면, 브라우저보다 명령줄 파이프라인이 낫습니다. jq로 두 파일을 정렬된 한 줄짜리 압축 형태로 정규화한 다음 diff에 넘기면 됩니다. 어느 쪽이든 모든 처리는 로컬에서 이뤄지지만, 명령줄에는 반응성을 유지해야 할 페이지가 없습니다.

자주 묻는 질문

키 순서가 비교 결과에 영향을 주나요?
아닙니다. 객체는 위치가 아니라 키를 기준으로 비교하므로 순서를 바꿔도 차이가 생기지 않습니다. 배열은 순서가 의미를 가지므로 인덱스로 비교합니다.
데이터가 서버로 전송되나요?
아닙니다. 비교는 브라우저의 JavaScript로만 실행됩니다. 두 데이터 모두 기기를 벗어나지 않으므로 운영 환경의 API 응답도 안전하게 비교할 수 있습니다.
user.roles[2] 같은 경로는 무슨 뜻인가요?
차이가 발생한 위치를 나타냅니다. user 객체 안 roles 배열의 세 번째 요소라는 뜻입니다. 점은 객체 키를, 대괄호는 배열 인덱스를 구분합니다.
null과 키가 없는 것은 어떻게 다른가요?
구분해서 표시합니다. 한쪽에는 키가 있고 값이 null이며 다른 쪽에는 키 자체가 없다면 변경이 아니라 추가 또는 삭제로 보고됩니다.

마지막 업데이트:

문제 신고

진행 단계, 예상 결과, 실제 결과를 적어 주세요. 비밀 정보와 개인 정보는 삭제해 주세요.

관련 도구