인터랙티브 AI CLI를 백그라운드 자동화에서 안정적으로 돌리기 — asyncio 자식 프로세스 충돌 진단과 PTY+로컬 채널 해법
비동기 데몬에서 인터랙티브 AI CLI를 실행할 때 생기는 간헐 인증 실패의 근본 원인(이벤트 루프 자식 관리 충돌)을 실험으로 규명하고, PTY+로컬 채널 구조로 해결한 기록 (흐름도 포함)
인터랙티브 AI CLI(터미널에서 대화형으로 도는 에이전트)를 백그라운드 자동화 데몬에서 돌릴 때 생기는 간헐 인증 실패를, 원인까지 파고들어 PTY + 로컬 채널 구조로 해결한 기록입니다.
낯선 용어부터
| 용어 | 쉬운 설명 |
|---|---|
| 인터랙티브 CLI | 명령 한 줄로 끝나지 않고, 사람이 계속 입력하며 대화하는 터미널 프로그램 |
| 백그라운드 데몬 | 화면 없이 계속 떠서 일감을 받아 처리하는 상주 프로그램 |
| asyncio | 파이썬에서 여러 작업을 동시에 다루는 비동기 실행 방식 |
| 자식 프로세스 | 어떤 프로그램이 실행시킨 또 다른 프로그램 |
| PTY(유사 터미널) | "지금 진짜 터미널에 연결돼 있다"고 프로그램을 속이는 가짜 터미널 장치 |
| SIGCHLD | 자식 프로세스가 끝났을 때 부모에게 오는 운영체제 신호 |
1. 무엇이 문제였나
작업을 자동으로 돌리는 백그라운드 데몬이, 인터랙티브 AI CLI를 실행해 일을 시켰습니다. 그런데 이 CLI가 간헐적으로 인증 실패(401) 를 내며 작업이 죽었습니다.
- 겉으로는 "일정 시간 응답 없음"으로 보였지만, 실체는 인증 토큰이 거부됨(401)
- 토큰 자체는 멀쩡한데도 실패 → 재시도해도 특정 시간대엔 계속 실패
2. 근본 원인 — "같은 명령인데 실행 위치에 따라 결과가 다르다"
핵심 발견은 이것이었습니다. 똑같은 토큰·똑같은 명령인데 어디서 실행하느냐에 따라 성공/실패가 갈렸습니다. 실패율을 실행 방식별로 측정했습니다:
| 실행 방식 | 인증 실패율 |
|---|---|
순수 블로킹 실행(subprocess.run) | 0 / 10 |
| Popen + 스레드 (asyncio 안에서) | 약 8% |
| asyncio 기본 서브프로세스 방식 | 약 25% |
| 사람이 터미널에서 직접 실행 | 0% |
| 백그라운드 데몬을 통해 실행 | 간헐 실패 |
결론: 데몬의 asyncio 이벤트 루프가 자식 프로세스를 관리(SIGCHLD 수거)하는 동작이, 그 CLI가 내부에서 PTY로 또 다른 자식(실제 AI 프로세스)을 만드는 것과 충돌해서 간헐 인증 실패가 났습니다. 토큰·환경변수·모델·동시성·파일핸들 누수·좀비 프로세스는 하나씩 실험으로 전부 배제했습니다(단독 실행은 100% 성공).
교훈: 서드파티 실행기를 asyncio 데몬 안에서 그냥 띄우면, 그 실행기가 내부에서 하는 프로세스 관리와 이벤트 루프의 프로세스 관리가 보이지 않게 충돌할 수 있다. "단독으론 되는데 데몬에선 안 됨"은 이 신호다.
3. 결정적 검증 — 원인을 뒤집어 확인
"인터랙티브 모드 자체가 불안정한가?"를 갈라내기 위해, 우리가 직접 PTY로 인터랙티브 CLI를 띄워 반복 실행했더니 — asyncio 안에서 + 동시 실행 + 무거운 모델 조건에서도 8번 중 8번 전부 성공했습니다.
즉 문제는 인터랙티브 모드가 아니라, 서드파티 실행기가 자신을 띄우는 방식(asyncio와의 결합) 이었습니다. 우리가 실행을 직접 제어하면 충돌이 사라진다 — 이게 해법의 출발점입니다.
4. 해법 — PTY + 로컬 채널 러너
서드파티 실행기가 하던 일을, 우리가 제어하는 독립 프로세스로 다시 구현합니다. 핵심은 두 통로의 역할 분담입니다.
| 통로 | 역할 |
|---|---|
| PTY(유사 터미널) | 인터랙티브 CLI를 "진짜 터미널인 척" 띄우고, 시작 확인 프롬프트에 자동으로 Enter — 여기까지만 |
| 로컬 채널(유닉스 소켓) | 실제 프롬프트 주입 + 구조화된 결과 회수 + 중간 질문 자동 응답 |
왜 채널이 필요한가: 인터랙티브 CLI는 사람이 보는 화면(TUI)만 내놓고, 프로그램이 파싱할 구조화 출력(JSON 이벤트) 은 그냥은 안 줍니다. 그래서 CLI와 소켓으로 연결되는 로컬 채널을 두어, 그 채널을 통해 프롬프트를 넣고 구조화된 결과를 받아냅니다.
전체 구조
실행 순서 (사람 개입 0)
왜 안정적인가
채널 서버와 CLI를 데몬의 asyncio 이벤트 루프 밖, 독립 프로세스에서 구동하기 때문입니다. 앞서 충돌의 원인이던 "이벤트 루프의 자식 관리"가 이 프로세스엔 관여하지 않으므로, 간헐 인증 실패의 뿌리가 사라집니다. 데몬 입장에선 그냥 "자식 프로세스 하나 띄우고 그 출력을 읽는" 단순한 모양이 됩니다.
5. 근본 해결 전까지의 임시 완화책
당장 서드파티 실행기를 계속 써야 할 때 적용한, 효과가 검증된 완화책들입니다.
- 데몬을 로그인 터미널 세션에서 기동 (실행 족보에 따른 인증 실패 회피)
- 자식 실행을 블로킹 Popen 방식으로 (실패율 25% → 8%)
- "한 작업 실패가 전체를 잠그던" 전역 차단기 제거 (간헐 실패엔 오히려 독)
- 재시도 시 토큰을 새로 발급받아 사용
6. 정리
이 문제의 본질은 "서드파티 실행기를 비동기 데몬에 결합했을 때의 보이지 않는 자식 프로세스 관리 충돌" 이었고, 그 실행기의 소스를 고칠 수 없어 근본 해결은 우리가 실행을 직접 제어하는 것뿐이었습니다. PTY + 로컬 채널 러너는 서드파티 실행기의 장점(구조화 출력·무인 일회성 실행)은 유지하면서, 실행 제어권을 가져와 간헐 실패를 없애는 구조입니다.
핵심 교훈 세 가지:
- "단독으론 되는데 데몬에선 안 됨" 은 프로세스 관리 충돌을 의심하라.
- 원인 규명은 변수를 하나씩 배제하는 실험(실행 방식·환경·모델·동시성)으로.
- 통제 불가능한 서드파티가 문제면, 그 역할을 내가 제어하는 최소 구현으로 대체하는 게 가장 확실하다.
Comments
No comments yet. Be the first to comment!
★Reviews for this material
Write a reviewNo reviews for this material yet.