products.detail_html 의 깨진 <img> 를 고치는 도구를 만들고 있었다. 계획에 본문을 정규화해서 다시 저장한다는 항목이 들어 있어서 기존 처리기 DetailHtmlProcessor 를 읽었다.
Table of contents
Open Table of contents
배경 — 계획에 있던 본문 정규화
원래 시스템의 처리기는 여러 단계의 str_replace() 와 경로 매핑과 방어 검사를 거친 뒤에 마지막으로 DOMDocument::loadHTML() 로 파싱해서 saveHTML() 로 다시 문자열을 만들고 있었다. 마지막 단계가 이 계획의 문제였다.
내가 만들려는 도구는 Python 으로 짜고 있었고 lxml 로 같은 처리를 재현해야 했다. 재현의 기준이 무엇인지가 여기서 걸렸다.
원인 — 재직렬화가 바꾸는 것들
파싱했다가 다시 문자열로 만들면 속성 순서와 따옴표 방식과 태그 안 공백과 <br> 의 닫는 표기와 같은 엔티티 인코딩이 원본과 달라진다. 같은 언어의 다른 파서끼리도 다르고 언어가 다르면 차이가 더 커진다.
바이트 단위로 같은 출력을 보장할 방법이 없으므로 그 계획은 재현이 불가능한 것을 재현하겠다는 것이었다. 여기서 계획을 이어 가는 대신 원래 목적으로 돌아갔다.
원래 목적으로 돌아가기
이 작업의 목적은 깨진 이미지를 고치는 것이지 본문을 표준화하는 것이 아니었다. 정규화가 그 목적을 달성하는지를 먼저 물어야 했다.
깨진 사례를 실제로 열어 보니 본문은 멀쩡했고 src 가 /resized/ 아래 변환본 경로를 정확히 가리키고 있었다. S3 에 그 키가 없는 것이 원인이었다.
해결 — 원인이 본문에 없던 이유
원인이 변환 누락이므로 변환본만 만들어 넣으면 이미지가 되살아난다. 본문은 한 글자도 안 바꿔도 된다.
두 가지 길이 있었는데 수백만 행의 고객 노출 본문을 다시 쓰는 것과 누락된 변환본만 Intervention\Image 로 만들어 올리는 것이다. 뒤쪽을 골랐다. 이유는 본문에 원인이 없는데 본문을 건드리면 고칠 것 없는 곳에 위험만 얹는 셈이라서다. 대신 원본 이미지까지 없는 건은 이 도구로는 못 고치고 따로 세어 넘겨야 하는 것을 감수했다. 위험도가 완전히 다른 두 작업이었고 목적으로 돌아간 것이 그 차이를 만들었다.
대안 — 재현이 아니라 위임
표준화 자체가 의미 없는 것은 아니지만 그것은 다른 목적이다. 두 목적을 한 작업에 묶으면 문제가 생겼을 때 어느 쪽 때문인지 못 가른다.
정말 필요해지면 전체를 재직렬화하지 않고 src 속성만 정규식으로 치환하거나 원래 DetailHtmlProcessor 를 정본으로 두고 artisan 커맨드로 호출해서 결과를 받는 방법이 있다. 뒤의 것은 재현이 아니라 위임이므로 출력이 정의상 같아지며 이 지점은 계획 검토에서 이미 지적받은 자리였다.
정리
loadHTML()뒤saveHTML()은 바이트 단위로 원본과 달라진다- 같은 언어의 다른 파서끼리도 출력이 다르다
- 다른 언어로는 동일 출력을 보장할 수 없다
- 재현이 불가능한 것을 재현하려는 계획인지 먼저 본다
- 막히면 원래 목적으로 돌아가 그 작업이 필요한지 다시 묻는다
- 원인이 다른 곳에 있으면 훨씬 좁은 작업으로 바뀐다
- 표준화와 수정을 묶으면 실패 시 원인을 못 가른다
- 재현 대신 원래 처리기에 위임하면 출력이 정의상 같다