설명서를 믿었는데 코드가 달랐다
5대 노드에 동시에 지시를 보내는 자동화를 처음 발사했다. 세션 점검은 5/5 통과 — 모든 기계가 살아있다고 확인됐다. 버튼을 눌렀다. 5대 전부 같은 줄에서 멈췄다. file not found: <지시문 본문>. 파일을 못 찾는다고? 파일을 보낸 게 아닌데. 오류 메시지를 세 번 읽었다. 세 번 다 같았다. 이상한 데 없이 이상한 오류였다.
첫 오류 메시지는 범인을 가리키지 않았다
오류 메시지만 읽으면 “파일이 없다”는 말이다. 그래서 처음엔 세션이 죽었나, 소켓 경로가 잘못됐나, 퍼미션 문제인가 — 그쪽을 먼저 봤다. 세션 점검은 방금 전 5/5로 통과했다. 세션 문제가 아니었다.
그럼 왜 파일 오류가?
스크립트를 뜯어봤다. 오류가 나오는 줄은 딱 한 곳이었다. 스크립트 안에 특정 옵션 글자 뒤에 오는 값이 실제 파일 경로인지 확인하는 로직이 있었다. 파일이 없으면 file not found를 뱉고 멈추게 되어 있었다.
그런데 나는 그 자리에 파일 경로를 넘기지 않았다. 지시문 본문 텍스트를 통째로 넘겼다. 설명서가 그렇게 하라고 적혀 있었으니까.
원인은 세션이 아니었다. 오류 메시지가 말하는 것과 실제 원인은 완전히 다른 곳에 있었다. 설명서와 코드가 같은 글자에 대해 다른 약속을 하고 있었던 것이다. 표면 오류는 파일을 못 찾는다고 했지만, 진짜 문제는 설명서가 코드와 어긋나 있다는 것이었다.
5대가 멈춘 이유는 5대 전부 같은 설명서를 읽었고, 같은 방식으로 따라 했고, 같은 자리에서 같은 이유로 깨졌기 때문이다. 변수가 없었다. 모두 한 방향으로 틀렸다.
같은 글자, 사람에겐 X, 기계에겐 Y
비개발자에게 설명할 때 쓰는 비유가 있다.
레스토랑 안내문에 “주문을 확정하려면 ★를 누르세요”라고 인쇄되어 있다. 손님은 안내문대로 ★를 눌렀다. 그런데 주방 시스템에서 ★는 “주문 취소” 신호다. 안내문 쓴 사람과 주방 시스템 만든 사람이 ★의 의미를 다르게 정한 것이다. 손님은 아무 잘못 없이 확정하려다 주문을 취소했다. 안내문대로 했는데 안내문과 다른 결과가 나왔다.
이번 사고가 정확히 그랬다. 설명서에는 “이 옵션 글자를 쓰면 지시문을 넘기는 것”이라고 적혀 있었다. 실제 코드에선 “이 옵션 글자 뒤에 오는 값은 파일 경로”라고 구현되어 있었다. 같은 글자, 다른 약속.
그래서 지시문 본문 전체가 “존재하지 않는 파일의 경로”로 해석됐다. 본문이 길든 짧든 상관없다. 그 이름을 가진 파일은 없으니 항상 실패한다. 5대가 모두 같은 설명서를 읽고 같은 방식으로 지시를 받았으니, 5대 전부 같은 줄에서 같은 이유로 멈췄다.
이런 어긋남을 drift라고 부른다. 설명서와 코드가 한때는 일치했는데, 어느 시점에 한쪽이 바뀌고 다른 쪽이 따라가지 못해 벌어진 간극이다. 기술 세계에선 이 gap 이 아주 작게 시작한다. 옵션 글자 하나의 의미 차이. 그런데 이게 발사 버튼을 눌렀을 때 5대가 동시에 멈추는 사고가 된다.
Drift는 자연스럽게 쌓인다
코드는 바뀐다. 기능이 추가되고, 옵션 글자의 의미가 재정의되고, 호출 방식이 개편된다. 코드를 고칠 때마다 설명서를 같이 고치는 사람은 — 솔직히 말하면 — 많지 않다.
의도적인 방치인 경우는 드물다. 코드를 고치고 나면 “됐다, 동작한다”는 느낌이 온다. 설명서를 고치는 건 그 다음 일처럼 느껴진다. 지금 당장 막는 건 아닌 것처럼. 그러다 한 달이 지나고, 누군가 다른 사람이 — 또는 자동화가 — 그 설명서를 읽을 때가 온다.
설명서를 쓴 사람은 코드를 알고 있다. 코드를 보면 “이 설명서가 지금은 틀렸다”는 걸 바로 안다. 문제는 설명서 읽는 쪽이 코드를 먼저 보지 않고 설명서를 먼저 본다는 것이다. 그래서 설명서가 진실이라고 믿는다.
설명서는 “이렇게 하면 됩니다”라고 말한다. 믿는다. 그대로 한다. 깨진다.
설명서가 맞던 시절이 있었다. 그때 이후로 코드가 바뀌었다. 설명서는 그 시절 그대로 멈춰 있다. 이 시간차가 drift의 본질이다. 코드와 설명서는 각자의 속도로 흘러가고, 그 거리가 벌어질수록 설명서를 믿었을 때 뭔가 깨질 가능성이 높아진다. 아무도 눈치채지 못한 채로.
가장 위험한 drift는 설명서가 완전히 틀린 경우가 아니다. 90%는 맞고 한 부분만 어긋난 경우다. 대부분은 설명서대로 잘 된다. 그래서 틀린 걸 의심하지 않는다. 그러다 그 한 부분이 실행될 때 깨진다.
나쁜 설명서보다 거의 맞는 설명서가 더 위험할 수 있다. 나쁜 설명서는 처음부터 의심한다. 거의 맞는 설명서는 믿게 된다.
이번 설명서가 그랬다. 나머지 부분은 정확했다. 이 한 줄만 코드와 어긋나 있었다. 그래서 의심하지 않았다. 설명서가 대체로 잘 되어 있었으니까.
AI가 설명서를 읽을 때
이번 사고에서 설명서를 읽은 게 사람이 아니라 AI였다는 점이 중요하다.
사람이 설명서를 읽으면 문맥을 읽는다. 앞뒤를 본다. “이 옵션이 파일 경로를 받는다고? 왜 파일 경로를 받지? 지시문인데?”라고 의심할 수 있다. 실제로 그렇게 멈추는 사람이 있다. 설명서가 이상하다고 느끼는 사람이 있다. 코드를 직접 열어보는 사람도 있다.
AI는 설명서를 문자 그대로 읽는다. “이 옵션 글자를 쓰면 지시문을 넘기는 것”이라고 적혀 있으면, 그렇게 한다. 주변 문맥에서 이상함을 감지하는 게 약하다. 설명서가 맞다고 가정하고 따른다.
그래서 AI가 설명서를 읽고 자동화를 실행하는 구조에서 drift의 위험이 더 크다. 사람이라면 감지했을 “이상한 느낌”이 없다. 설명서가 틀려도 자동화는 그대로 실행된다. 5대 동시에.
인간 직관이 완충재 역할을 했던 시대가 있었다. 설명서가 조금 틀려도 사람이 읽으면서 “아, 이건 아닌 것 같은데” 하고 걸렀다. AI가 설명서를 읽고 실행하는 구조에서는 그 완충재가 없다. 설명서의 정확도가 직접 사고율에 연결된다.
설명서가 계약이라면, 계약이 어긋났을 때 손해 보는 건 믿은 쪽이다
설명서는 사용자와 코드 사이의 계약이다. “이렇게 쓰면 이렇게 동작합니다”라는 약속.
계약이 깨지면 손해 보는 건 계약을 믿은 쪽이다. 코드는 항상 자기 방식대로 돌아간다. 설명서가 틀려도 코드는 모른다. 코드 입장에선 약속을 어긴 게 없다. “나는 이 옵션 글자 뒤에 파일 경로를 받아서 그 파일에서 읽는다” — 그게 코드의 현실이고, 코드는 그대로 하고 있다. 설명서가 코드를 잘못 설명한 것이지, 코드가 틀린 게 아니다.
오류 메시지를 받는 건 설명서를 읽고 그대로 따라 한 쪽이다.
사람이 직접 따라 하는 경우라면 그나마 “뭔가 이상한데?” 하고 멈출 수 있다. 상황을 보고 “이게 아닌 것 같다”고 느낄 수 있다. 자동화는 멈추지 않는다. 설명서에 적힌 대로 실행하고, 오류 메시지를 받고, 끝난다. 중간에 “이상한데?”가 없다.
자동화가 규모를 키우는 것처럼, drift의 영향도 규모에 비례한다. 사람 1명이 틀린 설명서를 따라 하면 1건이 깨진다. 자동화가 5대에게 동시에 틀린 방식으로 지시를 보내면 5대가 동시에 깨진다. 같은 실수가 배율이 붙어서 발생한다. 자동화를 구축할수록 설명서의 정확도가 더 중요해지는 이유다.
진실의 우선순위는 언제나 코드다
설명서를 쓴 사람 입장에선 억울할 수 있다. 그때는 맞는 설명서였다. 코드가 나중에 바뀐 거다.
그래도 어쩔 수 없다. 소프트웨어에서 진실은 코드에 있다. 설명서는 코드를 설명하는 것이지, 코드가 설명서를 따르지 않는다. 코드가 바뀌었으면 설명서도 바뀌었어야 했다.
이 논리는 자동화 설명서에만 해당하는 이야기가 아니다. 메모리든, 문서든, 과거에 기록한 무언가든 — 그게 특정 시점의 스냅샷이라는 사실을 잊는 순간 위험해진다. “이전에 이랬으니 지금도 이럴 것이다”라는 가정. 그 가정이 코드의 현재 상태와 맞지 않을 때 사고가 난다.
코드를 열어보면 된다. 설명서가 말하는 것과 코드가 실제로 하는 것을 비교하면 된다. 다르면 코드가 맞다. 설명서를 고쳐야 한다. 코드를 설명서에 맞춰 “원래 이랬어야 했는데”로 읽으면 안 된다.
진실의 우선순위: 코드 > 설명서. 이 순서가 뒤집히면, 낡은 문서가 현실처럼 통용되다가 어느 날 사고가 난다. 그리고 그 사고는 “설명서가 틀렸습니다”라는 오류 메시지를 보내지 않는다. file not found처럼, 전혀 다른 메시지를 보낸다.
수정은 단순했다, 항상 그렇듯이
원인을 알고 나면 수정은 단순했다. 설명서에서 그 옵션 글자를 지우고, 지시문 본문을 직접 인자로 넘기는 방식으로 바꿨다. 스크립트의 실제 호출 방식을 확인했다 — 본문은 직접 인자로 넘기면 되고, 파일에서 읽을 때만 그 옵션을 쓰면 된다. 설명서를 코드에 맞췄다.
그리고 설명서 상단에 경고 블록을 하나 추가했다. “지시문은 직접 인자로 넘기면 됩니다. 파일 경로 옵션은 파일에서 읽을 때만 쓰는 것이고, 본문 텍스트를 그 자리에 넣으면 파일 없음 오류가 납니다.” 이번 사고 기록을 링크로 박았다.
5대가 전부 같은 줄에서 깨진 이유가 설명서 한 줄 오류였다. 수정도 한 줄이었다. 근데 그 한 줄을 찾는 데 시간이 걸렸다. 오류 메시지가 다른 곳을 가리키고 있었으니까. 처음에 세션을 의심했고, 경로를 확인했고, 퍼미션을 봤다. 마지막에 설명서와 코드를 나란히 놓고 비교했을 때 보였다.
경고 블록을 추가한 이유는 하나다. 다음 번에 누군가 — 사람이든 자동화든 — 설명서를 읽고 그 자리에서 멈추게 하기 위해서다. 같은 실수를 두 번 하는 것보다, 설명서 안에 “여기서 사고가 한 번 났습니다”라고 박아두는 게 낫다. 사고의 흔적이 설명서 안에 살아있으면 다음 번엔 막을 수 있다.
설명서를 코드와 살아있게 유지하는 것도 인프라다
자동화 인프라를 구축하면서 배운 게 있다면, “잘 돌아가는 코드”와 “잘 돌아가는 자동화”는 같은 말이 아니라는 것이다. 잘 돌아가는 자동화에는 코드 외에도 그 코드를 정확하게 설명하는 문서가 필요하다. 코드는 기계가 읽고, 문서는 사람이나 다른 자동화가 읽는다. 둘 다 맞아야 한다.
오해가 생길까 봐 덧붙인다. 설명서를 믿지 말라는 게 아니다. 설명서를 코드의 진실에 맞춰 살아있게 유지하는 게 인프라의 일부라는 것이다. 코드를 고칠 때 문서도 같이 고쳐야 한다. 설명서가 낡으면 설명서를 믿고 따라 한 쪽이 깨진다.
코드를 고칠 때 설명서를 “나중에 할 일” 목록으로 미루면 그 빚이 쌓인다. 그 빚은 사고가 날 때까지 잘 안 보인다. 보이게 되는 순간은 file not found 같은 오류가 나올 때다.
설명서가 코드 변경의 일부라는 생각 — 코드를 고쳤으면 설명서도 고쳐야 그 코드 변경이 완료된 것이다 — 을 내면화하는 게 가장 단순한 예방이다. 코드 커밋에 설명서 업데이트가 같이 들어가는 것, 그게 기준이 되면 drift가 생길 여지가 줄어든다.
자동화가 그 누군가가 되면 배율이 붙는다. 한 명이 잘못 따라 하면 한 건이 깨진다. 자동화가 잘못 따라 하면 그 자동화가 닿는 모든 곳이 깨진다. 이번처럼 5대가 동시에.
배운 것
오류 메시지는 진짜 원인을 직접 가리키지 않는다. file not found는 파일 문제가 아니었다. 표면 메시지 너머에 있는 원인을 찾는 눈 — 오류가 말하는 것과 실제로 잘못된 것을 구분하는 것 — 이게 사고 이후에 남는 것이다.
설명서를 코드와 동기화하는 게 인프라의 일부라는 것. 코드가 바뀌면 설명서도 바뀌어야 한다. 설명서가 낡아있으면 설명서를 믿고 따라 한 쪽이 깨진다.
자동화를 구축할수록 설명서의 정확도가 더 중요해진다. 사람은 “이상한데?” 하고 멈출 수 있지만, 자동화는 멈추지 않는다. 설명서가 틀려있으면 자동화가 닿는 전부에 그 실수가 배율이 붙어 퍼진다.
자동화 설명서를 쓸 때마다 이제 한 가지를 더 확인한다. 이 설명서가 코드의 현재 동작을 설명하는가, 아니면 코드가 한때 동작하던 방식을 설명하는가. 다르면 코드가 맞다. 설명서를 고쳐야 한다.
그리고 사고가 나면 설명서에 흔적을 남긴다. 경고 블록 하나, 링크 하나. 같은 자리에서 같은 이유로 또 깨지지 않게. 설명서는 그렇게 사고를 통해 조금씩 더 정확해진다. 코드를 믿고, 코드에 맞게 설명서를 고치고, 사고가 나면 경고를 박는다. 이 사이클이 설명서를 살아있게 만드는 방법이다.
설명서는 코드의 번역본이 아니라 코드와 체결한 계약서다. 계약서가 실제와 달라졌으면 계약서를 고쳐야 한다. 고치지 않으면 계약서를 믿고 따라 한 쪽이 깨진다.
— 강대종 / @ssamssae