← 뉴스레터

2026-06-26

일은 안 하고 바쁘다 했다

2026년 6월 15일 아침 열시쯤, 텔레그램에 메시지가 왔다. “본진 idle 27%인데 왜 안 돌아요?” 스크린샷이 같이 왔다. 모니터 화면엔 idle 27%라고 찍혀있고, 워커 로그엔 ‘BUSY — 양보’라고 찍혀 있었다. 두 기록이 같은 시각, 같은 세션을 바라보고 있었다.


루프가 돌지 않은 밤

나는 작업 노드를 여러 대 운영하고 있다. AI 세션이 떠있는 기계들인데, 각 세션이 비어있을 때 할 일 목록에서 일감을 꺼내 자동으로 던져주는 루프를 직접 만들어 쓴다. idle 감지 → 작업 배분이다.

이 루프가 제대로 돌면 내가 자는 동안에도 할 일 목록이 조금씩 줄어 있어야 한다. 워커를 켜고 잠들면 아침에 일이 줄어있는 게 정상이다. 지금까지는 그래왔다.

그날 밤은 달랐다. 새벽 3시 30분부터 아침 10시 35분까지, 기록에는 한 건도 처리되지 않았다. 매 30분 틱마다 같은 한 줄이 찍혀 있었다. SKIP — REPL busy, 양보. 세션이 뭔가를 처리 중이니 이번 틱은 넘어간다는 뜻이다.

열네 번이다.

정확히 열네 번, 같은 이유로, 같은 결과가 나왔다. 그런데 실제로 그 세션 화면을 들여다봤더니 커서만 깜빡이고 있었다. 아무것도 처리 중이지 않았다. 시스템은 바쁘다고 했고, 세션은 비어 있었다.

busy 판정이 어떻게 작동하는가

워커가 일을 던지기 전에 확인하는 단계가 있다. 지금 그 세션이 다른 걸 처리 중인지 먼저 판단하는 거다. 이미 뭔가를 하고 있는 세션에 또 일을 던지면 두 작업이 엉키니까.

판단 방법은 단순하다. 세션 화면의 마지막 몇 줄을 긁어와서 특정 단어가 있으면 바쁜 것으로 판정한다.

AI 세션이 뭔가를 실행하는 동안은 화면에 고유한 신호가 뜬다. 중단 가능 안내가 뜨고, 처리하는 동안 내용이 실시간으로 흘러 내린다. 이 신호들을 잡아채는 게 busy 판정의 역할이다.

내가 그 패턴에 집어넣은 단어 중 하나가 “tokens”였다. 실행 중인 세션은 처리하는 동안 얼마나 많은 내용을 소화하고 있는지를 수치로 표시한다 — 그게 화면에 흐른다. 그러니 “tokens”가 보이면 처리 중이라는 신호로 읽는다는 논리였다. 합리적으로 보이는 선택이었다.

그 생각이 틀렸다.

tokens가 두 군데 있었다

AI 세션은 대화가 길어지면 — 컨텍스트가 많이 쌓이면 — 화면 하단에 안내 문구를 하나 띄운다.

요약하면 “지금까지 나눈 대화가 많이 쌓였으니 /clear로 정리하면 tokens를 아낄 수 있어요”라는 내용이다. 이 문구는 세션이 완전히 비어서 입력만 기다리는 상태에서도, 컨텍스트가 쌓여있으면 그냥 거기 떠 있다. 입력 커서 바로 위에, 조용히.

비유하자면 이렇다. 요리사가 바쁜지 확인하기 위해 “바쁠 때는 앞치마를 두르고 있다”는 기준을 세웠다고 하자. 그런데 그 요리사는 쉬는 중에도 앞치마를 두르고 있었다. 앞치마가 “바쁨”의 신호가 아니었던 거다. 기준은 그럴듯했지만, 그 기준이 측정하는 게 처음부터 “바쁨”이 아니었다.

“tokens”도 그랬다. 두 곳에 쓰이고 있었다.

실제로 처리 중인 세션이 흘려내는 실시간 수치 — “지금 이만큼 처리 중” — 와, 멀쩡히 비어있는 세션 하단에 고정으로 떠있는 안내 문구 — “나중에 정리하면 절약할 수 있어.” 완전히 다른 맥락에 같은 단어가 들어있었다. 내 판정 로직은 그 구분을 안 했다. 같은 단어면 같은 뜻이라고 봤다.

