티스토리 뷰
목차
이 블로그를 도는 발행 도구의 코드에는 이런 문장이 그대로 적혀 있습니다.
"작성자가 직접 실행하면 승인서를 위조할 수 있다."
검수 절차를 만든 쪽이 스스로 남긴 메모입니다.

이 프로젝트는 "글을 쓴 세션이 자기 글을 검수하면 안 된다"는 규칙을 상시로 두고 있습니다.
처음엔 저도 이걸 그냥 방침인 줄 알았습니다. 지키면 좋고, 깜빡해도 크게 표는 안 나는 그런 종류요.
그런데 발행 도구 코드를 열어보니 얘기가 달랐습니다. 이 규칙은 방침에서 끝나지 않고, 실제 승인서 구조 안쪽 깊숙이 박혀 있었습니다.
승인서 하나로는 못 막는 것
write_approval.py가 하는 일은 단순합니다. 원고 HTML 파일을 읽어 해시 두 개를 계산하고, 검수자 이름·위험도 점수와 함께 .review.json 파일 하나를 만듭니다.
여기서 중요한 건 이 도구가 하지 않는 일입니다.
이 도구는 "정말로 다른 사람이 이 글을 읽었는지"를 확인하지 않습니다. --reviewer 자리에 어떤 이름을 넣어도, 그 이름이 진짜인지 코드는 검증할 방법이 없습니다. 상태(status) 값도 무조건 approved로 씁니다.
그러니까 만약 글을 쓴 세션이 검수 없이 이 명령을 곧바로 실행해버리면 어떻게 될까요.
결과물은 진짜 승인서와 바이트 단위로 구분이 안 됩니다. html_sha256도 정확하고, content_sha256도 정확하고, 형식도 완벽합니다. 발행 검사(check_html+check_approval)를 돌려도 위반은 0건입니다.
위반이 0건이라고 해서 검수가 있었다는 뜻은 아닙니다. 이 둘은 서로 다른 이야기입니다.
승인서를 손으로(또는 스스로) 만들면 안 되는 이유
이 위험이 추상적인 이야기로 끝나지 않은 사건이 실제로 있었습니다. 2026-07-29 무인 발행 전환 과정에서 승인서 개념에 없던 필드 하나 때문에 그날 발행된 글 3편의 쿠팡 카드 반영이 영구히 막힌 적이 있습니다. 자세한 경위는 무인 발행 3중 안전장치 글에 정리해뒀습니다.
이 사고가 남긴 교훈은 "필드 하나 빠지면 사람이 눈치껏 못 메운다"는 것에서 끝나지 않습니다. 더 근본적인 질문이 남습니다. write_approval.py는 승인서를 만들 때 정확히 무엇을 확인하고, 무엇을 확인하지 않을까요.
이 프로젝트의 설계 메모에는 이런 문장이 그대로 남아 있습니다.
"write_approval.py는 검수를 실제로 거쳤는지 검증하지 않고 항상 approved를 쓴다. 즉 작성자가 직접 실행하면 승인서를 위조할 수 있다. 1인 운영 도구라 암호학적 신원 확인은 비현실적이며, '작성 세션과 검수 세션 분리'라는 절차 규칙에 의존한다."
이건 사고 이후에 덧붙인 반성문이 아닙니다. 도구를 처음 만들 때부터 팀이 스스로 알고 있던 한계를 적어둔 겁니다. --reviewer 자리에 어떤 이름을 적어도 코드는 그 이름을 검증하지 않고, status는 무조건 approved로 씁니다 — 앞서 본 그대로입니다. 이중 해시가 아무리 정교해져도 이 지점은 건드리지 못합니다. 해시가 보는 건 "승인 이후 내용이 바뀌었는가"뿐이고, "승인 시점에 진짜 다른 사람이 읽었는가"는 애초에 해시로 답할 수 있는 질문이 아니기 때문입니다.

이중 해시가 실제로 보장하는 것
html_sha256은 승인 시점 원고 전체 바이트를, content_sha256은 쿠팡 카드 자리를 지운 나머지 본문을 봅니다. 각각 어떤 조건에서 정상적으로 깨지고 어떤 경우 위반으로 잡히는지는 무인 발행 3중 안전장치 글에서 이미 다뤘으니, 여기서는 결론만 짚습니다.
발행 검사는 이 둘을 순서대로 씁니다. 전체 해시가 승인서와 다르면 우선 의심하고, content_sha256이 없으면 그 자리에서 바로 반려하고 — "재검수 후 승인서를 다시 발급하세요" —, 있으면 그 값까지 다시 대조합니다.
궁금하실 만한 것
Q. 그럼 왜 처음부터 승인서에 "진짜 검수했는지" 확인란을 안 만들었나요?
1인 운영 도구라서입니다. 검수자 이름을 적는 칸은 있지만 그 이름이 진짜인지 코드가 확인할 방법이 없습니다. 로그인·서명 같은 신원 확인 장치를 넣는 건 오히려 과합니다. 그래서 코드로 못 막는 부분은 "작성 세션과 검수 세션을 반드시 분리한다"는 절차가 대신 떠맡습니다.
Q. 그럼 이중 해시는 사실상 의미가 없는 건가요?
아닙니다. 승인 이후 몰래 바뀌는 것은 확실히 잡아냅니다. 다만 "승인 시점에 진짜 다른 사람이 읽었는가"까지는 증명하지 못한다는 뜻입니다. 그 경계를 아는 것과 모르는 것은 다릅니다.
이 구조가 결국 증명하는 건 딱 하나입니다. 승인 이후 내용이 몰래 바뀌지 않았다는 것.
"진짜 다른 사람이 이 글을 읽고 검수했다"는 사실은 이 해시 어디에도 담겨 있지 않습니다. 그 증명은 코드가 못 하고, 절차가 대신 떠맡고 있습니다.
지금 이 규칙이 굳어진 이유
그래서 이 프로젝트는 규칙을 한 겹이 아니라 두 겹으로 둡니다.
하나, 승인서는 반드시 write_approval.py로만 발급합니다. 손으로 만들면 2026-07-29처럼 필요한 필드 자체가 빠질 수 있고, 자동 계산이 아니면 그게 실수인지 고의인지도 나중에 가려낼 수 없습니다.
둘, 그 도구는 반드시 작성 세션과 다른 세션·에이전트가 실행합니다. 코드가 확인 못 하는 "진짜 검수를 거쳤는가"는 여기서 사람이 정한 절차가 지킵니다.
이 둘은 따로 두면 각각 허술해집니다. 도구만 있고 분리가 없으면 작성자가 스스로 눌러 위조나 다름없는 승인을 만들 수 있고, 분리만 있고 도구가 없으면 승인서에 뭘 남겨야 하는지조차 사람마다 달라집니다.
같이 있어야 "작성자가 검수하면 안 된다"는 말이 다짐에서 끝나지 않고, 매번 같은 방식으로 재현되는 절차가 됩니다.
발행 뒤에 카드 하나 끼웠다고 승인 전체가 막혀버렸던 그날의 전말은 무인 발행 3중 안전장치 글에, 검수를 백그라운드로 돌렸다가 결과가 통째로 사라졌던 사고는 검수 서브에이전트 백그라운드 사고 글에 각각 따로 정리해뒀습니다.
이번 글은 그 두 사고와 다른, 더 앞단의 질문 하나만 다뤘습니다. 왜 애초에 작성자와 검수자가 같은 존재면 안 되는가 하는 질문입니다.