세션이 끊기면, AI는 잊는다
2026-06-17 새벽 1시, 작업 노드 하나에 앱 배포를 위임했다. 버전 번호 올리고, 릴리스 빌드 만들고, 마켓 내부 트랙에 올리고, 스토어 스크린샷 찍는 것까지. 01시쯤 완료 보고가 올라왔다. 빌드·업로드·스크린샷 전부 끝, 남은 건 심사 제출 클릭 두 개라고. 그런데 10분 뒤, 그 노드가 다시 빌드를 시작했다.
나는 여러 작업 노드에 일을 분배해서 돌린다. 각 노드는 지시를 받아 수행하고 결과를 보고한다. 그날 밤 배포 작업도 그랬다. 빌드 시간 15분, 업로드 한 번, 스크린샷 여러 장 — 번거롭지만 순서대로 하면 되는 일이었다.
문제는 그 과정에서 API가 끊기면서 세션이 죽었다는 것이다. 작업 노드는 잠시 꺼졌다가 다시 켜졌다. 새로 시작된 세션이 이전 핸드오프 메모를 읽었다. “P1 머지게이트 대기 중, 스크린샷 필요” — 그 메모에는 이미 끝난 작업들이 아직 남아 있는 것처럼 적혀 있었다.
결과는 뻔했다. 재진입한 세션이 빌드를 다시 돌리고, 스크린샷을 다시 찍기 시작했다. 텔레그램 알림이 또 왔다. “스크린샷 작업 시작합니다.” 이미 30분 전에 찍은 거였다.
이게 내가 처음 만든 사고 유형은 아닐 거예요. 자동화를 길게 돌리다 보면 세션이 끊기는 일은 얼마든지 생깁니다. API 타임아웃, 네트워크 끊김, 컨텍스트 한도 초과. 어느 이유로든 세션이 끊기고 재진입이 생기는 순간, 기억을 이어받을 외부 구조가 없으면 같은 일이 반복될 수 있어요. 이번에 느낀 건 “왜 이게 발생했냐”가 아니라 “왜 나는 이걸 예상하지 못했냐”였습니다.
기억이 없는 직원에게 매일 같은 지시서를 주면
AI 에이전트의 세션은 사람의 기억이 아니다. 세션이 끊기면 그 안에 있던 것은 사라진다. 새 세션은 빈 상태로 시작해서 외부에 남겨진 글을 읽는 것으로 컨텍스트를 복원한다. 이전 핸드오프 메모, 작업 목록, 계획 문서 — 전부 글이다.
비유하자면 이렇더라고요. 매일 아침 기억을 잃고 출근하는 직원이 있다고 치면, 그 직원의 책상 위에는 어제 내가 써놓은 메모만 있어요. 메모에 “스크린샷 찍기”라고 쓰여 있으면 스크린샷을 찍습니다. 이게 이미 찍힌 건지, 오늘 찍어야 하는 건지 — 메모에 그 구분이 없으니까요. 전날 밤에 끝낸 일인지 오늘 해야 할 일인지, 글에서는 판정이 안 됩니다.
AI 세션이 정확히 그렇게 작동하거든요. 세션 재시작 = 기억 초기화. 외부에 남겨진 글 = 오늘의 지시서. 지시서에 “해야 할 것”과 “이미 한 것”의 구분이 없으면, 재진입한 세션은 모두 “해야 할 것”으로 읽습니다.
이건 AI 잘못이 아니에요. 그렇게 설계되어 있는 거거든요. 잘못이 있다면 지시서 쓴 사람이 그 구분을 안 했다는 거죠. 기억이 끊기는 직원에게 매일 같은 지시서를 주면, 직원은 매일 같은 일을 합니다. 당연하다. 지시서가 “오늘 할 일”인지 “이미 한 일”인지 구분이 없으니까.
사람 직원에게 이렇게 하면 “어제도 이거 했는데요?”라고 말해줘요. 하지만 세션은 어제가 없어요. 모든 재시작은 첫날 출근이에요. 책상 위에 뭔가 쓰여 있으면 그걸 오늘 할 일로 읽는 게 당연한 거예요. 시스템을 설계하는 입장에서 이 구조를 이해하고 대응해야 하는 거죠.
새벽 1시, 한줄일기 1.2.0
그날 위임받은 건 맥미니였습니다. 한줄일기 1.2.0 릴리스 배포 전체였어요. 할 일은 명확했어요. 버전 코드 올리고, Android AAB(앱 번들) 빌드하고, iOS 아카이브 빌드하고, 각각 Play 내부 트랙이랑 App Store Connect에 올리고, 스토어 스크린샷 여러 장 찍어두는 것까지. 배포 프로세스 전체를 새벽 중에 끝내달라는 거였어요.
새벽 00시 50분쯤, 맥미니가 보고를 올렸습니다. AAB·ipa 빌드 완료, Play 내부 트랙 업로드 완료, ASC 버전 생성 완료, 스크린샷 전부 완료. 남은 건 Android production 출시 클릭이랑 iOS 심사 제출 두 개라고. 딱 두 단계만 남았다는 정리였어요.
그런데 01시 00분, API가 끊겼습니다. 세션이 죽었어요. 맥미니 쪽 대화가 조용해졌어요.
잠시 뒤 세션이 재진입했습니다. 재진입한 세션이 핸드오프 메모를 읽었어요. 거기 적혀 있던 건 “P1 머지게이트 대기 중, 스크린샷 필요” — 이미 끝낸 상태가 아니라 남아있는 작업처럼 쓰여 있었어요. 재진입 세션 입장에선 당연한 판단이었습니다. 메모에 “완료”가 명시되어 있지 않으면 “할 것”으로 읽는 게 안전하니까요.
알림이 왔어요. “스크린샷 작업 시작합니다.” 그 순간 알았습니다. 다시 시작하고 있구나.
지켜보고 있자니 묘한 기분이었어요. 틀린 게 없거든요. 재진입한 세션은 메모를 읽었고, 메모에 적힌 대로 행동했어요. 메모가 시킨 일을 했을 뿐이에요. 그런데 30분 전에 이미 끝낸 일을 다시 하고 있는 거였습니다. 잘못은 세션이 아니라 메모에 있었어요. 완료 표시 없이 “해야 할 것”처럼 쓰인 메모가 재진입 세션에게는 새 지시서였던 거죠.
“했던 거 또 하고 스샷 또 찍는다. stale 좀 알려줘.”
한 줄 지적이었습니다. 정확했어요.
빌드는 끝났다, 그런데 또 한다
그날 사고를 정리하면 단순했습니다. 맥미니가 AAB·ipa 빌드·업로드·스크린샷을 마치고 완료 보고를 올렸다. API 정지로 세션이 끊겼다. 새 세션이 이전 핸드오프를 읽었다. 핸드오프에는 완료 표시가 없었다. 새 세션이 처음부터 다시 시작했다.
총 소요 시간이 두 배가 됐어요. 빌드를 두 번 돌렸고, 스크린샷을 두 번 찍었고, 업로드 요청도 두 번 갔어요. AAB 파일이 두 번 만들어졌다는 건 서명된 릴리스 바이너리가 두 개 생겼다는 거거든요. 업로드를 두 번 요청했을 때 마켓 내부 트랙이 중복 방지 로직을 걸어 두 번째가 에러로 튕겼어요. 덕분에 인지했습니다 — 아, 또 하고 있다는 것을.
만약 그 에러가 없었다면요? 두 번 돌아가고도 조용히 끝났을 거예요. 로그에는 “작업 완료”가 두 번 찍혔을 거고, 텔레그램 알림 두 번을 “음 빨랐네” 하고 넘겼을 수도 있어요. 중복을 발견한 건 운이었습니다. 마켓이 에러를 내줬으니까 알았지, 없었다면 30분 뒤에도 몰랐을 거예요.
빌드 15분 × 2 = 30분이 그냥 사라졌어요. AAB 빌드가 두 번 돌았다는 건 Gradle이 두 번 돌았고, 서명이 두 번 됐고, 파일이 두 개 생겼다는 뜻입니다. 그 시간 동안 다른 작업 하나를 더 할 수 있었던 거예요. 비용으로 따지면 빌드 30분치 토큰이 한 줄 메모 하나 때문에 날아갔습니다.
산문은 멱등성을 보장하지 못한다
근본 원인을 분석했더니 용어 하나로 정리됐습니다. 멱등성 부재예요. 같은 작업을 여러 번 실행해도 결과가 한 번 실행한 것과 같아야 한다는 성질 — 멱등성이 있으면 “한 번 했는지 두 번 했는지”가 중요하지 않거든요. 어떻게 됐든 결과는 같으니까요.
배포 작업의 세부 스텝들은 멱등하지 않았어요. 빌드를 두 번 하면 빌드 결과물이 두 개 생깁니다. 스크린샷을 두 번 찍으면 파일이 덮이거나 중복이 생겨요. 업로드를 두 번 요청하면 에러가 나거나 버전이 꼬입니다.
그런데 기존 방어 장치들이 이걸 잡지 못했어요. 커밋 기반 자동 체크, 완료 마킹, 외부영향 레저 — 전부 “task가 존재할 때 상태가 드리프트하는” 케이스를 잡는 장치였습니다. 이 방어막들은 “task가 완료됐는데 완료 마킹이 안 됐다” 또는 “배포가 됐는데 레저에 기록이 없다”를 잡아요. 레이어가 달라요. “세션이 재시작되면서 sub-step이 반복 실행되는” 케이스는 이 방어막들이 전제하지 않은 사각이었던 거죠. task가 이미 존재하느냐 마느냐와는 다른 레이어의 문제였습니다.
방어막이 있다고 전부 잡히는 게 아니에요. 방어막은 그 방어막이 설계된 케이스만 잡습니다. “내가 만든 방어막이 이걸 왜 못 잡았지?”라고 생각했는데, 당연히 못 잡죠. 이 케이스를 위해 만든 게 아니었으니까요. 방어가 있다는 안도감 자체가 위험할 수 있어요 — 방어막이 없는 구멍을 못 보게 만드니까요.
그리고 산문으로 쓴 핸드오프 메모는 멱등 판정을 할 수 없어요. “스크린샷을 찍었다”와 “스크린샷이 필요하다”는 글자 수가 비슷하지만 의미가 반대입니다. 새 세션은 이걸 글에서 추론해야 하는데, 글에는 언제나 해석의 여지가 있어요. 그 여지가 재실행으로 향합니다.
코드는 이렇지 않아요. step_done("screenshot") == True이면 스크린샷 스텝을 건너뜁니다. False이면 실행해요. 해석의 여지가 없거든요. 산문은 “이미 찍었다는 뜻인지, 아직 찍어야 한다는 뜻인지”를 문맥에서 추론해야 하는데, 문맥이 잘려나간 재진입 세션은 가장 안전해 보이는 쪽 — 다시 하는 것 — 을 선택합니다. 실수보다 중복이 낫다는 판단이거든요.
흔적을 시스템 밖에, 기계가 읽는 형태로
그날 루프를 끊은 건 “STALE 앵커”였습니다.
알림을 보고 나서 본진이 직접 ground-truth를 작성해 맥미니에게 쐈어요. “AAB·ipa 빌드 완료 — 재실행 금지. Play 내부 업로드 완료, ASC 버전 생성 완료, 스크린샷 완료 — 재실행 금지. 남은 건 Android production 출시 + iOS 심사 제출 두 개뿐.” 명시적으로 못박아서 보냈습니다.
맥미니가 즉시 멈췄어요. 빌드도 안 하고, 스크린샷도 안 찍고, 업로드 재시도도 안 했어요. 앵커에 적힌 두 개만 수행하고 끝냈습니다. 이게 수동으로 만든 멱등 판정이었어요. 글에도 “완료 — 재실행 금지”를 박으면 재진입 세션이 읽어도 넘어갈 수 있다는 걸 그날 밤 실증한 거죠.
해결책은 세 갈래로 설계했습니다.
첫 번째는 멱등성 스텝 레저예요. 비싼 스텝을 실행하기 전에 “이 스텝, 이미 했나?”를 기계에 물어보는 것이에요. 완료한 스텝을 레저에 기록하고, 다음에 같은 스텝을 실행하려 할 때 레저를 먼저 확인합니다. 기록이 있으면 넘어가요. 글이 아니라 기계가 읽을 수 있는 형태로 남기는 거거든요. 같은 노드가 재시작해도, 다른 노드가 이어받아도, 레저는 거기 있습니다.
두 번째는 STALE 앵커 컨벤션이에요. 멀티스텝 위임 지시를 보낼 때 “이미 DONE(재실행 금지)“과 “남은 것”을 명시적으로 박아놓는 겁니다. “빌드·업로드·스크린샷 완료 — 재실행 금지, 남은 것은 심사 제출 두 개”처럼요. 이러면 재진입 세션이 지시를 읽어도 뭘 하지 말아야 할지가 글에 적혀 있어요. 그날 밤 루프를 즉시 차단한 것도 이 수동 버전이었거든요.
세 번째는 훅이에요. 비싼 배포 명령 직전에 “방금 이 스텝 끝내지 않았냐?”를 감지해서 경고를 주는 장치입니다. 아직 관찰 모드라 차단까지는 안 하지만, 신호는 남겨요. 오탐이 많으면 실제 편집 작업 전체가 막힐 수 있어서 데이터를 먼저 보는 중이에요.
셋 다 하나의 원칙으로 귀결됩니다. 흔적을 산문이 아니라 레저에 남기는 것. 내가 한 일의 기록을, 다음 세션이 읽어도 명확하게 판정할 수 있는 형태로.
지금 생각해 보면 이게 원래 있어야 했던 인프라예요. 비싼 스텝을 실행하는 코드가 있으면, 그 코드 앞에 “이미 했냐?”를 체크하는 코드도 있어야 하거든요. 사람이 빌드하는 거라면 자기가 빌드했다는 걸 기억하지만, 세션이 중단되고 재시작되는 시스템에서 그 기억을 외부에 안 남겨두면 언제든 반복이 일어납니다. 이번이 처음이 아닐 수도 있고, 지금 못 알아챈 케이스가 더 있었을 수도 있어요.
가장 비싼 자동화 실수는 실패가 아니다
자동화를 돌리다 보면 실패를 두려워하게 됩니다. 빌드가 깨지거나, 업로드가 에러나거나, 스크린샷이 잘못 찍히거나. 그런 실패는 눈에 보여요. 에러 메시지가 뜨고, 보고가 올라오고, 대응할 수 있거든요.
이미 한 일을 모르고 다시 하는 건 달라요. 에러가 안 납니다. 로그에는 “작업 시작” → “작업 완료”가 두 번 찍혀요. 결과를 보면 정상처럼 보여요. 중복을 알아차리려면 타임스탬프를 비교하거나, 텔레그램 알림이 두 번 오는 걸 이상하게 여기거나, 두 번째 업로드가 에러로 튕겨서야 눈치 채게 됩니다. 그리고 대부분은 그 에러가 없어요. 조용히 두 배가 나가고, 아무도 모릅니다.
가장 비싼 자동화 실수는 실패가 아니에요. 이미 한 일을 모르고 다시 하는 것입니다. 두 배의 시간, 두 배의 토큰, 두 배의 비용이 조용히 나가거든요. 그리고 대부분은 아무도 모릅니다.
노드를 여러 대 굴리기 시작하면서 이 비용이 배율이 붙는 구조가 됐어요. 노드 하나가 실수하면 그 노드 한 대의 실수입니다. 그런데 자동화 루프 안에서 핸드오프 구조가 잘못돼 있으면, 그 구조로 일하는 모든 노드가 같은 실수를 할 가능성이 생겨요. 이번 사고는 운 좋게 한 대였지만, 구조 자체를 안 고쳤다면 다른 노드에서 다른 타이밍에 같은 일이 생길 수 있었습니다.
규칙은 코드입니다. 산문 핸드오프는 규칙이 아니에요 — 읽는 방식에 따라 결론이 달라지니까요. 레저는 코드입니다. 완료면 완료, 미완료면 미완료, 해석의 여지가 없어요.
배운 것
- 세션이 재시작되는 시스템에서 산문 핸드오프만 믿으면 언젠가 반드시 재실행이 발생합니다. 운 좋게 에러가 나면 알지만, 조용히 두 배 소모되는 경우가 더 많아요.
- “비싼 스텝을 실행하기 전에 이미 했는지 기계에 물어라”는 원칙은 단순했습니다. 하지만 이걸 구현하려면 “내가 한 일”의 흔적을 시스템 안에 기계가 읽을 수 있는 형태로 남겨야 합니다 — 산문이 아니라 레저로.
- 기존 방어 장치들이 잡지 못하는 사각이 있었어요. “task 완료 마킹”과 “외부영향 레저”는 다른 레이어의 문제를 잡는 장치였고, “세션 재진입 시 sub-step 재실행”은 별도 케이스였습니다. 방어가 있다고 전부 잡히는 게 아니에요.
- 멀티스텝 위임 지시에 “끝난 것”과 “남은 것”을 명시적으로 박아놓는 건 처음에는 번거로워 보입니다. 그런데 이게 없으면 재진입 세션이 헛돌아요. 한 줄 더 쓰는 게 낫습니다.
- STALE 앵커가 가장 빠른 임시방편이었고, 멱등성 스텝 레저가 영구적인 해결책이에요. 둘은 같은 원칙의 수동 버전과 자동 버전입니다. 어느 쪽이든 핵심은 하나 — 흔적을 재시작 이후에도 살아남는 형태로 남기는 것.
- 자동화를 길게 돌릴수록, 그리고 노드를 여러 대 쓸수록 이 문제는 더 자주 생깁니다. 세션 중단은 예외 케이스가 아니라 정상 운영의 일부예요. 거기에 맞는 설계가 처음부터 들어가야 하는 거거든요.
AI가 한 일을 또 하고 있다면, 그건 AI 잘못이 아니다. 흔적을 기계가 읽을 수 있는 형태로 남기지 않은 것이다. 기억이 끊기는 시스템에서 기억을 잇는 건 시스템 밖에 남긴 기록이거든요.
— 강대종 / @ssamssae