본진 세션은 오래 켜져 있어서 컨텍스트가 쌓여있었다. 그래서 안내 문구가 항상 화면 하단에 떠 있었다. 워커가 화면을 훑을 때마다 “tokens”를 발견했다. 발견할 때마다 바쁘다고 판정했다. 매 틱마다 양보하고 넘어갔다. 밤새 한 건도 처리하지 않았다.

처음엔 엉뚱한 데서 찾았다

원인을 파악하기 전까지는 제법 돌아갔다.

처음에 의심한 건 세션 연결 상태였다. 화면을 긁어오는 타이밍이 잘못된 건가, 소켓 경로가 바뀐 건가, 세션 자체가 깨진 건가. 로그에 “REPL busy”가 찍혀있으니까 세션이 실제로 살아있는지부터 확인했다. 살아있었다. 세션 문제가 아니었다.

다음엔 idle 감지 로직 전체를 훑었다. 게이트가 여러 단계 있는데, 각 단계를 하나씩 통과시켜 봤다. busy 판정 이전 단계는 다 통과했다. 좁혀졌다 — 문제는 busy 판정 자체에 있었다.

busy 판정 로직을 들여다봤다. 패턴에 어떤 단어들이 들어있는지를 다시 읽었다. “tokens”를 보는 순간, 안내 문구가 떠올랐다. 그 전에도 안내 문구를 봐왔는데, 그게 busy 감지 범위 안에 들어온다는 걸 그 순간 연결한 거다.

“아, 그게 거기도 들어있구나.”

그 다음은 순식간이었다. 세션 화면을 다시 열어보니 하단에 안내 문구가 정확히 있었다. 범인이 거기 있었다.

발견은 짧고, 탐색이 길다. 막상 찾고 나면 “이게 뭐야”가 나오지, “어렵겠다”가 안 나온다. 항상 그렇다.

한 줄 고치면 됐다

수정 자체는 단순했다. busy 판정을 하기 직전에, 화면에서 그 안내 문구 줄만 먼저 걸러내면 됐다. 안내 문구엔 특유의 표현이 들어있으니, 그 표현이 있는 줄을 제외하고 나머지만 검사하면 된다.

단어 하나가 패턴에 걸렸으니, 그 단어가 엉뚱한 맥락에서 나오는 줄을 먼저 빼낸 뒤 패턴을 보면 된다. 음향 엔지니어 비유로 하면: “엔진 소리를 분석하기 전에, 배경 음악 트랙을 먼저 꺼라.” 배경 음악과 엔진 소리가 같은 주파수를 공유하면, 음악을 끄기 전엔 엔진이 돌고 있는지 판단하기 어렵다.

줄 하나가 그 역할을 했다.

수정하고 나서 양방향으로 테스트를 돌렸다. 실제로 실행 중인 세션에서 busy 판정이 제대로 뜨는지, 비어있는 세션에서 false-busy가 더 이상 안 뜨는지. 둘 다 통과했다.

같은 버그가 세 곳에 있었다 — 본진 워커, 맥미니 워커, 또 다른 풀 워커 스크립트. 세 파일이 같은 패턴을 그대로 복사해 썼으니 같은 문제가 있었다. 코드 복사는 기능만 가져오는 게 아니라 버그도 같이 가져온다. 커밋 두 개로 세 파일을 고쳤다.

참고로 다른 계열의 워커는 실행 신호를 다른 단어로 확인하는 구조여서, 이번 버그에서 자유로웠다 — 건드리지 않았다.

모델이 거짓말한 게 아니다

처음 로그를 봤을 때 솔직히 든 생각은 “AI 세션이 뭔가 이상하게 걸린 건가”였다. 세션이 내부적으로 뭔가를 계속 처리하고 있는 건지, 아니면 제대로 응답을 못 하는 상태인 건지.

AI를 먼저 의심하는 건 반사적인 거다 — 보이지 않는 내부가 있으니까.

그런데 실제 원인은 AI 내부와 전혀 관계없는 곳에 있었다. 모델이 이상하게 동작한 게 아니었다. AI는 그냥 아무것도 안 하고 멀쩡히 기다리고 있었다. idle 27%가 맞는 말이었다.

