Posts

지워지지 않은 백스페이스

목차5

블로그를 새로 열고 검색 등록을 하던 날이었다. 구글 서치 콘솔은 사이트맵을 순순히 받아줬고, 네이버 서치어드바이저도 사이트맵까지는 문제가 없었다. 마지막으로 RSS 주소를 넣고 확인 버튼을 눌렀더니, 브라우저 상단에 회색 경고창이 하나 떴다.

사이트맵/RSS 형식이 올바르지 않습니다.

이상한 일이었다. 같은 RSS 주소를 브라우저로 열면 글 231편의 제목과 요약이 멀쩡하게 다 보였다. 깨진 글자도 없고, 빠진 태그도 없어 보였다. 필자의 눈에는 아무 문제 없는 파일을 네이버는 형식이 틀렸다고 하고 있었던 것이다.

그래서 이번 포스팅에서는 이 RSS 가 왜 거절당했는지, 그리고 범인이 왜 그렇게 오랫동안 아무에게도 들키지 않았는지를 이야기해보려고 한다. 미리 말해두자면 범인은 글자 하나였다. 그것도 화면에는 보이지 않는 글자.

눈으로는 안 보이는 것

사람 눈으로 안 보인다면 기계에게 물어보는 수밖에 없다. XML 파일이 문법적으로 올바른지 확인할 때 가장 손쉽게 쓰는 도구는 xmllint 다. 배포된 RSS 를 그대로 받아서 넣어봤다.

코드
$ curl -s https://graphite.is/rss.xml -o rss.xml
$ xmllint --noout rss.xml
rss.xml:48: parser error : PCDATA invalid Char value 8

48번째 줄에 값이 8인 글자가 있다는 말이었다. W3C 의 피드 검사기도 같은 줄을 가리키며 "잘 형성되지 않았다(not well-formed)"고 했다. 사람 눈에는 평범한 한국어 문장이 있는 줄이었는데, 기계 둘이 입을 모아 같은 자리를 범인으로 지목한 셈이다.

값이 8인 글자는 ASCII 표의 8번, 백스페이스다. 키보드의 지우기 키를 누를 때 오가는 바로 그 제어 문자다. 원래는 "앞 글자를 지워라"라는 명령을 전달하는 용도인데, 어쩌다 보니 명령이 아니라 글자로서 문장 한가운데에 저장되어 있었던 것이다. (지우라고 만든 글자가 정작 자기 자신은 몇 년째 안 지워지고 있었다는 게 이 이야기의 첫 번째 웃음 포인트다)

화면에는 일곱 글자, 파일에는 여덟 글자. 남은 하나는 투명 망토를 쓰고 있었다

원인을 알고 나니 찾는 건 금방이었다. 티스토리에서 옮겨 온 글 전체를 제어 문자로 검색해보니 네 편에서 하나씩 나왔다. 어떤 글은 "수정 후에는" 과 "포스트를" 사이에, 어떤 글은 커밋 메시지 따옴표 바로 뒤에 숨어 있었다. 정확히 어떻게 들어갔는지는 필자도 모른다. 에디터에서 무언가를 지우던 순간 지우기 명령이 문자로 함께 저장됐을 거라고 짐작할 뿐이다.

브라우저는 왜 아무 말도 하지 않았을까

재미있는 건 이 글자가 몇 년 동안 아무 문제도 일으키지 않았다는 점이다. 티스토리에서도, 새 블로그로 옮긴 뒤에도 글은 멀쩡하게 보였다. 브라우저가 이 글자를 조용히 무시했기 때문이다.

브라우저는 원래 관대한 녀석이다. 닫는 태그를 빼먹어도, 태그 순서가 엉켜도, 이상한 글자가 끼어 있어도 어떻게든 화면을 그려낸다. 지금의 HTML 명세는 아예 잘못된 문서를 만났을 때 어떻게 복구해야 하는지까지 정해두고 있다. 웹이 이렇게 커질 수 있었던 이유 중 하나도 이 관대함이다. 조금 틀린 문서라도 일단 보여주니까, 누구나 대충 만들어서 올릴 수 있었다.

그런데 RSS 는 HTML 이 아니라 XML 이다. 그리고 XML 은 HTML 과 정반대의 성격을 가진 친구다. XML 1.0 명세는 문서에 쓸 수 있는 글자의 범위를 정해두고 있는데, 탭과 줄바꿈과 캐리지 리턴을 제외한 제어 문자는 거기에 들어 있지 않다. 그리고 문서가 이런 규칙을 어기면, 파서는 그 자리에서 처리를 멈춰야 한다고 명세에 적혀 있다. 복구하지 말고, 그냥 멈추라는 것이다.

