XML 포맷터
브라우저에서 정렬되며 전송되지 않습니다. 텍스트와 요소가 섞인 내용은 줄바꿈과 들여쓰기를 넣으면 문서의 의미가 바뀌므로 한 줄로 유지합니다. 속성 순서와 따옴표도 그대로 두며, XML 선언을 임의로 추가하지 않습니다.
입력한 내용은 저장, 기록, 공유되지 않습니다.
XML을 붙여넣어 읽기 좋게 들여쓰거나 한 줄로 압축하고, 적격 형식인지 확인하세요. 오류는 정확한 줄과 열을 가리킵니다. 모든 처리가 브라우저에서 이루어지므로 아무것도 업로드되지 않습니다.
사용 방법
- XML을 입력란에 붙여넣습니다.
- 정렬 또는 압축을 고르고, 원하면 들여쓰기 폭을 설정합니다.
- 결과를 복사하거나, 오류의 줄과 열을 확인합니다.
잘 짜인 XML과 유효한 XML은 다르다
XML에는 서로 다른 두 가지 올바름의 기준이 있고, 이 둘은 자주 혼동됩니다. 잘 짜인(well-formed) 문서란 문법을 지킨 문서입니다. 루트 요소가 하나뿐이고, 모든 태그가 짝이 맞는 닫는 태그로 닫히며, 속성 값에 따옴표가 있고, 주석과 CDATA가 끝나 있으며, 루트 바깥에 불필요한 텍스트가 없는 상태를 말합니다. 문서만 있으면 확인할 수 있고, 이 도구가 알려 주는 것도 바로 이것입니다.
유효한(valid) 문서는 여기에 더해 DTD, XSD, RelaxNG 같은 스키마를 따르는 문서입니다. 스키마는 어떤 요소가 어떤 순서로 몇 번 나올 수 있는지, 속성 값이 어떤 형태여야 하는지를 정합니다. 스키마 없이는 이 검사가 불가능하며, 문법적으로는 완벽하게 잘 짜였더라도 따른다고 주장하는 스키마에 비추면 엉터리인 문서도 얼마든지 있습니다.
네임스페이스는 이 둘과 조금 다른 문제입니다. 대응하는 xmlns 선언 없이 접두사를 쓰면 잘 짜임 오류가 아니라 네임스페이스 오류가 됩니다. 그래서 문법 검사는 통과하더라도 네임스페이스를 인식하는 파서에서는 거부될 수 있습니다. SOAP나 Office 문서가 깔끔하게 포맷되는데도 거부된다면 선언되지 않은 접두사부터 확인해 보세요.
공백이 데이터일 때가 있다
포맷터의 모든 동작을 좌우하는 규칙입니다. 요소와 요소 사이의 공백은 거의 항상 레이아웃일 뿐이라 다른 레이아웃으로 바꿔도 달라지는 것이 없습니다. 하지만 혼합 콘텐츠, 즉 문단 안에 굵은 단어가 들어 있는 것처럼 텍스트와 요소가 나란히 있는 곳의 공백은 텍스트의 일부이므로, 줄을 바꾸면 문서 자체가 달라집니다.
그래서 혼합 콘텐츠를 담은 요소는 들여쓰기된 문서 한가운데에 긴 줄이 남더라도 쓰인 그대로 둡니다. 대부분의 포맷터는 반대로 모든 것을 들여쓰고 Hello와 <b>there</b> 사이에 줄바꿈을 넣어, 화면에 표시하면 원래 없던 틈이 생기는 문서를 만듭니다. 보기에는 깔끔하지만 틀린 결과입니다.
명세에는 이를 명시적으로 선언하는 xml:space="preserve"가 있으며, 텍스트 내용이 중요한 요소에는 붙여 둘 만합니다. 이 설정은 다시 지정될 때까지 하위 요소에 상속된다는 점에 주의하세요. 루트에 한 번만 설정하고 놀라는 경우가 많습니다.
XML 문서를 실제로 망가뜨리는 것들
단연 가장 흔한 원인은 이스케이프하지 않은 앰퍼샌드(&)입니다. XML에서 &는 엔터티 참조의 시작이므로, URL 쿼리 문자열이나 회사 이름에 그대로 들어 있으면 문서가 잘 짜이지 않은 상태가 됩니다. 반드시 &로 써야 하고, 텍스트 안의 작다 기호도 <로 써야 합니다. 미리 정의된 엔터티는 amp, lt, gt, quot, apos 다섯 개뿐이며, 나머지는 선언하거나 숫자 문자 참조로 써야 합니다.
그다음은 인코딩 불일치입니다. 파일 맨 위의 선언은 바이트가 어떤 인코딩인지 알려 주는데, 나중에 다른 인코딩으로 저장되면 파서는 멀티바이트 문자를 깨진 글자로 읽거나 아예 실패합니다. 선언 앞의 BOM 때문에 실패하는 파서도 있습니다. 또한 XML 1.0에서는 일부 제어 문자를 이스케이프해도 전혀 쓸 수 없습니다. 텍스트 필드에 섞여 들어간 바이너리 데이터는 인코딩으로 살릴 수 없고, 지우거나 Base64로 인코딩해야 합니다.
마지막은 HTML 습관입니다. 따옴표 없는 속성 값, <br>처럼 닫지 않은 태그, 중복 속성은 HTML에서는 괜찮지만 XML에서는 치명적입니다. HTML에서 온 문서라면 이것부터 살펴보세요.
엔터티, 그리고 신뢰할 수 없는 XML을 파싱할 때
XML에서는 문서가 DTD 안에서 스스로 엔터티를 선언할 수 있고, 파일 경로나 URL을 가리키는 외부 엔터티도 선언할 수 있습니다. 이는 잘 알려진 두 가지 공격의 바탕이 되므로, XML을 작성하지 않고 읽기만 하더라도 알아 둘 가치가 있습니다.
XML 외부 엔터티(XXE) 공격은 로컬 파일을 가리키는 엔터티를 선언해 서버가 그 파일을 읽고 내용을 응답에 담아 보내게 만듭니다. 업로드된 XML을 기본 설정으로 파싱하는 시스템에서 설정 파일과 인증 정보를 빼내는 확실한 방법으로 오랫동안 쓰였습니다. Billion Laughs 공격은 엔터티를 중첩해 각 엔터티가 다음 엔터티 여러 개로 펼쳐지게 하여, 몇 킬로바이트의 입력을 메모리에서 수 기가바이트로 불립니다.
두 경우 모두 방어책은 파서에서 DTD 처리와 외부 엔터티 해석을 끄는 것입니다. 요즘 주요 라이브러리는 대부분 기본값이 안전하지만, 그렇지 않은 오래된 코드도 여전히 많습니다. 이 페이지는 외부의 어떤 것도 해석하지 않습니다. 문서를 텍스트로 읽을 뿐, 문서가 참조하는 대상을 가져오지 않습니다.
네임스페이스, 접두사는 이름이 아니다
네임스페이스는 접두사를 URI에 묶어 주며, 요소를 실제로 식별하는 것은 URI입니다. 접두사는 얼마든지 바꿔 쓸 수 있는 로컬 약칭일 뿐입니다. 같은 URI에 대해 각각 soap: 와 s: 를 쓰는 두 문서는 동등하고, 둘 다 ns1: 를 쓰지만 URI가 다른 두 문서는 동등하지 않습니다. URI가 아니라 접두사로 매칭하는 코드는 누군가 문서를 다시 포맷하기 전까지만 잘 동작합니다.
xmlns 로 선언한 기본 네임스페이스는 요소에는 적용되지만 속성에는 적용되지 않습니다. 대부분이 잘못 알고 있는 규칙이 바로 이것입니다. 접두사가 없는 속성은 바깥 요소가 무엇을 선언했든 상관없이 어떤 네임스페이스에도 속하지 않습니다. xsi:type 과 xml:lang 을 언제나 접두사와 함께 쓰는 이유가 여기에 있습니다.
겉보기에 잘 구성된 문서를 파서가 거부한다면 대개 선언되지 않은 접두사가 원인입니다. 접두사 바인딩은 문법 규칙이 아니라 네임스페이스 규칙이므로 포맷팅 자체는 성공하고, 문제는 그 문서를 소비하는 쪽에서 뒤늦게 드러납니다.
스키마 검증이 실제로 주는 것
잘 구성됨은 문법의 문제이고, 유효함은 어떤 문법 규칙에 부합하는지의 문제입니다. DTD는 최초의 방식으로 기능은 제한적이지만 엔티티 선언을 위해 지금도 쓰입니다. 주류는 XSD로, 그 자체가 XML로 작성되고 데이터 타입 표현력이 뛰어나며, 한 편을 읽어 내는 것만으로도 기술이 필요할 만큼 장황합니다. RelaxNG는 훨씬 쓰기 편하지만 지원이 넓지 않다는, 늘 보던 그 맞바꿈이 있습니다.
스키마가 가져다주는 것은 코드가 데이터를 보기 전에 경계에서 기계적으로 검사할 수 있는 계약입니다. 필수 요소가 있는지, 숫자가 와야 할 자리에 숫자가 있는지, 열거값이 허용된 집합 안에 있는지 확인합니다. 파트너사와의 연동에서는 양쪽이 합의한 명세가 스키마뿐인 경우도 많습니다.
파이프라인에서의 검증은 명령 한 줄이면 됩니다. xmllint --schema schema.xsd file.xml --noout 을 실행하면 오류 메시지가 줄 번호와 요소를 짚어 줍니다. 애플리케이션 코드 안이 아니라 경계에서 검사하는 이유는, 스키마 실패는 이유가 분명한 깔끔한 거부인 반면 같은 문제를 세 겹 안쪽에서 발견하면 스택 트레이스가 되기 때문입니다.
XPath로 데이터 꺼내기
XPath는 XML을 위한 질의 언어이며, 누구에게든 한 시간을 투자할 가치가 있습니다. //book/title 은 문서 어디에 있든 book 아래의 모든 title 요소를 선택하고, //book[@id='b1'] 은 속성으로 선택하며, //book[price>30]/title 은 자식 요소의 값으로 걸러냅니다. XSLT, 자바의 XML 도구, 파이썬의 lxml, 그리고 대부분의 브라우저 자동화 도구 밑에 깔린 주소 지정 언어입니다.
까다로워지는 지점은 네임스페이스입니다. 식은 자신이 쓰는 접두사를 직접 바인딩해야 하고, 기본 네임스페이스가 선언된 문서에서는 요소에 접두사가 없어 보여도 실제로는 그 네임스페이스에 속합니다. //book 이 아무것도 찾지 못하고, SOAP 응답에 처음 써 본 XPath가 누구에게나 빈 결과로 돌아오는 이유입니다. 바인딩을 등록하는 방법은 도구마다 다르고, local-name() 함수가 투박한 비상구 역할을 합니다.
그 위에 세워진 변환 언어가 XSLT이며, 절차적 코드를 쓰지 않고 한 XML 어휘를 다른 어휘로, 또는 HTML로 바꾸는 가장 실용적인 방법이라는 자리는 지금도 그대로입니다. 어렵다는 평판은 문법보다 함수형 스타일에서 비롯된 것입니다.
XML은 여전히 어디에나 있다
오피스 문서는 XML입니다. .docx 나 .xlsx 는 XML 조각들이 담긴 디렉터리를 품은 zip 아카이브이며, 손상된 문서를 압축 해제해 파일 하나만 고쳐서 살려낼 수 있는 경우가 있는 것도 그 때문입니다. SVG도 XML이라 아이콘을 텍스트 편집기로 고치거나 템플릿으로 생성할 수 있습니다. RSS와 Atom 피드도 XML이고, 이 사이트가 공개하는 사이트맵도 마찬가지입니다.
기업과 정부의 시스템 연동에서 SOAP은 결코 죽지 않았습니다. 은행, 보험, HL7을 통한 의료, 그리고 대다수 국가의 관세·세무 시스템이 스키마를 갖춘, 그리고 흔히 서명된 XML 위에서 돌아갑니다. XML Signature와 XML Encryption을 쓰면 큰 문서 안의 단일 요소만 골라 서명할 수 있는데, JSON에는 이에 대응하는 표준이 없습니다.
안드로이드 레이아웃, Maven 빌드, .NET 설정, 그리고 헤아릴 수 없이 많은 CI 기술 파일도 XML입니다. 이 형식은 유행이 지난 것이지 쓸모가 없어진 것은 아니며, 편하게 읽어 내는 능력은 오래가는 기술입니다.
내가 쓰지 않은 XML을 파싱할 때
많은 XML 라이브러리의 기본 설정은 신뢰할 수 없는 입력에 대해 안전하지 않으며, 원인은 엔티티 메커니즘입니다. 외부 엔티티는 문서가 로컬 파일이나 URL을 참조하도록 해 주므로, 업로드된 XML을 기본 설정으로 파싱하는 서버는 /etc/passwd 를 읽어 그대로 돌려주도록 유도될 수 있습니다. 이것이 XXE 공격이며, 정리되어 알려진 지 여러 해가 지난 지금도 운영 시스템에서 발견됩니다.
다른 한 면은 확장입니다. billion laughs 공격은 엔티티를 중첩해 수 킬로바이트의 입력을 메모리에서 기가바이트 규모로 부풀려 프로세스를 죽입니다. 두 공격 모두 특별한 것이 필요 없습니다. 명세가 정의한 기능을 쓰는 평범한 XML일 뿐입니다.
방어는 대부분의 라이브러리에서 한 줄이면 됩니다. DTD 처리와 외부 엔티티 해석을 끄는 것입니다. 기본값은 언어와 버전마다 다르므로 그러려니 하지 말고 직접 확인해 보는 편이 좋습니다. 이 페이지는 외부의 무엇도 해석하지 않습니다. 문서를 텍스트로 읽을 뿐, 문서가 참조하는 것을 가져오는 일은 절대 없습니다.
자주 묻는 질문
- 왜 일부 XML은 한 줄로 남아 있나요?
- "Hello <b>there</b>."처럼 텍스트와 요소가 함께 있는 혼합 콘텐츠이기 때문입니다. 여기에 들여쓰기를 넣으면 줄바꿈과 공백이 텍스트 자체에 삽입되어, 화면에는 "Hello there ."처럼 불필요한 간격이 생깁니다. 그래서 해당 요소는 쓰인 그대로 둡니다. 대부분의 XML 포맷터는 그래도 다시 흘려 넣어 문서를 조용히 바꿔 버립니다.
- 정렬이 문서의 의미를 바꾸는 경우가 있나요?
- 없으며, 그것이 위 규칙의 목적입니다. 요소 사이의 공백은 레이아웃이라 교체되고, 혼합 콘텐츠 안의 공백은 데이터라 보존됩니다. 속성 순서와 따옴표 방식은 쓰인 그대로 유지되고, 주석과 CDATA는 손대지 않고 복사되며, 원래 없던 XML 선언을 덧붙이지도 않습니다.
- "적격 형식"은 정확히 무엇을 검사하나요?
- 모든 요소가 짝이 맞는 태그로 닫혔는지, 속성값에 따옴표가 있는지, 주석과 CDATA 블록이 끝났는지, 그리고 루트 요소가 정확히 하나이고 그 바깥에 떠도는 텍스트가 없는지를 봅니다. DTD나 스키마에 대한 유효성 검사는 하지 않습니다. 그러려면 스키마 자체가 필요하고, 그것은 다른 도구의 몫입니다.
- 제 속성은 왜 거부되었나요?
- XML은 모든 속성값에 따옴표를 요구합니다. HTML에서는 disabled=true나 width=100처럼 써도 브라우저가 처리하지만 XML은 그렇지 않고, 이후의 모든 파서가 거부합니다. 문서가 HTML에서 왔다면 대개 여기가 가장 먼저 고칠 곳입니다.
- 압축과 정렬은 어떻게 다른가요?
- 같은 문서에 대한 반대 방향의 작업입니다. 정렬은 읽을 수 있도록 요소 사이에 줄바꿈과 들여쓰기를 넣고, 압축은 그것을 모두 없애 전송에 필요한 최소 크기로 만듭니다. 둘 다 혼합 콘텐츠는 건드리지 않으므로, 하나를 한 뒤 다른 하나를 하면 원래 상태로 돌아옵니다.
- 제 XML이 어딘가로 업로드되나요?
- 아닙니다. 파싱과 정렬이 브라우저에서 이루어지므로 설정 파일, SOAP 페이로드, 내보낸 덤프처럼 내부 호스트명과 식별자, 개인정보를 자주 담고 있는 데이터가 기기를 벗어나지 않습니다.
마지막 업데이트:
진행 단계, 예상 결과, 실제 결과를 적어 주세요. 비밀 정보와 개인 정보는 삭제해 주세요.
