← 뉴스레터

2026-06-26

ok가 떴다, 도착은 안 했다

받는 분에게 명함을 이메일과 문자 두 경로로 동시에 보냈다. 이메일엔 이미지 첨부가 붙었다. 문자는 스크립트가 처리했고, 실행 결과로 성공을 돌려줬다. 나는 됐다고 생각하고 다음 일로 넘어갔다. 나중에 “명함 이미지가 없었는데요”라는 말을 들었다.


이런 말을 들었을 때의 감각이 있다. “분명히 보냈는데?” 하고 로그를 열어보는 그 짧은 순간. 스크립트는 분명히 성공을 돌려줬다. 나는 그걸 믿었다. 그게 틀렸다.

ok가 뭔지 물어본 적 없었다

자동화 도구를 쓰다 보면 성공 신호를 받는 일에 익숙해진다. 명령이 끝나고 에러 없이 돌아오면, 됐다고 생각하게 된다. 이게 반복되면 “ok가 정확히 어디까지를 보장하는가”를 물어보는 습관이 사라진다. 도구를 신뢰하는 게 아니라 결과 코드를 신뢰하게 되는 것이다.

처음 그 도구를 짰을 때는 ok가 무엇을 의미하는지 정확히 알았다. 이 ok는 “발신 시도가 완료됐다”는 뜻이고, 실제 전달 확인은 별도 단계라는 것도 알고 있었다. 그런데 시간이 지나면서, 그리고 다른 여러 도구들이 쌓이면서, 각 도구의 ok가 어디까지를 커버하는지 흐릿해진다. 어차피 ok가 뜨면 다음으로 넘어갔으니까. 도구들이 많아질수록 각 도구의 경계가 흐려지고, 결국 ok라는 글자 하나가 모든 것을 보장하는 것처럼 느껴진다. 그런 착각이 쌓이는 시간이 있다.

이번엔 그 차이가 표면에 드러났다.

문자로 이미지를 보내는 경로는 단순하지 않았다. 표준 문자 도구는 텍스트 전용이라, 이미지는 별도로 macOS의 스크립팅 레이어를 거쳐 Messages 앱에 첨부하는 방식을 썼다. 코드는 실행됐고, 성공을 리턴했다. 그런데 이미지는 가지 않았다.

샌드박스는 조용히 막는다

왜 그랬는지는 찾아보면 단순했다. 이미지 파일을 /tmp 아래에 뒀는데, macOS의 Messages 앱은 샌드박스 제한 때문에 그 경로를 읽지 못한다. 첨부를 붙이는 코드는 실행됐고, 앱이 그 경로를 거부했으며, 거부는 조용히 일어났다. 스크립팅 레이어는 에러를 돌려주지 않고 그냥 성공을 리턴했다.

이게 문제의 핵심이다. 도구가 ok를 돌려주는 것과, 그 작업이 실제로 완료된 것은 다른 이야기다. ok는 “내가 실행하려고 시도했다”는 신호일 뿐이고, 실제로 상대방에게 도달했는지는 또 다른 확인이 필요하다. 시도의 성공과 전달의 성공은 다른 레이어에 있다.

에러가 조용히 삼켜지는 케이스는 생각보다 많다. 파일 경로가 잘못됐어도, 앱이 거부했어도, 네트워크 경로가 중간에 막혔어도 — ok가 떴다면 우리는 보통 넘어간다. 그리고 나중에 “왜 안 왔냐”는 말을 듣는다. 에러가 터지면 알아채기라도 하지, 조용히 삼켜지면 모른다는 게 더 무섭다. 소음 없는 실패가 가장 위험한 실패다.

같은 날 다른 사고도 있었다. 텔레그램으로 메시지를 보냈는데 아무 응답이 없어서, 터미널로 직접 접속해서 확인해봤더니 그쪽 텔레그램 발신이 계속 타임아웃이 나고 있었다. 다른 기기들은 정상이었는데 그 기기만 POST 요청이 조용히 실패하고 있었다. GET 요청은 200이 떴으니까 “연결은 되는데?”라고 생각하기 쉬운 상황이었다. 실제로는 큰 패킷이 중간에서 조용히 드롭되고 있었던 것이다. 연결됨 ≠ 전달됨이라는 것이 또 다른 형태로 나타난 케이스다.

