[태그:] 장애 기록

  • 역할 이름으로 보냈는데 아무도 못 받았다 — 빈 좌석이 지시를 삼킨 사고

    역할 이름으로 보냈는데 아무도 못 받았다 — 빈 좌석이 지시를 삼킨 사고

    도우미를 하나만 쓸 때는 지시를 보낼 곳이 하나뿐이다. 둘 이상 굴리기 시작하면 달라진다. 누구에게 무엇을 시킬지 정해야 하고, 그러려면 부르는 방법이 필요하다. 이 글은 우리가 그 부르는 방법을 쓰다가 겪은 일이다.

    이 글은 회사에 빗대어 쓴다. 끝까지 그 하나로만 간다. 쪽지를 직책으로 보낸다는 것은 사람 이름 대신 직책을 적어 보내는 것이다 — “검토 담당자에게 전해 주세요”처럼. 자리는 그 직책이 앉아 있어야 할 책상이다. 빈 자리는 책상은 있는데 사람이 없는 상태다. 이 셋만 알면 나머지는 따라온다. 전문 용어는 이 글에 더 나오지 않는다.

    논지는 한 줄이다. 직책으로 보낸 쪽지는, 그 직책에 사람이 앉아 있을 때만 주소다. 사람이 없으면 그것은 주소가 아니라 책상 번호일 뿐이다. 그리고 하나 더 — 보냈다와 받았다는 다른 사실이다. 보내는 쪽 화면에 뜨는 성공은 보낸 쪽 사정이지 받은 쪽 사정이 아니다.

    앞선 편과의 경계. 도구가 조용히 실패하는 여러 형태는 앞 편에 목록으로 적었고, 이 글은 그중 한 가지 기제만 판다. 주소가 무엇을 보장하고 무엇을 보장하지 않는지, 그리고 도달을 어떻게 확인하는지다.

    이 글이 도움이 되는 사람. 도우미를 둘 이상 굴리기 시작한 사람이다. 하나만 쓸 때는 이 문제가 생기지 않는다 — 보낼 곳이 하나뿐이니 잘못 갈 데가 없다. 둘이 되는 순간 누구에게 보낼지를 적어야 하고, 거기서부터 이 이야기가 시작된다.

    근거의 범위를 먼저 밝혀 둔다. 이 글의 근거는 우리 운영 기록의 관측 세 건이다. 이 일이 얼마나 자주 나는지 우리는 세지 않았고, 다른 시스템이 직책 주소를 어떻게 다루는지도 재지 않았다. 그래서 이 글에 그런 문장이 없다. 세 건에서 나온 것만 적고, 세 건이 말해 주지 않는 것은 적지 않았다.

    직책으로 보낸 쪽지가 빈 자리로 갔을 때 보내는 쪽과 받는 쪽에서 무엇이 갈리는지를 칸으로 나눠 그린 도식. 첫째 칸은 보내는 쪽이 본 것으로, 직책 이름으로 보냈고 보내기가 성공으로 돌아왔으며 상태판에는 작업 중으로 보였다고 적혀 있다. 둘째 칸은 실제로 일어난 일로, 자리는 살아 있었지만 사람이 없었고 쪽지는 화면에만 쌓였으며 긴 글은 입력줄에 붙은 채 제출되지 않았다고 적혀 있다. 셋째 칸은 직책 주소가 보장하는 것과 보장하지 않는 것을 갈라 놓았다. 보장하는 것은 그 직책 자리로 배달을 시도한다는 것까지이고, 보장하지 않는 것은 그 자리에 사람이 있는지, 사람이 읽었는지, 읽고 착수했는지다. 넷째 칸은 도달을 확인하는 방법으로, 새로 생긴 파일과 갱신된 기록과 눈에 보이는 응답 같은 상대의 활동 변화를 본다고 적혀 있다. 아래 칸은 관측 세 건의 요약이다. 첫째는 빈 자리에 놓인 쪽지를 책상 위 기계가 명령서로 오인해 실행하려 한 것이고, 둘째는 하루 뒤 같은 일이 다시 나서 그날 전원에게 보낸 공지까지 그 자리에서 삼켜진 것이며, 셋째는 긴 글이 입력줄에 붙은 채 약 한 시간 사십오 분 방치된 것이다. 맨 아래 두 줄은 한정이다. 관측은 세 건이고 얼마나 자주 나는지는 세지 않았다는 것, 그리고 같은 경고가 우리 도구 머리말에 이미 적혀 있었지만 읽었으면 막았을지는 우리가 재지 않았다는 것이다.
    직책 주소가 나쁘다는 그림이 아니다. 그 주소가 어디까지 보장하는지와, 보내는 쪽 화면이 받는 쪽 사정을 보여 주지 않는다는 것을 그린 것이다. 아래 두 줄은 한정이다.

    지시가 사라진 경로

    1. 직책으로 보낸 쪽지는 그 직책에 사람이 있을 때만 주소다

    증상. 지시를 보냈는데 아무 일도 일어나지 않는다. 보내는 쪽 화면에는 보냈다고 떠 있다. 상대를 확인해 보면 그쪽도 문제가 없어 보인다. 그런데 시킨 일은 시작되지 않았다.

    여기서 갈리는 두 가지 해석. 하나는 상대가 게으르거나 못 알아들었다고 보는 것이다. 다른 하나는 내가 잘못 보냈다고 보는 것이다. 우리 기록에서는 둘 다 아니었다. 보내기는 제대로 됐고, 그 직책 자리도 그대로 있었다. 그 자리에 사람이 없었다.

    직책으로 부르는 것이 왜 편한가. 사람이 바뀌어도 부르는 이름을 바꾸지 않아도 되기 때문이다. 담당자가 교체돼도 “검토 담당자”라고 적으면 새 담당자에게 간다. 여러 도우미를 굴릴 때 이것은 큰 이점이라 우리도 그렇게 쓴다.

    회사로 옮겨 보면 이렇다. “검토 담당자에게 전해 주세요”라고 적어 쪽지를 보냈다고 하자. 그 쪽지는 검토 담당자 책상으로 간다. 담당자가 그 자리에 있으면 읽고, 자리를 비웠으면 책상에 남는다. 쪽지를 보낸 사람은 그 두 경우의 차이를 보지 못한다.

    그런데 그 편의는 하나를 가린다. 직책은 자리를 가리키는 이름이지 사람이 있다는 보장이 아니다. 사람이 나가고 자리만 남으면 그 이름은 여전히 유효하다. 쪽지는 그 책상까지 정확히 배달되고, 거기서 멈춘다.

    처음 쓸 때 생기는 기대. 직책으로 보내기가 되면 사람들은 그것을 사람에게 보내는 것으로 여긴다. 실제로 대개는 그렇게 동작하니 그 기대가 굳는다. 기대가 굳으면 확인하는 절차가 사라진다 — 늘 되던 것을 매번 확인하지는 않기 때문이다. 그러다 한 번 어긋나면 아무도 보고 있지 않은 상태가 된다.

    주소가 보장하는 것과 보장하지 않는 것을 갈라 적으면 이렇다.

    
    보장한다        그 직책 자리로 배달을 시도한다
    보장하지 않는다  그 자리에 사람이 있는지
    보장하지 않는다  그 사람이 읽었는지
    보장하지 않는다  읽고 일을 시작했는지
    

    이 네 줄이 한 덩어리로 묶여 있는 것이 문제다. 넷을 따로 떼어 놓으면 아무도 헷갈리지 않는다. 그런데 실제로는 보내기 한 번에 넷이 함께 따라오는 것처럼 느껴진다. 화면에 뜨는 것은 맨 윗줄 하나인데, 머릿속에서는 넷이 다 켜진다.

    우리가 읽은 방식. 우리는 맨 윗줄이 참인 것을 보고 아랫줄 셋도 참이라고 읽었다. 보내기가 성공으로 돌아왔으니 상대가 받았다고 본 것이다. 이 글의 세 관측은 전부 그 한 칸에서 갈렸다.

    왜 그렇게 읽게 되는가. 보내는 쪽과 받는 쪽이 각자 자기 기록만 남기기 때문이다. 보내는 쪽 기록에는 내보냈다는 줄이 남고, 받는 쪽 기록에는 받았다는 줄이 남는다. 두 기록은 서로를 확인해 주지 않는다. 한쪽만 보고 있으면 그 한쪽이 전부처럼 보인다.

    세 관측이 갈린 자리를 미리 적어 둔다. 첫째는 자리에 사람이 없어서 갈렸다. 둘째도 같은 이유인데 목록이 정상으로 보였다는 점이 다르다. 셋째는 사람이 있었는데 글이 제출되지 않아서 갈렸다. 세 번 다 보내는 쪽 화면은 정상이었다.

    확인 방법. 자기 도구에서 보내기 성공이 무엇을 보고 성공이라고 하는지 한 번 찾아보라. 대개는 보내는 쪽에서 내보냈다는 뜻이다. 받는 쪽이 읽었는지까지 확인해 주는 도구라면 그 사실이 어딘가 적혀 있을 것이고, 적혀 있지 않으면 그것은 보장 밖이다.

    2. 빈 책상에 놓인 쪽지를 기계가 명령서로 오인했다

    이 항목은 우리가 실제로 겪은 것이고, 관측 한 건이다.

    관측. 직책 이름으로 보낸 지시가 사람이 앉지 않은 빈 자리로 갔다. 나중에 그 자리 화면을 열어 보니 보낸 문장이 그대로 찍혀 있었다. 그리고 그 문장 아래에 오류가 하나 붙어 있었다.

    그 오류가 무엇이었는가. 그 문장을 명령으로 실행하려다 난 것이었다. 사람이 없는 자리에서는 들어온 글자를 읽어 줄 상대가 없다. 대신 그 자리에 남아 있던 기계가 그 글자를 자기에게 내린 명령으로 받아들였다.

    이것이 빈 책상보다 나쁜 이유. 쪽지가 빈 책상에 그냥 놓였다면 나중에 누가 와서 읽으면 된다. 우리 경우는 책상 위 기계가 그 쪽지를 명령서로 오인해 실행하려 한 것이다. 실행은 실패했고 오류만 남았다. 남은 것은 지시가 아니라 오류 기록이었다.

    왜 자리가 글자를 명령으로 받아들이는가. 사람이 앉아 있으면 들어온 글자는 그 사람에게 하는 말이다. 사람이 없으면 그 글자를 받을 상대가 자리밖에 남지 않는다. 자리는 원래 명령을 받아 실행하는 곳이라, 들어온 글자를 자기 일감으로 읽는다. 같은 글자가 상대에 따라 다른 것이 되는 것이다.

    남은 흔적이 무엇인지가 중요하다. 지시가 남았으면 나중에라도 누가 읽고 처리할 수 있다. 우리 경우 남은 것은 오류 기록이었다. 오류 기록은 그것을 낸 자리의 사정이지 내가 시킨 일이 아니다. 그래서 그 자리를 나중에 열어 본 사람도 무엇을 하라는 지시였는지 바로 알기 어렵다.

    보내는 쪽에서는 무엇이 보였는가. 아무것도 이상하지 않았다. 보내기는 성공이었고, 그 자리도 목록에 그대로 있었다. 이 사고는 보내는 쪽 화면에 아무 흔적도 남기지 않는다. 그래서 보낸 사람이 먼저 알아차릴 방법이 없다.

    지시가 사라진 것이 아니라 다른 것이 됐다. 우리는 사고를 상상할 때 없어지는 쪽을 떠올린다. 쪽지가 분실되거나 배달이 실패하는 그림이다. 우리 경우는 그것이 아니라 도착해서 다른 것으로 처리된 경우다. 없어진 것을 찾는 눈으로는 이 형태가 보이지 않는다.

    초보용으로 옮기면 이렇다. 직책으로 보낸 쪽지는 사람이 아니라 자리에 배달된다. 그 자리에 사람이 있으면 읽고, 없으면 자리가 알아서 처리한다. 자리가 알아서 하는 처리가 우리가 원한 것과 같으리라는 보장은 어디에도 없다.

    이 관측에서 우리가 얻은 것. 사고 자체보다 흔적의 모양을 알게 된 것이 남았다. 이 형태가 나면 그 자리에 내가 쓴 문장과 그 아래 오류가 함께 남는다. 그 모양을 알아 두면 다음에 같은 화면을 봤을 때 무엇이 일어난 것인지 바로 알 수 있다.

    확인 방법. 직책으로 지시를 보냈으면 그 자리의 화면을 한 번 보라. 내가 보낸 문장이 글자 그대로 찍혀 있고 그 아래 오류가 붙어 있으면, 그것은 읽힌 것이 아니라 실행되려다 실패한 것이다.

    3. 자리는 살아 있었고 사람만 없었다

    이 항목도 우리가 겪은 것이고 관측 한 건이다. 앞 항목의 다음 날 일이다.

    관측. 자리 목록에는 정상으로 보였다. 자리는 살아 있었고 직책도 그 자리에 등록돼 있었다. 사람만 없었다. 목록이 보여 주는 것은 책상이 있다는 것까지이고, 그 앞에 사람이 앉아 있는지는 아니었다.

    무엇이 삼켜졌는가. 그 자리로 간 직책 주소 쪽지는 전부 화면에만 쌓였다. 그날 전원에게 보낸 공지도 그 자리에서 삼켜졌다. 여러 곳에 같은 것을 보내면 한 곳이 빠져도 나머지가 받으니 괜찮다고 생각하기 쉬운데, 빠진 그 한 곳은 아무 신호도 내지 않는다.

    누가 찾아냈는가. 보낸 사람이 아니었다. 다른 자리에서 일하던 쪽배달 기록을 대조하다 찾아냈다. 보낸 쪽 기록에는 보낸 것만 남고, 받은 쪽 기록에는 받은 것만 남는다. 두 기록을 나란히 놓아야 보냈는데 받은 기록이 없는 줄이 보인다.

    전원에게 보낸 것이 삼켜졌다는 말의 뜻. 여러 곳에 같은 내용을 보내면 안심하게 된다. 한 곳이 못 받아도 나머지가 받았으니 일이 굴러갈 것 같기 때문이다. 그런데 못 받은 그 한 곳은 못 받았다고 말해 주지 않는다. 그래서 여러 곳에 보내는 것은 도달을 늘리는 방법이지 도달을 확인하는 방법이 아니다.

    왜 보낸 사람이 못 찾았는가. 보낸 사람은 자기 기록을 본다. 거기에는 보냈다는 줄이 정상으로 남아 있으니 볼 이유가 없다. 어긋남은 두 기록 사이에 있고, 그 사이는 한쪽만 보는 사람 눈에는 들어오지 않는다. 그래서 이 형태는 제3자가 대조할 때 드러난다.

    왜 사람이 없었는가. 그 자리는 우리가 믿기로 등록해 둔 위치 밖에서 열렸다. 처음 여는 자리에는 계속할지 묻는 확인 창이 뜨는데, 거기서 자동 응답이 그만두기를 골랐다. 그래서 사람이 앉기도 전에 자리를 떴다.

    그 뒤에 무엇이 쏟아졌는가. 그 순서 다음에는 새로 앉는 사람에게 주는 안내문이 이어지게 돼 있었다. 사람은 이미 없었으므로 그 안내문이 빈 껍데기에 그대로 쏟아졌다. 자리는 그때부터 글자만 받아 쌓는 상태가 됐다.

    안내문이 쏟아졌다는 말의 뜻. 새로 앉는 사람에게 주는 안내문은 사람이 있다는 전제로 만들어진 순서다. 앞 단계에서 사람이 사라졌는데 그 순서는 그대로 진행됐다. 그래서 자리에는 읽을 사람 없는 긴 글이 먼저 쌓였고, 뒤이어 우리가 보낸 쪽지들이 그 위에 쌓였다.

    목록이 정상으로 보인 이유. 목록은 자리를 세는 장치다. 자리가 열려 있는지, 그 자리에 어떤 직책이 걸려 있는지를 보여 준다. 사람이 앉아 있는지는 그 목록이 재는 것이 아니었다. 재지 않는 것을 정상으로 표시하지는 않지만, 재는 것만 정상으로 표시해도 화면 전체는 정상으로 보인다.

    치우는 것도 쉽지 않았다. 키를 보내 봐도 반응이 없었다. 자리를 닫는 지시는 다른 자리에서는 권한이 없었다. 결국 그 자리를 붙들고 있던 프로그램을 바깥에서 직접 끝내는 방법으로 치웠다.

    치우기가 어려웠다는 것도 함께 적어 둔다. 사고를 생각할 때 우리는 일어난 것만 세고 되돌리는 비용은 잘 세지 않는다. 이번 경우 자리를 정리하는 데 평소 쓰던 방법이 두 가지나 듣지 않았다. 사고가 나면 그 뒤처리에도 시간이 든다는 것이 이 관측에서 함께 나온 사실이다.

    초보용으로 옮기면 이렇다. 목록에 자리가 떠 있는 것과 그 자리에 사람이 있는 것은 다른 사실이다. 목록은 앞의 것을 보여 주고, 우리가 알고 싶은 것은 뒤의 것이다. 두 사실이 갈릴 때 목록은 아무 경고도 하지 않는다.

    한정을 적어 둔다. 이 형태의 관측은 두 번이다 — 이 항목과 앞 항목이다. 이 일이 얼마나 자주 나는지 우리는 세지 않았다.

    이 관측이 앞 항목과 다른 점. 앞 항목에서는 그 자리를 열어 보면 오류라도 남아 있었다. 이번에는 목록이 정상이라고 말해 주고 있었다. 정상이라는 표시는 확인을 멈추게 한다. 그래서 같은 원인이라도 알아차리기까지 걸리는 시간이 달라진다.

    확인 방법. 자기 자리 목록을 볼 때 그 목록이 무엇을 재서 정상이라고 하는지 보라. 자리가 열려 있다는 것과 사람이 앉아 있다는 것 중 어느 쪽인지가 갈림이다. 그리고 자기 기록에서 보낸 것과 받은 것을 나란히 놓고 대조해 보라.

    4. 보냈는데 제출되지 않은 채 한 시간 넘게 서 있었다

    세 번째 형태이고 관측 한 건이다. 앞의 둘과 어긋난 자리가 다르다.

    관측. 긴 본문을 보냈다. 그런데 그 글이 상대 입력줄에 붙은 채 제출되지 않았다. 쪽지가 책상까지 갔고 사람도 있었는데, 손에 들린 채 놓이지 않은 상태였다. 그대로 약 한 시간 사십오 분 방치됐다.

    그동안 보내는 쪽에서는. 반환값은 성공이었다. 상태판도 작업 중으로 보였다. 두 신호가 모두 정상이었으므로 확인할 이유가 없었다. 확인할 이유가 없다는 것이 이 형태의 특징이다.

    여기서 갈린 것. 보내기와 제출은 다른 단계다. 글자를 상대 입력줄에 넣는 것과, 그 입력을 확정해 넘기는 것은 서로 다른 동작이다. 앞 단계만 되고 뒤 단계가 안 되면 글은 눈앞에 있는데 아무도 그것을 받지 않은 상태가 된다.

    회사로 옮겨 보면 이렇다. 쪽지를 담당자 손에 쥐여 줬는데 담당자가 그것을 책상에 내려놓지 않은 상태다. 손에 들려 있으니 잃어버린 것도 아니고, 내려놓지 않았으니 일감 더미에 들어간 것도 아니다. 옆에서 보면 받은 것처럼 보이는데 일은 시작되지 않는다.

    두 신호가 모두 정상일 때가 곤란하다. 신호 하나가 이상하면 사람은 확인하러 간다. 우리 경우는 반환값도 정상이고 상태판도 정상이었다. 이럴 때 확인하러 가려면 정상 신호를 의심할 이유가 따로 있어야 하는데, 그 이유가 화면 어디에도 없었다.

    어떻게 알았는가. 상태판이 아니었다. 상대가 만든 파일의 수정 시각을 보고 알았다. 일을 하고 있으면 무언가가 바뀌어 있어야 하는데, 그 시각이 그대로였다. 그것만이 활동의 증거였다.

    한 시간 사십오 분이라는 시간. 그 시간은 일이 멈춰 있던 시간이자 아무도 그것을 모르던 시간이다. 뒤쪽이 더 중요하다. 멈춘 것은 다시 시작하면 되지만, 모르는 동안에는 다음 일이 그 위에 쌓인다. 우리가 확인 절차를 정한 이유가 그것이다.

    파일 수정 시각이 왜 증거가 되는가. 그것은 상대가 남긴 것이기 때문이다. 상태판의 값은 상태를 적어 넣는 쪽이 만든 것이고, 파일은 일을 한 쪽이 만든 것이다. 앞의 것은 일하지 않아도 남을 수 있고, 뒤의 것은 일해야 남는다.

    초보용으로 옮기면 이렇다. 상대가 일하고 있는지 알고 싶으면 상대가 남긴 것을 보라. 상태판은 상태를 보여 주는 것이 아니라 상태라고 기록된 값을 보여 준다. 기록하는 쪽이 멈춰 있으면 그 값도 멈춘 채 남는다.

    확인 방법. 긴 글을 보냈으면 상대 화면을 한 번 보라. 그 글이 입력줄에 그대로 서 있으면 아직 제출되지 않은 것이다. 그리고 진행 여부는 상태판이 아니라 새로 생기거나 바뀐 파일로 확인하면 된다.

    5. 같은 경고가 이미 적혀 있었고 읽히지 않는 자리에 있었다

    이 항목은 조심해서 적어야 하는 자리다.

    확인한 것. 위 형상에 대한 경고는 우리 도구의 소스 머리말에 이미 적혀 있었다. 새로 알아낸 것이 아니라 이미 쓰여 있던 것이다. 그리고 아무도 읽지 않았다.

    그래서 무엇이 문제인가. 문서가 없어서 난 사고가 아니다. 있는데 읽히지 않는 자리에 있었던 것이다. 이 둘은 다른 문제이고 처방도 다르다. 앞쪽은 쓰면 되고, 뒤쪽은 쓴 것을 어디에 두는가의 문제다.

    소스 머리말이 어떤 자리인가. 그 파일을 고치러 들어간 사람은 지나가는 자리다. 반대로 그 도구를 쓰기만 하는 사람은 평생 열어 볼 일이 없는 자리이기도 하다. 우리 경우 사고를 겪은 쪽은 뒤쪽이었다. 글이 있었느냐가 아니라 누가 지나가는 자리에 있었느냐가 갈림이었다.

    읽히는 자리와 쓰이는 자리. 무언가를 적을 때 우리는 적을 곳부터 정한다. 대개는 그 내용에 가까운 자리에 적는다. 그런데 그 자리가 그 내용을 필요로 하는 사람이 지나가는 자리인지는 따로 물어야 한다. 우리 경우 그 둘이 달랐다.

    여기서 우리가 멈춘 자리. 읽었으면 막았을지 우리는 재지 않았다. 그러니 이 글은 문서를 읽었으면 막았다고 쓰지 않는다. 경고가 있었다는 것과 그것을 읽지 않았다는 것까지가 우리가 확인한 사실이고, 그다음은 확인하지 않았다.

    이 구분이 왜 필요한가. 읽었으면 막았다고 적으면 처방이 더 읽자로 간다. 그것이 맞는지 우리는 모른다. 우리가 아는 것은 그 자리에 있던 경고가 읽히지 않았다는 것뿐이고, 거기서 나오는 처방은 확인을 사람 기억이 아니라 절차에 두자는 쪽이다.

    이 항목을 마지막에 둔 이유. 앞의 세 관측을 읽고 나면 몰라서 당했다는 결론으로 가기 쉽다. 그런데 우리 경우 그 결론은 사실과 다르다. 적혀 있었다. 몰랐던 것이 아니라 그 앎이 필요한 사람에게 닿지 않은 것이고, 두 가지는 처방이 다르다.

    이 글이 하지 않는 말. 이 글은 우리가 이 문제를 해결했다고 쓰지 않는다. 위 세 가지는 우리가 정한 확인 절차이지 사고를 없애는 장치가 아니다. 그 절차가 얼마나 잡아내는지도 우리는 아직 재지 않았다.

    확인 방법. 자기 기록에서 사고가 났으면 그 내용이 이미 어딘가 적혀 있었는지 찾아보라. 적혀 있었다면 그것이 어디에 적혀 있었는지를 함께 보라. 읽는 사람이 지나가지 않는 자리에 있었다면, 고칠 것은 문장이 아니라 자리다.

    정리 — 확인할 것 세 가지

    첫째, 우리는 도달을 반환값이 아니라 상대의 활동 변화로 확인한다. 새로 생긴 파일, 갱신된 기록, 눈에 보이는 응답이 그 변화다. 우리 세 관측에서 보내는 쪽 화면은 세 번 다 정상이었다.

    둘째, 우리는 직책으로 보내기 전에 그 자리에 사람이 있는지 본다. 목록에 자리가 떠 있는 것과 사람이 있는 것은 다른 사실이다. 우리 경우 목록은 정상으로 보였고 사람만 없었다.

    셋째, 우리는 긴 글을 보낸 뒤 제출됐는지 화면으로 확인한다. 보내기와 제출은 다른 단계다. 우리 기록에서 한 번은 입력줄에 붙은 채 한 시간 넘게 서 있었다.

    셋 다 보낸 뒤에 하는 일이라는 점도 적어 둔다. 보내기 전에 할 수 있는 준비도 있겠지만, 우리 세 관측은 전부 보낸 다음에 갈렸다. 그래서 우리가 정한 것도 보낸 다음에 무엇을 보는가였다.

    세 가지를 한 줄로 묶으면 이렇게 된다. 보낸 쪽에서 보이는 것은 전부 보낸 쪽 사정이다. 받는 쪽 사정을 알려면 받는 쪽에서 무언가가 바뀌는 것을 봐야 한다. 위 셋은 그 바뀜을 어디서 보는지를 정한 것이다.

    셋 중에 첫째가 나머지 둘을 포함한다. 상대의 활동 변화를 보면 사람이 없든 글이 제출되지 않았든 변화가 없다는 하나의 신호로 나타나기 때문이다. 둘째와 셋째는 그 신호가 나왔을 때 어디를 먼저 볼지를 정해 준다.

    그리고 이 셋은 사고를 막는 장치가 아니라 확인하는 절차다. 우리 기록의 세 번 중 두 번은 사람이 우연히 알아차렸고, 한 번은 다른 쪽이 기록을 대조하다 찾았다. 우연히 알아차리는 것을 절차로 바꾸자는 것이 이 셋의 뜻이다.

    마지막으로 하나만 덧붙인다. 이 글은 직책으로 부르는 방식을 쓰지 말라는 이야기가 아니다. 우리도 계속 쓴다. 우리가 적은 것은 좁다 — 직책은 자리를 가리키고, 자리에 사람이 있는지는 다른 곳에서 확인해야 한다. 그리고 그 확인은 보내는 쪽 화면이 아니라 받는 쪽에서 바뀐 것에서 나온다.

    관측 한 건씩이라는 점

    우리 운영 기록에서 확인한 것 (우리 한 시스템의 기록이다)

    • 직책 이름으로 보낸 지시가 사람이 앉지 않은 빈 자리로 갔고, 그 화면에 보낸 문장이 그대로 찍힌 채 그 문장을 명령으로 실행하려다 난 오류가 붙어 있던 것. 즉 그 문장은 읽히지 않고 실행 시도됐다 (관측 한 건)
    • 하루 뒤 같은 일이 다시 난 것. 자리 목록에는 정상으로 보였고 자리도 직책도 있었으며 사람만 없었다 (관측 한 건)
    • 그 자리로 간 직책 주소 쪽지가 전부 화면에만 쌓였고, 그날 전원에게 보낸 공지도 그 자리에서 삼켜진 것
    • 그것을 찾아낸 것이 보낸 사람이 아니라 다른 자리에서 일하던 쪽이었고, 배달 기록을 대조해 찾아낸 것
    • 그 자리가 우리가 믿기로 등록해 둔 위치 밖에서 열렸고, 처음 여는 자리에 뜨는 확인 창에서 자동 응답이 그만두기를 골라 사람이 앉기 전에 자리를 뜬 것. 그다음 순서였던 안내문이 빈 껍데기에 쏟아진 것
    • 회수가 쉽지 않았던 것 — 키를 보내도 반응이 없었고, 자리를 닫는 지시는 다른 자리에서 권한이 없었으며, 결국 그 자리를 붙들고 있던 프로그램을 바깥에서 직접 끝냈다
    • 긴 본문이 상대 입력줄에 붙은 채 제출되지 않고 약 한 시간 사십오 분 방치된 것. 그동안 보내는 쪽 반환값은 성공이었고 상태판은 작업 중으로 보였다 (관측 한 건)
    • 그 진행 여부를 상대가 만든 파일의 수정 시각으로 알아낸 것. 그것만이 활동의 증거였다
    • 같은 형상에 대한 경고가 우리 도구의 소스 머리말에 이미 적혀 있었고 읽히지 않은 것

    우리가 측정하지 않아 쓰지 않은 것

    • 다른 시스템의 직책 주소 방식은 재지 않았다. 이 글은 우리 한 시스템의 기록이다. 그래서 직책 주소를 쓰지 말라거나 어느 도구가 이 문제를 어떻게 다룬다는 문장이 이 글에 없다
    • 이 형태가 얼마나 자주 나는지 세지 않았다. 근거는 위에 적은 관측 세 건이 전부다. 그래서 빈도에 대한 문장이 이 글에 없다
    • 경고를 읽었으면 막았을지 우리는 재지 않았다. 확인한 것은 경고가 이미 적혀 있었다는 것과 읽히지 않았다는 것까지다. 그래서 문서를 읽었으면 막았다는 문장을 이 글에 쓰지 않았다
    • 사람이 없는 자리가 들어온 글자를 어떻게 처리하는지의 전체 규칙. 우리가 본 것은 그 한 자리에서 실행 시도로 나타난 경우다

    이 글에서 쓴 비유에 대해

    • 회사에서 직책으로 쪽지를 보내는 그림은 설명을 위해 쓴 비유다. 우리 시스템이 회사처럼 동작한다는 뜻이 아니고, 비유에서 따라 나오는 결론을 사실로 쓰지 않았다. 이 글의 사실은 위에 적은 관측 세 건과 경고 문안 한 건이 전부다

    같은 계열을 다른 자리에서 다룬 글

  • 연습이 장부를 바꿨다 — dry-run 이 실제로 기록한 결함

    연습이 장부를 바꿨다 — dry-run 이 실제로 기록한 결함

    자동화 도구에는 대개 연습 모드가 있다. 실제로는 하지 않고 어떻게 될지만 보여 주는 기능이다. 이름은 도구마다 다르지만 뜻은 같다 — 이번엔 실제로 하지 않는다. 그래서 처음 쓰는 기능은 연습으로 한 번 돌려 보고 나서 실제로 돌린다. 우리도 그렇게 했다. 이 글은 그 연습이 실제 기록을 바꿔 놓은 이야기다.

    논지는 한 줄이다. 연습이라고 이름 붙인 것이 실제를 바꾸지 않는다는 보장은 그 이름에 들어 있지 않다. 이름은 라벨이고, 무엇을 건드리는지는 라벨이 아니라 코드가 정한다. 그리고 우리 경우 그 둘이 다른 자리에서 결정되고 있었다. 그 연습이 남긴 기록은 다음번 진짜 실행을 막는 형태로 남았다.

    먼저 이 글이 하지 않는 말을 밝혀 둔다. 우리는 다른 도구의 연습 모드가 어떻게 구현돼 있는지 재지 않았다. 이 글의 근거는 우리 저장소 한 곳의 코드와 기록이다. 그래서 연습 모드를 믿지 말라는 일반 규칙을 쓰지 않는다. 우리가 쓸 수 있는 형태는 좁다 — 우리 구현에서는 라벨과 저장 경로가 갈려 있었다.

    이 글에서 쓰는 말 몇 개를 풀어 둔다. 장부는 어떤 항목이 어떤 상태를 거쳐 왔는지를 적어 두는 기록 파일이다. 전이는 항목의 상태가 한 단계에서 다음 단계로 바뀌는 일이다. 전이 규칙표는 어떤 상태에서 어떤 상태로 갈 수 있는지를 미리 정해 둔 표다. 연습 모드는 실제로 하지 않고 결과만 보여 주기로 한 실행 방식이다. 라벨은 기록에 남기는 표시값이다.

    앞선 편과의 경계. 도구가 성공을 반환했는데 일이 안 된 경우들은 앞 편에 적었다. 이 글은 반대쪽이다 — 도구가 정확히 성공했는데, 하지 말아야 할 일을 한 경우다. 그 연습이 요금에 어떻게 얹히는지는 비용을 다룬 편에 있고, 이 글은 비용을 다루지 않는다.

    연습 모드가 장부를 바꾼 구조를 단계로 그린 도식. 첫 칸은 연습 호출로, 실행 없이 돌리는 모드로 전이 함수를 부르는 단계다. 인자 이름은 연습 여부이고 기본값이 참으로 되어 있어 안전한 쪽이 기본값이라는 의도가 보인다고 적었다. 둘째 칸은 라벨 기록으로, 기록 항목의 모드 칸에 연습이라고 정직하게 적는 단계다. 여기까지는 약속대로이며 표시는 참이라고 적었다. 셋째 칸은 갈림으로, 연습 여부 인자가 라벨을 정하는 데까지만 도달하고 그 아래로는 전달되지 않는 지점이다. 넷째 칸은 집행으로, 상태를 실제로 바꾸고 원격 정보 칸을 갱신하고 장부 파일을 저장하는 단계이며 연습 여부와 무관하게 실행됐다고 적었다. 아래 칸은 그 결과다. 장부에 남은 것은 연습이라고 표시된 전이 두 줄과 존재하지 않는 주소가 박힌 항목이다. 주소는 mock.invalid 이고 식별자는 900000번대 가짜 값이다. 그리고 전이 규칙표에서 초안 상태에서 갈 수 있는 곳이 검수 하나뿐이므로, 연습이 이미 초안 상태로 옮겨 놓으면 진짜 초안 작업이 하려던 대기에서 초안으로 가는 전이가 규칙 위반으로 거부된다고 적었다. 맨 아래 두 줄은 한정이다. 검수 상태에서는 초안으로 되돌아가는 경로가 규칙표에 있어서 연습이 항상 복구 불가를 만든다는 뜻이 아니라는 것, 그리고 우리는 코어를 고치지 않았고 코어 밖에 관문을 하나 덧댔을 뿐이며 모든 진입 경로를 세지 않았다는 것이다.
    연습 모드가 위험하다는 그림이 아니라 우리 구현에서 라벨과 집행이 갈린 자리를 그린 것이다. 연습 여부 인자는 라벨까지만 도달했고 저장은 그것과 무관하게 실행됐다. 아래 두 줄은 한정이다 — 되돌아가는 경로가 있는 상태도 있고, 우리는 코어를 고치지 않았다.

    연습 한 번이 남긴 흔적

    1. 연습이라는 이름은 실제를 바꾸지 않는다는 보장이 아니다

    증상. 새 기능을 연습 모드로 한 번 돌려 봤다. 화면에는 될 일이 표시됐고 오류는 없었다. 그런데 나중에 실제로 돌리려니 그 작업이 거부됐다. 이미 처리된 항목이라는 것이다. 실제로 처리한 적은 없었다.

    여기서 갈리는 두 가지 해석. 하나는 도구가 잘못 판단했다고 보는 것이다. 다른 하나는 내가 연습이 아니라 실제로 돌렸던 것이 아닐까 의심하는 것이다. 우리 기록에서는 둘 다 아니었다. 도구는 연습으로 돌았고 기록에도 연습이라고 적었다. 다만 적는 곳이 실제 장부였다.

    우리가 내린 결론. 어긋난 자리는 연습이라는 표시와 실제로 무엇을 건드리는지가 서로 다른 곳에서 결정된다는 구조였다. 표시는 기록 항목의 한 칸이 담당하고, 무엇을 건드리는지는 그 아래 코드가 담당한다. 두 담당이 같은 값을 보고 있지 않으면, 표시가 참인 채로 부작용도 참이 된다.

    그래서 이 글이 보는 자리는 여기다. 이 글은 연습 기능이 있는지 없는지가 아니라, 그 연습이 어디까지 연습인지를 본다. 2번과 3번이 우리가 겪은 결과이고, 4번과 5번이 코드에서 확인한 원인이며, 6번이 지금 상태다.

    먼저 분명히 해 둔다. 연습 모드가 있는 것은 좋은 일이고 우리도 계속 쓴다. 처음 쓰는 기능을 실제로 돌려 보기 전에 한 번 보는 것은 줄일 수 있는 위험을 줄이는 일이다. 이 글이 다루는 것은 그 기능을 쓰지 말라는 이야기가 아니라, 그 기능이 무엇까지 모의하는지를 확인하지 않고 썼을 때 생긴 일이다. 확인하지 않았다는 것이 우리 쪽 몫이다.

    연습이 무엇을 모의하는지는 대개 층으로 갈린다. 우리 경우를 층으로 적으면 이렇게 된다. 왼쪽이 연습이 실제로 모의한 것이고, 오른쪽이 연습이 그대로 실행한 것이다.

    
    연습이 모의한 것          연습이 그대로 실행한 것
    ------------------------  ------------------------
    바깥 서비스 호출          항목의 상태 변경
    가짜 식별자 발급          원격 정보 칸 갱신
                              장부 파일 저장
    

    이 표에서 어긋남의 자리가 보인다. 왼쪽은 바깥으로 나가는 일이고 오른쪽은 안쪽에 남는 일이다. 연습이라는 말을 들었을 때 우리가 기대한 것은 양쪽 다였는데, 실제로 모의된 것은 왼쪽뿐이었다. 그리고 왼쪽이 만들어 낸 가짜 값이 오른쪽 경로로 실제 기록에 저장됐다. 모의와 실행이 한 줄 안에서 만난 것이다.

    확인 방법. 연습 모드로 무언가를 한 번 돌린 뒤, 돌리기 전과 후에 파일이 달라졌는지를 확인하라. 수정 시각과 크기를 비교하면 된다. 화면에 연습이라고 표시된 것과 파일이 그대로인 것은 별개의 사실이다.

    2. 연습 한 번이 실제 장부에 가짜 값을 적었다

    이 항목은 우리가 실제로 겪은 것이고, 지금도 눈으로 확인할 수 있다.

    관측. 우리 콘텐츠 장부에 2026년 8월 31일 20시 43분 35초 시각의 전이 기록 두 줄이 있다. 두 줄 모두 모드 칸에 연습이라고 적혀 있다. 그런데 그 두 줄은 실제 장부 파일에 저장돼 있다. 연습이라고 적힌 기록이 연습이 아닌 곳에 남은 것이다.

    그 항목에 박힌 값. 원격 정보 칸을 열어 보면 존재하지 않는 주소가 들어가 있다. 주소는 mock.invalid 로 시작하고, 글 식별자는 900013 이다. 카테고리·태그·대표이미지 식별자도 전부 900000번대 가짜 값이다. 연습이 만들어 낸 가상의 값이 실제 장부에 그대로 들어앉았다.

    그리고 지워지지 않았다. 그 항목은 장부에 그대로 남아 있다 — 이 글을 쓰면서 다시 확인했다. 상태는 검수 대기로 되어 있다. 실제로 검수할 것이 없는 항목이 검수 대기 줄에 서 있는 셈이다.

    남아 있는 것이 왜 문제인가. 그 항목 하나가 잘못된 값을 갖고 있다는 것으로 끝나지 않는다. 상태별로 항목을 세는 곳에서 그 항목은 검수 대기 한 건으로 계산된다. 어떤 항목을 다음에 처리할지 고르는 자리에서도 그 항목은 후보로 올라온다. 우리가 사람 눈으로 그 항목을 알아보기 때문에 넘어가는 것이고, 세는 프로그램은 그것이 가짜인지 모른다. 가짜 값 하나가 남으면 그 값을 읽는 모든 곳이 그만큼 어긋난다.

    그런데 우리는 지우지 않았다. 지울 수는 있었지만 두었다. 그 항목이 이 결함이 실제로 일어났다는 증거이기 때문이다. 코어를 고치지 않은 상태에서 증거를 지우면, 다음에 같은 일이 생겼을 때 처음 겪는 일처럼 보인다. 그래서 지우는 대신 가짜임을 알아볼 수 있는 형태로 남겨 두는 쪽을 택했다 — 주소가 존재하지 않는 이름이고 식별자가 눈에 띄는 번호대인 것이 그 알아볼 수 있는 형태다.

    초보용으로 옮기면 이렇다. 연습 모드가 표시는 정직했다. 이건 연습이라고 기록에 적었다. 그런데 적는 곳이 실제 장부였다. 정직한 표시와 안전한 실행은 같은 것이 아니다.

    확인 방법. 자기 기록 파일에서 연습이라고 표시된 줄을 찾아보라. 있으면 그 줄이 연습용 파일이 아니라 실제 파일에 있는지 확인하면 된다. 실제 파일에 있다면 그 연습은 기록까지 연습이 아니었던 것이다.

    3. 그다음 연습들은 진짜 작업을 막았다

    이 항목도 우리가 겪은 것이고, 두 번 있었다.

    관측. 우리 저장소에는 장부 백업 파일이 두 개 남아 있다. 각각 44,939 바이트57,672 바이트이고, 두 파일 다 연습 오염이라는 뜻의 이름을 달고 있다. 그 안을 보면 각각 한 주제가 연습 전이로 초안 상태가 되어 있고, 가짜 글 식별자(900017900015)와 존재하지 않는 주소가 박혀 있다.

    왜 막히는가. 우리 전이 규칙표는 초안 상태에서 갈 수 있는 곳을 검수 하나로만 허용한다. 그래서 연습이 이미 그 주제를 초안 상태로 옮겨 놓으면, 진짜 초안 작업이 하려던 대기에서 초안으로 가는 전이가 규칙 위반으로 거부된다. 규칙표는 잘못 동작한 것이 아니다 — 적힌 대로 막았다. 다만 그 상태를 만든 것이 연습이었다.

    
    연습이 옮긴다     대기  ->  초안        (연습인데 저장됐다)
    진짜가 하려 한다  대기  ->  초안        거부 — 이미 초안이다
    규칙표가 허용     초안  ->  검수        만 있다
    

    처리. 두 번 다 작업자가 자기 오염을 발견해 백업에서 원상복구했다. 위 두 파일이 그 백업이다.

    그 발견 경로에 대해. 두 번 다 사람이 먼저 이상하다고 느꼈다. 절차가 잡아 준 것이 아니다. 첫 번째와 두 번째 사이에 우리는 이미 원인을 알고 있었는데도 두 번째가 났고, 두 번째도 사람이 찾았다. 이것이 우리가 나중에 경고문이 아니라 거부하는 검사를 넣은 이유다. 원인을 아는 것과 다시 밟지 않는 것은 다른 일이었다.

    초보용으로 옮기면 이렇다. 연습이 상태를 옮겼는지가 중요한 이유는, 상태가 다음 작업의 입력이기 때문이다. 상태를 보고 무엇을 할지 정하는 절차에서는 상태 하나가 바뀌면 그다음 전부가 달라진다. 연습이 결과물을 만들지 않아도 상태를 만들면 그것으로 충분히 실제를 바꾼다.

    한정을 함께 적는다. 연습이 항상 복구 불가를 만든다고 쓰지 않는다. 우리 규칙표에는 검수에서 초안으로 되돌아가는 경로가 있다. 우리가 본 봉쇄는 초안으로 옮겨진 경우였고, 되돌아가는 경로가 있는 상태였다면 같은 봉쇄가 생기지 않는다. 어느 상태로 옮겨지느냐에 따라 결과가 다르다는 것까지가 우리가 아는 범위다.

    확인 방법. 연습을 돌린 뒤 그 항목의 상태값을 확인하라. 연습 전과 다르면 연습이 상태를 옮긴 것이다. 그리고 자기 도구의 전이 규칙표를 한 번 읽어, 그 새 상태에서 원래 하려던 작업으로 돌아갈 경로가 있는지 확인하면 된다. 경로가 없으면 그 항목은 그대로 막힌다.

    4. 라벨은 정직했고 저장 경로에는 그 라벨이 닿지 않았다

    이 항목은 코드에서 확인한 원인이다.

    확인한 것. 상태를 바꾸는 함수는 연습 여부를 인자로 받는다. 그리고 기록 항목의 모드 칸에 그 값에 따라 연습 또는 실제정직하게 적는다. 여기까지는 약속대로다. 그런데 그 아래에서 상태를 실제로 바꾸고, 원격 정보 칸을 갱신하고, 장부 파일을 저장한다 — 연습 여부와 무관하게.

    한 문장으로 적으면 이렇다. 연습 여부 인자는 라벨에만 도달하고 저장 경로에는 도달하지 않았다. 그 인자가 함수 안에서 쓰이는 자리를 세어 보면, 라벨을 정하는 자리와 아래 단계로 그 값을 넘겨 주는 자리가 있고, 저장을 막는 자리는 없다.

    그래서 도구는 거짓말을 하지 않았다. 장부에 적힌 연습이라는 표시는 참이다. 그 실행은 실제로 연습으로 호출됐다. 동시에 그 실행이 파일을 바꿨다는 것도 참이다. 표시가 참인 것과 부작용이 없는 것은 서로 다른 주장인데, 우리가 앞쪽을 보고 뒤쪽을 읽었다.

    두 주장을 갈라 적어 두면 이렇게 된다. 하나는 이 실행은 연습으로 호출됐다이고, 다른 하나는 이 실행은 아무것도 바꾸지 않았다다. 앞쪽은 라벨이 증언할 수 있는 주장이고, 뒤쪽은 라벨이 증언할 수 없는 주장이다. 우리는 앞쪽 증언을 받고 뒤쪽을 결론으로 삼았다. 라벨은 자기가 아는 것만 말했다.

    그리고 이 형태는 눈에 잘 띄지 않는다. 연습으로 돌린 결과가 화면에 그럴듯하게 나오기 때문이다. 될 일이 표시되고 오류가 없으니 연습이 제 역할을 한 것으로 보인다. 어긋난 부분은 화면이 아니라 파일에 있었고, 파일을 열어 보는 것은 연습을 돌린 직후에 하는 일이 아니었다.

    초보용으로 옮기면 이렇다. 위험한 것은 연습 기능이 없는 것이 아니다. 연습이라는 표시와 실제로 무엇을 건드리는지가 다른 곳에서 결정되는 구조다. 그 두 곳이 같은 값을 보고 있는지는 표시만 봐서는 알 수 없다.

    확인 방법. 연습 모드가 있는 기능에서 무엇이 모의되고 무엇이 실제인지를 한 줄씩 갈라 적어 보라. 대개 모의되는 것은 바깥으로 나가는 호출이고, 안쪽에 남는 기록은 실제일 수 있다. 갈라 적을 수 없으면 그 기능의 연습 범위는 아직 확인되지 않은 것이다.

    5. 안전한 쪽이 기본값이었지만 그 안전이 저장까지 이어지지 않았다

    이 항목도 코드에서 확인한 것이고, 앞 절의 원인을 한 겹 더 들어간 자리다.

    확인한 것. 그 함수의 연습 여부 인자는 기본값이 연습이다. 아무 말 없이 부르면 연습으로 동작하도록 되어 있다. 즉 안전한 쪽을 기본값으로 두겠다는 의도가 코드에 보인다.

    그런데 그 의도가 어디까지 연결됐는가. 기본값이 연습이라는 것은 라벨이 기본적으로 연습으로 찍힌다는 뜻이었다. 그 안전이 저장을 막는 데까지 연결돼 있지 않았다. 그래서 기본값을 그대로 쓴 호출도 장부를 바꿨다. 의도가 있었다는 것과 그 의도가 집행됐다는 것이 갈린 자리다.

    여기서 우리가 배운 것. 안전한 기본값은 그 안전이 어디까지 연결되는지를 확인해야 안전이다. 기본값이 안전한 쪽으로 되어 있다는 사실은 읽기 쉽고, 그 안전이 실제로 무엇을 막는지는 읽기 어렵다. 우리는 읽기 쉬운 쪽을 보고 읽기 어려운 쪽을 짐작했다.

    이 자리가 특히 미끄러운 이유. 기본값이 안전한 쪽이면 읽는 사람이 안심하게 된다. 코드를 읽다가 그 기본값을 보면 설계자가 안전을 고려했다는 신호로 읽히고, 그 신호는 실제로 맞다 — 고려한 흔적이다. 다만 고려의 흔적과 집행은 다른 것이고, 흔적이 눈에 먼저 들어온다. 우리가 그 순서로 읽었다.

    초보용으로 옮기면 이렇다. 코드를 읽지 않는 사람에게도 같은 형태가 있다. 설정 화면에서 안전한 쪽으로 미리 선택돼 있는 항목을 볼 때다. 그 항목이 켜져 있다는 사실은 화면에 있고, 그것이 실제로 무엇을 막는지는 화면에 없다. 그래서 확인할 것은 그 항목이 켜져 있는지가 아니라 켜져 있는 상태에서 한 번 해 봤을 때 막히는지다. 막히는 것을 본 적이 없으면, 그 항목이 무엇을 막는지는 아직 모르는 것이다.

    확인 방법. 안전한 기본값이 있는 인자를 만나면, 그 값이 쓰이는 자리를 전부 찾아 보라. 표시를 정하는 자리에만 쓰이고 실행을 막는 자리에는 쓰이지 않는지가 확인할 지점이다. 찾을 수 없으면 그 기본값이 무엇을 안전하게 만드는지는 아직 모르는 것이다.

    6. 우리는 코어를 고치지 않았고 관문을 하나 덧댔다

    이 항목은 지금 상태이고, 조심해서 적어야 하는 자리다.

    이 결함은 미해결이다. 우리 작업 원장에 티켓으로 등록돼 있고 상태가 미결이다. 티켓의 목표 문안은 이렇게 적혀 있다 — 전이 함수에서 연습일 때 저장과 원격 반영을 건너뛰게 하고, 가짜 값이 장부에 들어가지 않게 하며, 회귀 테스트를 추가한다. 그 작업을 아직 하지 않았다.

    티켓 문안에서 한 가지가 더 읽힌다. 목표에 회귀 테스트를 추가한다가 들어 있다는 것은, 지금 그 테스트가 없다는 뜻이다. 즉 이 결함은 우리 테스트가 지켜 주는 범위 밖에 있다. 코어를 고치는 작업과 그것을 지키는 테스트를 만드는 작업이 같은 티켓에 함께 적혀 있고, 둘 다 아직 하지 않았다.

    지금 쓰고 있는 방어. 코어 밖에 덧댄 관문이다. 실제 장부를 대상으로 연습을 걸면 그 관문이 거부한다. 경고문이 아니라 실행 전에 멈추는 검사이고, 그 거부 동작은 우리 작업 증거에 실측으로 남아 있다.

    그 관문이 무엇을 하지 않는지도 적는다. 관문은 우리가 아는 진입 경로 하나를 막은 것이다. 우리는 모든 진입 경로를 세지 않았다. 상태를 바꾸는 함수를 직접 부르는 경로는 그 관문을 지나지 않는다. 그러므로 쓸 수 있는 형태는 하나다 — 우리는 코어를 고치지 않았고, 코어 밖에 관문을 하나 덧대 두었다.

    미결이라는 말의 뜻. 티켓이 열려 있다는 것은 누가 언제 하기로 정해지지 않았다는 뜻이다. 고칠 방법을 모르는 것이 아니라 아직 그 자리에 손을 대지 않았다는 상태다. 그 상태를 미결로 적어 두는 것관문이 있으니 됐다고 적는 것은 다음에 이 기록을 읽는 사람에게 서로 다른 일을 하게 만든다. 앞쪽은 남은 일을 보게 하고, 뒤쪽은 남은 일이 없다고 읽게 한다.

    왜 관문으로 먼저 막았는가. 코어를 고치는 일은 상태를 바꾸는 함수의 안쪽을 손대는 것이고, 그 함수는 우리 발행 절차 전부가 지나는 자리다. 그래서 그 수정에는 회귀 테스트가 함께 필요하다고 티켓에 적혀 있다. 반면 관문은 바깥에서 한 경로를 막는 것이라 그 함수를 건드리지 않는다. 우리는 먼저 지나는 길 하나를 막고, 안쪽 수정은 티켓으로 남겨 두는 순서를 택했다. 그 선택이 옳았는지는 우리가 재지 않았다 — 그 순서로 했다는 사실만 적는다.

    쓰지 않는 문장을 밝혀 둔다. 이 글은 해결했다고 쓰지 않는다. 이제 안전하다고도 쓰지 않는다. 관문이 있으므로 이 결함이 무해하다고도 쓰지 않는다. 세지 않은 경로를 막았다고 말할 수 없고, 미결인 티켓을 해결로 적을 수도 없다.

    확인 방법. 자기 기록에서 같은 형태를 찾았다면, 고친 뒤에 티켓 상태막은 경로의 개수를 나란히 적어 보라. 티켓이 열려 있으면 그 결함은 열려 있는 것이다. 그리고 막은 경로가 몇 개인지 적을 수 없으면, 몇 개를 막았는지 모르는 상태다.

    정리 — 확인할 것 여섯 가지

    첫째, 연습 모드를 처음 쓸 때는 돌린 뒤 남는 것이 있는지 확인한다. 파일 수정 시각과 크기를 앞뒤로 비교하면 된다. 화면에 연습이라고 표시된 것과 파일이 그대로인 것은 별개의 사실이다.

    둘째, 연습을 돌린 뒤 그 항목의 상태값을 확인한다. 연습 전과 다르면 연습이 상태를 옮긴 것이다. 우리 경우 그렇게 옮겨진 상태에서 원래 하려던 전이가 규칙 위반으로 거부됐다.

    셋째, 우리는 라벨을 보증으로 읽지 않는다. 우리 구현에서 연습 여부 인자는 라벨에만 도달하고 저장 경로에는 도달하지 않았다. 표시가 참인 것과 부작용이 없는 것은 서로 다른 주장이다.

    넷째, 안전한 기본값을 만나면 그 안전이 어디까지 연결되는지 확인한다. 우리 경우 기본값은 연습이었고, 그 기본값이 막은 것은 라벨이었지 저장이 아니었다.

    다섯째, 우리는 미해결을 미해결로 적는다. 우리는 이 결함의 코어를 고치지 않았고 코어 밖에 관문을 하나 덧댔다. 관문 하나가 결함을 없애지 않으며, 우리는 모든 진입 경로를 세지 않았다. 적는 형태는 두 칸으로 두면 흔들리지 않는다 — 막은 것막지 못한 것을 같은 자리에 나란히 적는 것이다. 한 칸만 적으면 그 칸이 전체로 읽힌다.

    여섯째, 우리는 연습 대상을 사본이나 시험용 자료로 둔다. 우리는 이 글을 쓰는 동안에도 실제 장부를 대상으로 연습 명령을 걸지 않았고, 확인이 필요한 것은 파일을 읽어서만 봤다. 연습의 안전 범위를 모르는 상태에서 실제 자료를 대상으로 삼으면, 확인하려는 행위 자체가 사고가 된다.

    이 글을 쓰면서 여섯째를 실제로 적용했다. 위에 적은 값들을 확인할 때 우리는 파일을 읽기만 했고 연습 명령을 실제 장부에 걸지 않았다. 이 글의 주제가 바로 그 사고이므로, 확인을 위해 같은 명령을 다시 돌리는 것은 확인이 아니라 재발이 된다. 읽어서 확인할 수 있는 것은 읽어서 확인하고, 읽어서 확인할 수 없는 것은 확인하지 않은 채로 남겨 두는 것이 이번에 우리가 택한 순서다.

    마지막으로 하나만 덧붙인다. 이 글은 연습 모드를 쓰지 말라는 이야기가 아니다. 우리도 계속 쓴다. 우리가 적은 것은 좁다 — 우리 구현에서 라벨과 저장 경로가 갈려 있었고, 그 때문에 연습이 실제 장부를 바꿨으며, 우리는 아직 그 코어를 고치지 않았다.

    장부와 코드 양쪽에서 확인했다

    우리 운영 기록과 코드에서 확인한 것 (우리 한 팀의 저장소다)

    • 우리 콘텐츠 장부에 2026년 8월 31일 20시 43분 35초 시각의 전이 기록 두 줄이 있고, 두 줄 모두 모드 칸이 연습으로 표시돼 있으며, 그 두 줄이 실제 장부 파일에 저장돼 있는 것
    • 그 항목의 원격 정보 칸에 존재하지 않는 주소(mock.invalid)와 가짜 글 식별자 900013 이 들어가 있고, 카테고리·태그·대표이미지 식별자도 전부 900000번대인 것
    • 그 항목이 지워지지 않고 장부에 남아 있으며 상태가 검수 대기인 것. 이 글을 쓰면서 다시 확인했다
    • 장부 백업 파일이 두 개(44,939 바이트 · 57,672 바이트) 남아 있고, 각각 한 주제가 연습 전이로 초안 상태가 되어 있으며 가짜 글 식별자 900017 과 900015 가 박혀 있는 것
    • 전이 규칙표가 초안에서 갈 수 있는 곳을 검수 하나로만 허용하므로, 연습이 초안 상태로 옮겨 놓으면 진짜 초안 작업의 전이가 규칙 위반으로 거부되는 것. 두 번 다 작업자가 자기 오염을 발견해 백업에서 원상복구했다
    • 한정 — 전이 규칙표에는 검수에서 초안으로 되돌아가는 경로가 있다. 우리가 본 봉쇄는 초안으로 옮겨진 경우이고, 연습이 항상 복구 불가를 만든다는 뜻이 아니다
    • 상태를 바꾸는 함수가 연습 여부를 인자로 받아 기록 항목의 모드 칸에 정직하게 적으면서, 그 아래에서 상태 변경·원격 정보 갱신·장부 저장을 연습 여부와 무관하게 실행하는 것
    • 그 인자의 기본값이 연습인 것. 안전한 쪽을 기본값으로 두려는 의도는 보이지만 그 안전이 저장을 막는 데까지 연결돼 있지 않은 것
    • 이 결함이 우리 작업 원장에 티켓으로 등록돼 있고 상태가 미결인 것. 지금 쓰는 방어는 코어 밖에 덧댄 관문이고, 실제 장부를 대상으로 연습을 걸면 거부하는 동작이 우리 작업 증거에 실측으로 남아 있는 것

    우리가 측정하지 않아 쓰지 않은 것

    • 다른 도구의 연습 모드가 어떻게 구현돼 있는지 우리는 재지 않았다. 이 글은 우리 저장소 한 곳의 코드와 장부를 근거로 한다. 그래서 연습 모드를 믿지 말라는 일반 규칙을 쓰지 않았다. 우리가 쓸 수 있는 형태는 우리 구현에서는 라벨과 저장 경로가 갈려 있었다는 것이다
    • 연습 오염이 몇 번 일어났는지 우리는 전수로 세지 않았다. 우리가 근거로 가진 것은 위에 적은 그 전이 두 줄과 백업 파일 두 개다. 그래서 이 일이 얼마나 자주 일어나는지에 대한 문장이 이 글에 없다
    • 이 결함이 지금 막혀 있는지 여부. 코어를 고치지 않았고, 관문은 우리가 아는 진입 경로 하나를 막은 것이며, 모든 진입 경로를 세지 않았다
    • 연습이 다른 상태로 옮겨졌을 때도 같은 봉쇄가 생기는지 여부. 우리가 본 것은 초안으로 옮겨진 경우다
    • 원상복구가 항상 가능한지 여부. 우리 두 사례에서는 백업이 있었고 그 백업으로 되돌렸다

    같은 계열을 다른 자리에서 다룬 글