오판한 건 내 판정 로직이었다. 내가 만든 busy 감지 코드가 단어 하나를 잘못 짚었다. 아이러니하게도, AI는 아무 잘못이 없었다. 잘못은 AI를 감시하는 스크립트에 있었다. 감시하는 코드가 틀린 것을 감시당하는 모델이 고쳐줄 수는 없다.

자동화를 구축하다 보면 이 구분이 점점 중요해진다. 무언가가 이상하게 돌아갈 때, “AI가 이상한가”와 “내 로직이 이상한가”를 먼저 분리해야 한다. idle 감지, busy 감지, 상태 판단처럼 AI 바깥에서 돌아가는 코드는 모델과 완전히 별개다. 그 코드가 틀리면 모델이 아무리 멀쩡해도 시스템 전체가 틀린 방향으로 돈다.

이번엔 내 쪽이었다. 대부분은 내 쪽이다.

단어로 상태를 감지하는 것의 한계

수정 이후에 예방 원칙을 하나 세웠다. busy 판정에 새 단어를 추가하기 전에 “이 단어가 idle 화면에도 나올 수 있는가”를 한 번 확인하는 것. 복잡한 로직이 필요한 게 아니다. 그 체크 하나면 이번 같은 실수는 막을 수 있다.

단어로 상태를 감지하는 방식은 태생적으로 약점이 있다. 같은 단어가 두 가지 맥락에서 쓰이는 순간 판정이 흔들린다. 구조적으로 막으려면 단어 자체보다 그 주변 맥락까지 같이 봐야 한다. “tokens”라는 단어만 보면 구분이 안 된다. 그 단어가 실행 중 흐르는 수치 뒤에 붙어있는지, 아니면 “절약하세요”라는 안내 문구 안에 들어있는지를 같이 봐야 한다. 단어만 잡아채면 맥락을 잃는다.

왜 단순한 한 줄이 이렇게 큰 사고를 내는가.

자동화는 실수가 빠지는 구멍을 크게 만든다. 사람이 30분마다 직접 확인했다면, 한두 번 바쁘다 판단하고 나서 화면을 실제로 들여다봤을 거다. 비어있으면 “아, 오판했구나” 하고 그 자리에서 다시 생각했을 거다. 자동화는 그 중간 점검이 없다. 틀린 결론을 열네 번 연속으로, 7시간 동안, 같은 이유로 반복한다.

수정 한 줄의 반대급부도 같이 크다. 고치면 그다음 틱부터 즉시 정상으로 돌아온다. 버그가 자동화를 타면 피해가 배율이 붙고, 수정도 배율이 붙는다. 양쪽 다 한 줄이 하는 일이 크다는 말이다.

시스템을 오래 운영하다 보면 이런 맥락이 하나씩 쌓인다. 처음엔 보이지 않던 엣지 케이스가 실제로 발생하고 나서야 보인다. 버그는 항상 운영 이후에 발견된다. 그래서 설계 단계에서 모든 걸 잡으려고 하는 게 아니라, 사고가 나면 그 자리에 예방 로직 하나씩 박는 거다. 이번처럼.

배운 것

가장 단순한 게 제일 오래 숨어 있을 수 있다는 것. 워커 로직 전체를 다시 읽는 동안, 진짜 범인은 단어 하나였다. 복잡한 데서 찾다가 결국 제일 기본적인 곳에서 나왔다.

자동화가 틀릴 때 AI를 먼저 의심하지 말 것. 모델 바깥에서 돌아가는 감지 코드가 틀려도 모델이 일을 못 받는 건 매한가지다. 잘못은 감시 스크립트에 있었고, AI는 멀쩡히 기다리고 있었다.

새 판정 단어를 추가하기 전에 “이 단어가 idle 상태에서도 뜨는가”를 한 번 확인하는 것. 단어 하나의 의미는 맥락에 따라 완전히 다르다. 같은 단어가 두 맥락에서 쓰이는 순간, 그 단어만으로 상태를 판단하면 안 된다.

바쁜 척하는 게 꼭 의도가 있어야 하는 건 아니라는 것. 시스템이 잘못 읽은 거였다. 잘못 읽게 만든 건 내가 짠 코드였다. 탓할 곳이 없을 때 내 코드를 다시 보면 된다.

막히는 지점은 언제나 단순하다. 한 줄 고치면 됐다.

— 강대종 / @ssamssae