두 사고가 같은 날 일어났다는 게 우연이 아닌 것 같았다. 구조가 같았으니까.

실제 전달을 확인한다는 것

고쳐서 다시 보냈다. 이번엔 파일을 샌드박스가 읽을 수 있는 경로로 옮겼다. 그리고 보낸 뒤 바로 메시지 DB를 직접 열어서 확인했다. 전송됨, 수신됨 — 이 두 상태가 떴을 때야 비로소 “보냈다”가 “도착했다”로 바뀌었다.

불편한 방식이다. 매번 DB를 열어서 확인하자고 할 수는 없다. 그래서 이게 코드로 가야 한다. 보낸 뒤 자동으로 전달 여부를 검증하고, 실패하면 폴백 경로로 다시 보내는 것 — 그게 도구가 해야 할 일이다. 지금은 그 코드가 없어서 수동으로 확인했고, 수동 확인이 있었기 때문에 ok를 믿지 않을 수 있었다.

전달 여부는 도구 리턴값이 아니라 수신 측 증거로 확인한다. iMessage는 전달 상태 플래그, 텔레그램은 API 응답의 ok와 HTTP 상태코드, 이메일은 message_id — 각 경로마다 증거가 따로 있다. 그 증거를 보기 전까지는 “전송 시도함(미확인)” 상태다.

“보냈다”와 “도착했다”는 두 개의 다른 사실이고, 두 개 다 확인해야 한다. 발신 측 ok로 수신 측 상태를 추론하면 안 된다. 이걸 룰로 박는 게 맞다. “외부발신 후 전달 증거 없으면 미확인으로 분리 보고” — 이 한 줄이 확인 없이 단정하는 사고를 막는다.

자동화가 쌓이면 경계가 흐릿해진다

자동화를 많이 쓸수록 이 문제가 커진다.

도구 하나를 직접 짤 때는 그 도구가 무엇을 하는지 다 안다. 어디서 성공을 돌려주는지, 어디까지를 보장하는지, 어떤 실패 케이스가 있는지. 그런데 도구들이 쌓이면, 각 도구의 경계가 흐릿해진다. “이 도구는 텍스트만 보낸다”는 기억이 “이 도구는 메시지를 보낸다”로 단순화된다. 이미지 첨부는 기억에서 빠진다.

더 진행되면 도구들이 체인을 이룬다. A 도구가 B 도구를 호출하고, B가 C를 호출한다. 각 도구가 ok를 돌려줄 때 어느 레이어의 ok인지가 모호해진다. A의 ok는 A의 실행 성공이지, C의 전달 성공이 아닌데, 체인의 끝에서 ok를 받으면 전체가 됐다고 느끼게 된다.

이게 바이브코딩 방식으로 빠르게 자동화를 쌓을 때 생기는 부채 중 하나다. 속도는 빠른데, 각 레이어의 보장 범위를 정리하는 작업이 뒤처진다. 그러다 어느 날 ok를 믿었는데 안 됐다는 걸 알게 된다.

단순했다 — 막히는 지점은 언제나 단순하다. 파일 경로 하나가 잘못됐고, 샌드박스가 조용히 막았다. 복잡한 버그가 아니라 당연한 제약이었는데 한 번도 확인하지 않았다.

그래서 도구를 쌓을 때 검증 레이어도 같이 쌓아야 한다. 실행 코드 + 검증 코드가 한 유닛이어야 한다. 발신 함수 옆에 전달 확인 함수가 붙어 있고, 실패하면 재시도 또는 폴백으로 이어지는 구조. 이걸 나중에 붙이려고 미루면 도구가 커질수록 리팩토링 비용이 커지고, 결국 안 하게 된다. 처음 짤 때부터 검증이 세트여야 한다.

