두 AI가 같은 빌드를 올린 밤
앱 업데이트를 준비하던 밤이었다. 한 세션이 빌드를 만들고 스토어 관리 화면에 올렸다. 버전은 1.3.1, 빌드 번호는 15. 메타데이터까지 붙었고, 이제 사람 손으로 눌러야 하는 웹 제출 단계만 남아 있었다. 거기서 멈추는 게 맞았다. 그런데 11분 뒤, 같은 버전의 빌드 16이 하나 더 올라왔다.
처음엔 단순한 재시도인 줄 알았다. 빌드가 실패해서 다시 만든 건가. 업로드가 끊겼나. 아니면 스토어가 늦게 반영되면서 같은 것을 두 개처럼 보여준 건가. 확인해보니 아니었다. 실제로 두 개가 있었다. 빌드 15와 빌드 16. 같은 앱, 같은 마케팅 버전, 다른 빌드 번호.
둘 중 하나는 내가 의도한 산출물이었다. 다른 하나는 고아 빌드였다.
이상한 건 두 세션 모두 자기 입장에서는 틀린 일을 하지 않았다는 점이다. 한 세션은 지시를 받고 빌드를 만들었다. 다른 세션도 지시를 받고 빌드를 만들었다. 각자 로그만 보면 성실했다. 문제는 성실한 두 명이 같은 일을 동시에 한 것이다.
중복은 게으름에서 나오지 않는다
AI 자동화를 운영하다 보면 “일을 안 해서” 생기는 문제보다 “일을 너무 열심히 해서” 생기는 문제가 더 무섭다. 안 한 일은 눈에 보인다. 결과가 없고, 로그가 비어 있고, 물어보면 막힌 지점이 나온다.
같은 일을 두 번 한 것은 더 헷갈린다. 결과가 있다. 심지어 두 개나 있다. 양쪽 모두 완료 보고를 한다. 겉으로 보면 생산성이 좋아 보일 수도 있다. 그런데 스토어 제출 같은 작업에서는 이게 바로 사고다. 올려야 할 빌드가 하나인데 두 개가 생기면, 어느 것이 정본인지 다시 사람이 판단해야 한다.
그날도 그랬다. 빌드 15는 메타데이터까지 붙은 상태였다. 빌드 16은 나중에 올라온 중복 산출물이었다. 다행히 심사 제출 전이라 되돌릴 수 있었다. 중복 빌드는 삭제하지 않고 그냥 고아 상태로 두었다. 없애는 것보다 남겨두고 정본만 확정하는 쪽이 안전했다.
정리 결정은 단순했다. 이 작업의 주인은 한 세션으로 고정한다. 제출 기준은 빌드 15로 한다. 빌드 16은 건드리지 않는다. 여기서 더 진행하지 않고 사람 손이 필요한 웹 단계에서 멈춘다.
겉으로는 작은 정리다. 하지만 이 작은 정리 뒤에는 꽤 큰 교훈이 있었다.
”내가 할게”는 락이 아니다
사람끼리는 “내가 할게”라는 말이 어느 정도 락 역할을 한다. 회의에서 누가 “이건 제가 맡겠습니다”라고 하면 보통 다른 사람은 같은 일을 시작하지 않는다. 사람이 기억하고, 맥락을 보고, 같은 일임을 알아챈다.
AI 세션은 다르다. 세션은 각자 자기에게 보이는 지시와 상태만 보고 움직인다. 한쪽에서 “내가 할게”라고 했다는 사실이 다른 세션에게 자동으로 전달되지 않는다. 전달됐다 해도 그것이 실행 전 차단 조건으로 연결되지 않으면 아무 의미가 없다.
말로 정한 주인은 운영 메모다. 락은 아니다.
락은 실행 직전에 걸려야 한다. 빌드를 시작하기 전에 “이 앱의 이 버전 빌드는 지금 누가 잡고 있는가”를 확인해야 한다. 이미 누가 잡고 있으면 멈춰야 한다. 그 확인이 산문 메모가 아니라 기계가 읽는 장부여야 한다. 같은 노드 안의 두 세션이든, 다른 기계의 세션이든, 모두가 같은 장부를 보고 같은 결론을 내야 한다.
이 사고에는 그런 장치가 없었다. 할 일 번호를 중복으로 만들지 않는 장치는 있었다. PR을 함부로 머지하지 못하게 막는 장치도 있었다. 그런데 “스토어 빌드와 업로드 착수”라는 순간을 직렬화하는 장치는 없었다.
그래서 두 세션이 같은 문을 동시에 통과했다.
스토어 빌드는 로컬 파일이 아니다
코드 빌드만 놓고 보면 중복 실행이 큰 문제가 아닐 때도 있다. 로컬에서 빌드를 두 번 돌리면 시간이 낭비될 뿐, 마지막 산출물이 남는다. 테스트도 두 번 돌릴 수 있다. 문서 렌더도 두 번 할 수 있다.
스토어 업로드는 다르다. 한 번 올라가면 외부 시스템에 흔적이 남는다. 빌드 번호가 올라가고, 처리 상태가 생기고, 이후 제출 화면에서 선택지가 된다. 실제 공개 전이라도 이미 내 컴퓨터 밖에 기록이 생긴 것이다.
그래서 스토어 빌드는 “빌드 명령”이 아니라 “외부 상태 변경”에 가깝다. 로컬에서 파일 하나 만든 것처럼 다루면 안 된다. 시작 전에 더 강한 확인이 필요하다.
나는 이걸 늦게 분리했다. 빌드라는 단어 때문에 로컬 작업처럼 느꼈다. 하지만 앱스토어에 업로드되는 순간부터는 외부 장부에 줄이 하나 생기는 일이다. 그 장부는 내가 마음대로 되감는 곳이 아니다.
이번에는 운이 좋았다. 심사 제출 전이었고, 중복 빌드는 그냥 안 쓰면 됐다. 하지만 같은 패턴이 결제 상품 생성, 공개 업로드, 릴리스 배포에서 터지면 훨씬 지저분해진다. “안 쓰면 됨”으로 끝나지 않을 수 있다.
정본을 고르는 데도 비용이 든다
중복 산출물이 생기면 그 다음엔 정본을 골라야 한다. 이 과정도 비용이다.
두 빌드가 같은 코드인지 확인해야 한다. 어떤 빌드에 메타데이터가 붙었는지 봐야 한다. 제출 화면에서 어느 빌드를 선택해야 하는지 결정해야 한다. 나중에 기록을 읽는 사람이 “왜 빌드 16이 있는데 15를 제출했지?”라고 오해하지 않게 설명을 남겨야 한다.
자동화가 시간을 아끼려고 만든 시스템인데, 중복이 생기는 순간 사람 판단이 다시 들어간다. 그리고 그 판단은 꽤 피곤하다. 잘못 고르면 실제 사용자에게 나가는 버전이 바뀔 수 있기 때문이다.
그래서 중복 방지는 단순한 깔끔함의 문제가 아니다. 사람의 판단 시간을 아끼는 장치다. 누가 먼저 잡았는지, 어떤 산출물이 정본인지, 다음 세션이 다시 묻지 않게 만드는 장치다.
해결책은 사람에게 더 조심하라고 하는 게 아니었다
이런 사고가 나면 쉽게 나오는 말이 있다. “다음부터 중복 배정 조심하자.” 맞는 말처럼 들린다. 하지만 자동화 시스템에서는 별 도움이 안 된다.
조심은 상태가 아니다. 다음 세션은 내가 지금 반성했다는 사실을 모른다. 다른 세션도 모른다. 사람이 피곤하거나 컨텍스트가 끊기면 같은 실수가 다시 난다.
해결책은 빌드 착수 전 lease다. 앱 이름, 버전, 빌드 종류, task id를 묶어 “이 작업은 지금 누가 잡고 있다”를 먼저 기록한다. 이미 살아 있는 lease가 있으면 두 번째 세션은 빌드를 시작하지 못한다. “이미 진행 중입니다”라고 보고하고 멈춘다.
이때 중요한 것은 기록 위치다. 한 세션의 로컬 파일이면 안 된다. 같은 컴퓨터 안의 다른 세션도 보고, 다른 기계도 볼 수 있는 곳이어야 한다. 모두가 지나가는 원격 기록이나 공용 장부가 직렬화 지점이 되어야 한다.
이런 장치는 재미없다. 버튼 하나 늘고, 시작 전에 한 줄 더 확인하고, 실패하면 멈춘다. 하지만 자동화에서 진짜 중요한 건 이런 재미없는 장치다. 멋진 생성보다 중복 실행 방지가 먼저다.
배운 것
두 AI가 같은 일을 동시에 하면, 둘 중 하나가 게으른 게 아니다. 시스템이 주인을 하나로 고정하지 못한 것이다.
“내가 맡았다”는 보고는 락이 아니다. 실행 전 차단 조건으로 연결된 장부만 락이다. 특히 스토어 빌드처럼 외부 시스템에 흔적이 남는 작업은 시작 전에 반드시 잡아야 한다.
그리고 중복 산출물은 그냥 하나 더 생기는 문제가 아니다. 나중에 사람이 정본을 고르게 만들고, 기록을 설명하게 만들고, 다음 세션이 다시 헷갈리게 만든다. 자동화가 아끼려던 시간을 그 자리에서 다시 쓴다.
이후 규칙은 단순해졌다. 외부에 남는 빌드는 만들기 전에 잡는다. 잡지 못하면 만들지 않는다.
AI에게 일을 나눠주는 것보다 먼저 해야 할 일이 있다. 같은 일을 두 명에게 주지 않는 것이다.
— 강대종 / @ssamssae