XML 을 쓰는 사람들은 이걸 반쯤 농담으로 "가혹한 오류 처리"라고 부른다. 글자 하나만 틀려도 문서 전체를 거절하니까. 네이버의 RSS 수집기는 이 명세를 성실하게 따랐을 뿐이다. 231편 중 한 편의 요약에 섞인 글자 하나 때문에, 나머지 230편까지 통째로 거절한 것이다.

관대함과 엄격함 사이

네트워크 프로토콜의 세계에는 오래된 격언이 하나 있다. 인터넷 초창기의 설계자 존 포스텔(Jon Postel)이 1980년 TCP 명세인 RFC 761 에 적어둔 문장으로, 흔히 포스텔의 법칙 또는 견고함의 원칙이라고 부른다.

보낼 때는 보수적으로, 받을 때는 관대하게.

이 원칙은 서로 다른 사람들이 만든 수많은 구현이 그럭저럭 말이 통하게 만드는 데 큰 역할을 했다. 브라우저의 관대함도 같은 정신에 가깝다. 받는 쪽이 웬만한 실수는 눈감아주면, 보내는 쪽이 조금 서툴러도 대화는 이어진다.

하지만 이 원칙에는 오래 쌓인 부작용도 있다. 받는 쪽이 계속 눈감아주면, 보내는 쪽은 자기가 틀렸다는 사실을 영원히 모른다. 그렇게 쌓인 실수들은 어느 날 덜 관대한 수신자를 만났을 때 한꺼번에 터진다. 2023년에 나온 RFC 9413 「견고한 프로토콜 유지하기(Maintaining Robust Protocols)」가 바로 이 문제를 다룬다. 이 문서는 지나친 관대함이 오히려 생태계를 망가뜨릴 수 있으니, 문제를 덮기보다 드러내서 고치게 만드는 편이 장기적으로 낫다고 이야기한다.

필자의 백스페이스가 정확히 이 경로를 밟았다. 티스토리의 에디터도, 브라우저도 이 글자를 눈감아줬다. 덕분에 몇 년 동안 아무도 몰랐고, 그동안 이 글자는 네 편의 글로 조용히 퍼졌다. 그리고 처음으로 엄격한 수신자인 XML 파서를 만난 날, 한꺼번에 들통났다. 관대함은 실수를 없애주는 게 아니라, 실수가 들킬 날짜를 미뤄주는 것뿐이었다.

고친 방법

고치는 일 자체는 싱거울 만큼 간단했다. 세 군데에서 제어 문자를 걸러냈다.

코드
// XML 1.0 이 허용하지 않는 제어 문자 제거
const stripControl = (s: string) => s.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");

먼저 이미 저장된 네 편의 글에서 해당 글자를 지웠다. 다음으로 RSS 를 만드는 코드에서 글자를 XML 에 넣기 전에 한 번 더 걸러냈다. 마지막으로 티스토리 글을 가져오는 스크립트에도 같은 처리를 넣어서, 나중에 글을 다시 가져와도 이 손님이 따라 들어오지 못하게 했다.

여기서 재미있는 고민이 하나 생긴다. RSS 를 만드는 코드에서 제어 문자를 걸러내는 건, 따지고 보면 우리도 관대한 쪽이 되는 일이다. 들어온 데이터가 틀렸어도 조용히 고쳐서 내보내니까. 그래서 필자는 거르는 것과 별개로, 원본 글에서도 글자를 지우는 쪽을 택했다. 나가는 문은 보수적으로 지키되, 문제를 덮어두지는 않기 위해서였다.

수정 후 다시 xmllint 를 돌리니 아무 말도 하지 않았다. 이 도구가 아무 말도 하지 않는다는 건 칭찬이다. 그런데 W3C 검사기는 한동안 같은 오류를 계속 보여줬다. 고친 파일을 받아서 파서에 직접 넣어보면 멀쩡했으니, 아마 검사기 쪽에 예전 결과가 남아 있었던 것 같다. 범인을 잡고 나서도 한동안 수배 전단이 붙어 있었던 셈이다.

보이지 않는 것에 대하여

이 일을 겪고 나서 필자가 가장 오래 생각한 건 "보이지 않는다"는 말의 의미였다. 그 백스페이스는 몇 년 동안 분명히 거기 있었다. 다만 그걸 보여주는 화면이 없었을 뿐이다. 우리가 문제없다고 믿는 많은 것들이 사실은 문제가 없어서가 아니라, 아직 엄격한 눈을 만나지 않았을 뿐일지도 모른다.

그래서 요즘은 무언가가 "잘 되는 것처럼 보일 때" 한 번쯤 엄격한 도구에 넣어보는 습관을 들이려고 한다. HTML 은 검사기에, XML 은 xmllint 에, JSON 은 스키마 검사에. 관대한 화면이 아무 말도 하지 않는 것과, 엄격한 도구가 아무 말도 하지 않는 것은 전혀 다른 의미이기 때문이다.

이상으로 지워지지 않은 백스페이스 포스팅을 마친다.