범위를 넓혀보면

이번 사고는 명함 한 장의 이야기지만, 같은 구조의 실수는 더 넓은 범위에서 일어난다.

앱 스토어에 아이콘을 교체한다. Play Console에 이미지를 올리고 검토 전송 버튼을 누른다. 이건 git 커밋을 남기지 않는다. 배포 스크립트가 서버에 파일을 올리고 “배포 완료”를 출력한다 — 이것도 커밋을 안 남기는 경우가 있다. 뉴스레터 발송, 앱 심사 제출, 스토어 메타데이터 갱신 — 전부 코드 저장소 밖에서 일어나는 일들이다.

다음 세션에서 “이 작업이 됐는지”를 확인할 때 가장 먼저 보는 게 git log다. 커밋이 없으면 “안 한 것”으로 읽히기 쉽다. 메일함을 뒤져도 승인 메일이 없으면 “아직 안 됐나” 싶다. 그런데 실제로는 됐을 수 있다. 그 작업들은 커밋을 남기는 구조가 아닌 거다.

“커밋 없음 = 안 했음” 추론 — 이게 또 다른 단정이고, 또 다른 사고의 씨앗이다. 이 추론은 합리적으로 보인다. 그래서 더 위험하다. “없다는 걸 확인했다”와 “확인 경로가 없어서 못 찾겠다”는 다른 말이다. 그런데 두 상황에서 같은 판단을 내리기 쉽다.

커밋이 없는 행동의 사각지대

완료 마킹 구조가 있다. 작업이 끝나면 할 일 목록에 표시하고, 커밋에 작업 ID를 달아서 자동으로 완료를 추적한다. 이 구조는 커밋이 있는 작업에서는 잘 돌아간다. 커밋이 연결 고리가 되어서 “이 커밋으로 저 작업이 끝났다”를 기계가 추적한다.

그런데 아이콘 교체는 커밋이 없다. 배포 버튼 클릭은 커밋이 없다. 자동 추적 시스템이 아무리 열심히 돌아도, 커밋이 없으면 고리가 없어서 추적이 안 된다. 이런 행동들은 사각지대가 된다.

실제로 있었던 일이다. 야간에 두 앱의 Play Console 아이콘을 교체하고 검토를 전송했다. 커밋은 없었다. repo 변경이 없었으니 없는 게 맞고, Play Console UI 조작이니까 git과 무관한 게 당연하다. 그런데 다음에 “됐는지”를 확인하는 쪽에서 repo 커밋 이력과 승인 메일함을 보고 “처리된 증거를 못 찾겠다”고 보고했다. 실제로는 됐는데.

확인 경로를 빠뜨린 것이다. 커밋이 없을 수 있다는 전제를 빠뜨렸고, 그 전제 없이 “없음 = 안 됨”으로 단정한 것이다. 이 단정이 그냥 묻히면 모르고 지나갈 수 있었는데, 이미 된 것을 다시 하려고 했으면 낭비가 생기거나 오히려 문제가 생길 수도 있다.

행위 장부는 다른 레이어다

그래서 별도의 장부가 필요하다. 커밋 기반 추적이 “코드 변경의 흔적”을 잡는 레이어라면, 행위 장부는 “커밋 없는 외부영향의 흔적”을 잡는 자매 레이어다.

배포했을 때, 제출했을 때, 발송했을 때 — 그 순간 바로 한 줄을 장부에 박는다. 종류(배포인지 제출인지 발송인지), 대상, 타임스탬프. 단순하다. 코드 한 줄이다. 귀찮아서 빠뜨리고 싶은 순간이 생긴다 — 바로 그 순간을 막아야 한다. 습관으로 막으면 바쁠 때 무너지고, 코드로 강제해야 빠지지 않는다. 규칙은 코드입니다.

이 한 줄이 없으면, 다음 세션이 git log와 메일함만 보고 “못 찾겠다, 안 된 것 같다”는 판단을 내린다. 그 판단 위에 또 작업을 쌓으면, 이미 된 것을 다시 하거나 잘못된 전제에서 움직이게 된다. 다음 세션이 나 혼자일 수도 있고, 자동화 루프일 수도 있고, 협업 중인 다른 사람일 수도 있다 — 어느 쪽이든 확인 경로가 없으면 틀린 전제에서 시작한다.

마킹 룰이 커밋 기반 작업을 잡는다면, 장부는 커밋 없는 외부영향을 잡는다. 둘 다 있어야 빈틈이 없다. 첫이름 PDF를 배포하고 장부에 안 박았더니 다음 세션이 “배포됐는지 모르겠다”고 다시 처리하려 했던 일이 있었다. 그 일 이후로 커밋 없는 외부영향은 장부에 박는 게 룰이 됐다.

두 사고는 같은 구조였다

명함 미전송과 스토어 제출 추적 누락 — 처음엔 다른 이야기처럼 보였다. 하나는 메시징 앱 샌드박스 문제고, 하나는 git 기록 부재 문제니까.

그런데 뿌리는 같다. 둘 다 “도구가 ok를 돌려줬으니 됐다”는 전제에서 출발하고, 둘 다 실제 완료의 증거를 따로 확인하지 않았을 때 생긴다. 명함은 발신 도구의 ok를 믿었고, 스토어 제출 추적은 “없으면 안 된 것”이라는 단정을 믿었다.

자동화가 많아질수록 이 구조의 구멍도 함께 늘어난다. 자동화는 인간의 손을 줄이지만, 각 단계에서 “됐는지”를 확인하는 레이어를 알아서 붙여주지는 않는다. 그 레이어는 따로 설계해야 한다. 설계하지 않으면 자동화가 늘어날수록 사각지대도 같이 늘어난다.

ok는 출발점의 신호다. 도착점의 신호가 아니다. 그리고 커밋이 없는 행동은 git이 기억하지 않는다. 이 두 가지를 같이 쥐고 있어야 자동화 위에서 안전하게 움직일 수 있다.

배운 것

도구의 성공 신호는 수신 측 증거가 아니다. ok가 떴어도 전달됐는지는 따로 봐야 한다. 경로마다 증거가 다르고, 그 증거를 보기 전까지는 미확인이다. “전송 시도함(미확인)“과 “도착했다”는 다른 상태다. 이 두 상태를 같은 것으로 묶는 순간 사각지대가 생긴다. 발신 코드 옆에 수신 확인 코드가 붙어 있어야 비로소 한 유닛이 완성된다.

커밋이 없는 외부영향 작업은 git 기록으로 추적이 안 된다. 배포, 제출, 발송은 커밋을 남기지 않는 경우가 많다. 이런 행동은 행위 장부에 한 줄로 박아야 다음 세션이 “됐는지”를 알 수 있다. 장부는 선택이 아니라, 커밋 추적 시스템이 커버하지 못하는 사각지대를 메우는 구조다. 마킹 룰이 코드 변경의 흔적을 잡는다면, 장부는 코드 변경 없는 외부행동의 흔적을 잡는다.

“없음”이 “안 했음”이 아닐 수 있다. git log에 없고 메일함에 없어도 된 것일 수 있다. 확인 경로에 장부가 없으면 부분 확인이고, 부분 확인으로 내린 단정은 틀릴 수 있다. 특히 “못 찾겠다”와 “안 됐다”는 다른 말이다. 못 찾겠으면 확인 경로를 넓히는 게 먼저다.

자동화 도구를 짤 때마다 이 두 질문을 박아두는 게 좋겠다고 생각했다. “이 도구의 ok는 어디까지를 보장하는가?” 그리고 “이 행동이 일어났다는 흔적이 어디에 남는가?” 이 두 질문에 답이 없으면, 나중에 모른다.


ok가 떴을 때, 그 ok가 어디까지를 보장하는지 한 번 더 묻는 것 — 그게 자동화를 제대로 믿는 방식이다.

— 강대종 / @ssamssae