이 글에는 제휴 링크가 없고 OpenAI·앤트로픽과 아무 관계가 없습니다. 제가 직접 만든 중계기의 개발 기록을 바탕으로 썼습니다. 삽화는 GPT 이미지 생성, 도식은 직접 제작했습니다.
끄는 것도 기술이다 — 퇴근했는데 사무실 불이 켜져 있다 (LLM 중계기 개발기)
퇴근길에 건물을 올려다봤는데, 내가 쓰던 방 창문 하나에 아직 불이 켜져 있다고 해 볼게요. 나오면서 분명히 다 끄고 왔다고 생각했는데 말이죠. 그 방에서는 퇴근 소식을 듣지 못한 누군가가 혼자 일을 계속하고 있어요. 다음 날 아침 그 방을 쓰려던 사람은 문 앞의 ‘사용 중’ 팻말을 보고 돌아서야 하고요.
제가 만든 LLM 중계기에서 꼭 이런 일이 있었어요. 중계기를 껐는데, 중계기가 띄워 둔 AI 프로그램이 뒤에서 혼자 살아남아 자리를 차지하고 있었습니다. 그래서 다음에 중계기를 켜면 실패했어요.
이 글은 LLM 중계기 개발기의 21편입니다. 지난 20편 「맥에서 되는데 윈도에서 안 되는 이유」에서는 만드는 컴퓨터와 쓰는 컴퓨터가 다를 때 생기는 고장을 이야기했어요. 오늘은 켜는 것만큼 까다로웠던 끄는 일의 이야기입니다.

결론 요약
- 증상 — 중계기를 꺼도 중계기가 띄운 Codex 앱 서버가 뒤에 살아남아 문 번호(포트)를 붙들고 있었고, 다음 실행이 실패했어요. 당시 안내 창은 사용자에게 작업 관리자를 열라고 했습니다
- 정정 — 첫날 밤 “고쳤다”고 기록했지만, 다음 날 저녁 “그 보고는 틀렸다”고 다시 적었어요. 트레이 메뉴로 끌 때만 정리가 돌았고, 창을 X로 닫으면 정리 코드가 한 번도 불리지 않았거든요
- 규칙 — 끄는 길을 전부 찾아 정리 담당 한 곳으로 모으고, 정리가 ‘작동하는지’와 ‘불리는지’를 따로 검사해요. 나중에는 부모가 어떻게 끝나든 자식까지 함께 꺼지도록 윈도에 묶어 두는 안전망도 더했습니다
🏢 1. 껐는데, 꺼지지 않았다
6편의 셔틀버스를 기억하시나요? 중계기는 Codex를 쓰려고 앱 서버라는 프로그램을 하나 켜 두고 계속 씁니다. 이 앱 서버는 중계기와 이야기를 주고받으려고 내 컴퓨터 안에 문을 하나 열어 둬요.
여기서 문이란 포트(port)를 말해요. 한 컴퓨터 안에서 프로그램끼리 서로 찾아가는 문 번호예요. 중요한 규칙이 하나 있는데, 한 문 번호는 한 번에 한 프로그램만 쓸 수 있습니다. 이미 누가 쓰는 번호를 다른 프로그램이 열려고 하면 “이미 사용 중”이라는 오류가 나요.
개발 첫날 밤, 저장 기록(커밋)에 이런 증상이 남았어요.
- 중계기를 껐는데 Codex 앱 서버는 꺼지지 않고 살아 있었다
- 그 앱 서버는 부모가 사라진 뒤에도 자기 문을 계속 붙들고 있었다
- 그래서 다음에 중계기를 켜면 앱 서버를 새로 띄우려다 “이미 사용 중” 오류로 실패했다
그때 중계기가 띄우던 안내 창에는 “작업 관리자에서 중계기 프로세스를 종료한 뒤 다시 실행하세요”라는 뜻의 문장이 적혀 있었어요. 저장 기록은 이 안내를 두고 개발자가 아닌 사람에게 요구할 수 없는 일이라고 적었습니다.
👨👧 2. 부모 프로세스, 자식 프로세스, 그리고 고아
6편에서 실행 중인 프로그램 하나를 프로세스라고 부른다고 했죠. 프로그램이 다른 프로그램을 띄우면, 띄운 쪽을 부모 프로세스, 띄워진 쪽을 자식 프로세스라고 불러요.
윈도에서 중계기가 Codex 앱 서버를 띄우는 모양은 이랬어요.
- 중계기가 윈도의 명령 해석기를 띄우고
- 명령 해석기가 Codex 명령어 도구를 띄우고
- 그 도구가 실제로 일하는 앱 서버를 띄운다
할머니, 엄마, 아이처럼 줄줄이 이어지죠. 위에서 아래로 가지를 치는 모양이라 개발자들은 이걸 프로세스 나무(process tree)라고 불러요.
그런데 저장 기록에 적힌 핵심은 이 한 줄이었어요. 부모가 끝나도 자식은 저절로 끝나지 않는다. 부모가 먼저 떠나 혼자 남은 자식을 흔히 고아 프로세스라고 부릅니다. 개발용 컴퓨터에서 직접 확인했을 때, 남아 있던 앱 서버의 부모 칸에는 중계기 대신 1번이 적혀 있었어요. 원래 부모가 사라져서 운영체제의 맨 처음 프로세스가 대신 떠맡았다는 표시예요.
이 글의 제목에 쓴 좀비는 ‘꺼졌어야 하는데 뒤에서 계속 살아 있는 프로세스’를 편하게 부른 말이에요. 엄밀한 개발 용어로는 좀비와 고아가 조금 다른 것을 가리키지만, 저장 기록에서는 둘을 묶어 좀비라고 불렀고 이 글도 그렇게 부를게요.
퇴근하는 관리자가 방마다 들르지 않고 혼자 나가 버린 셈이에요. 퇴근 소식을 못 들은 직원은 불을 켠 채 계속 일하고, 다음 날 아침 그 방은 ‘사용 중’입니다.
🔧 3. 첫 수정 — 정리 담당을 세우다
첫날 밤 10시 4분, v0.1.15에서 이 문제를 고쳤어요(4편에서 예고했던 바로 그 판입니다). 저장 기록은 원인을 두 가지로 적었어요.
- 정리 코드가 ‘기다려 주지 않는’ 자리에 있었다 — 끄기 직전에 정리를 시작하게 해 두었는데, 앱은 정리가 끝나기를 기다리지 않고 그대로 꺼졌어요. 불을 끄라고 말만 해 두고 문을 잠그고 나간 셈이에요
- 부모가 죽어도 자식은 안 죽는다 — 2절에서 본 그대로예요
그래서 네 가지를 넣었습니다.
① 자식 명부 — 중계기가 띄우는 자식 프로세스를 모두 명부에 적어요. 자식이 스스로 끝나면 명부에서 지우고요 ② 나무째 끄기, 꺼질 때까지 기다리기 — 끌 때는 자식의 자식까지 한 번에 끄고, 정말 꺼질 때까지 기다렸다가 다음으로 넘어가요. 기록에 따르면 앱이 곧 사라지는 순간에는 “꺼 줘”라고 부탁만 해 두면 그 부탁이 실행되기 전에 앱이 먼저 사라질 수 있거든요 ③ 정리 담당 하나 — 중계기가 연 창구 두 개를 닫고, 연결 부품을 정리하고, 명부의 자식을 모두 끄는 일을 한 곳에 모았어요. 창구를 닫을 때는 열려 있는 연결부터 먼저 끊어요. 그렇지 않으면 문 닫기가 끝나지 않았거든요. 앱은 이 정리가 끝날 때까지 종료를 미루되, 3초가 지나면 강제로 꺼요 ④ 켤 때의 뒷정리 — 켤 때 문 번호가 이미 차 있으면, 그 자리를 차지한 프로세스가 누구인지 이름으로 확인해요. 우리 중계기가 남긴 좀비면 끄고 문을 되찾아요. 남의 프로그램이면 절대 끄지 않고, 비어 있는 다른 문 번호로 옮겨서 켜요. 작업 관리자를 열라는 안내는 이때 지웠습니다

그리고 4편의 ‘고장 하나에 검사 하나’ 습관대로 검사를 붙였어요. 이름은 종료 정리 검사(check-shutdown)예요.
- 진짜 자식 프로그램을 하나 띄운 다음 정리 기능으로 끄고, 정말 사라졌는지 확인해요
- 중계기 서버를 시험용 문 번호로 켰다가 정리하고, 문이 정말 닫혔는지 확인해요
검사가 진짜로 무는지도 봤어요. 일부러 끄기 기능을 아무것도 하지 않는 빈 껍데기로 바꿔 보니 검사가 “자식이 아직 살아 있음”으로 실패했습니다. 마지막으로 앱을 켜고 Codex를 연결한 뒤 ‘꺼라’ 신호를 보내 꺼 봤더니, 앱 서버가 사라졌고 문 번호 세 개가 모두 비었고, 곧바로 다시 켜도 아무 문제가 없었어요. 저장 기록의 제목은 ‘좀비 Codex 앱 서버를 끝낸다’였습니다.
🙇 4. “지난번 보고는 틀렸다” — 창 X는 다른 길이었다
다음 날 저녁 8시 17분, v0.1.29의 저장 기록은 이렇게 시작해요.
정리 코드는 이미 잘 갖춰져 있었다. 문제는 트리거였다.
트리거(trigger)는 방아쇠, 그러니까 정리 코드를 부르는 쪽이에요. 그리고 같은 기록에 이 문장이 이어집니다.
v0.1.15의 “좀비 고침” 보고는 트레이 경로만 고친 것으로, 틀렸다.
무슨 일이었을까요? 중계기를 끄는 길은 하나가 아니었어요.
- 화면 구석 트레이 아이콘의 메뉴에서 ‘종료’를 누르기
- 명령창 같은 곳에서 ‘꺼라’ 신호를 보내기
- 창 오른쪽 위의 X를 눌러 닫기
앞의 두 길은 전날 세운 정리 담당을 불렀어요. 그런데 X는 창을 숨기기만 했고, 창이 모두 닫혔을 때 무엇을 할지 정해 둔 처리도 아예 없었어요. 기록 그대로 옮기면 트레이 메뉴의 ‘종료’로 끝낸 사람만 정상 종료였고, X로 닫은 사람의 컴퓨터에서는 중계기가 뒤에 그대로 남아 있었습니다. 정리는 작동했지만, 불리지 않았던 거예요.
왜 전날 확인에서 놓쳤는지도 기록을 보면 보여요. 전날 끝까지 확인할 때는 ‘꺼라’ 신호로 껐고, 종료 정리 검사는 정리 기능을 직접 불러서 확인했어요. 정리가 제대로 작동하는지는 봤지만, 사람이 실제로 끄는 길 하나하나가 정리를 부르는지는 보지 않은 거예요.
이번에는 이렇게 고쳤어요.
- X로 닫기와 ‘창이 모두 닫힘’도 정리 담당을 부른다 — 둘 다 완전 종료가 됐어요. 트레이 아이콘을 눌러 창을 열고 숨기는 동작은 그대로 두었고요
- 앱 자신의 나무도 통째로 회수한다 — 이때의 중계기 앱은 화면을 그리려고 도우미 프로세스 몇 개를 함께 띄웠는데, 윈도에서는 앱을 끈 뒤에도 이 도우미들이 남는 경우가 있었어요. 그래서 마지막에 앱 자기 자신부터 시작하는 나무 전체를 한 번에 끄게 했습니다. 전날 넣은 ‘켤 때의 뒷정리’는 남이 남긴 좀비를 치우는 용도라 자기 나무는 잡지 않았거든요
- 새 검사, 종료 경로 검사(check-quit-path) — 창 닫기가 ‘숨기기’면 실패, ‘창이 모두 닫힘’ 처리가 없거나 종료를 부르지 않으면 실패, 자기 나무 회수·정리·시간 상한이 빠져도 실패해요

기록은 두 검사를 짝으로 설명해요. 종료 정리 검사는 정리가 ‘작동’하는가를, 종료 경로 검사는 정리가 ‘불리는’가를 본다고요.
그리고 한계도 솔직하게 적어 두었어요. 맥에서는 운영체제가 남은 도우미 프로세스를 알아서 치워 주기 때문에, 윈도에서 생기는 이 좀비를 맥에서는 아예 재현할 수 없었어요. 20편에서 본 ‘여기선 되는데 저기선 안 되는’ 문제의 끄기 버전이죠. 그래서 기록에는 “맥에선 창 닫기가 종료 함수를 실제로 호출하는가까지만 검증. 잔존 0개는 Windows 사용자 확인 필요.”라는 문장이 남아 있습니다.
🛡️ 5. 그 뒤에 더한 안전망
끄기 문제는 그 뒤에도 모양을 바꿔 세 번 더 찾아왔어요.
① 껍데기를 바꾸자 같은 구멍이 다시 보였다 (8월 6일) 중계기의 무거운 앱 껍데기를 걷어내는 작업(25편에서 다룹니다)을 하면서, 관리 화면은 평범한 브라우저 창이 됐어요. 그러자 창이 닫혀도 중계기가 그 사실을 알 수 없게 됐습니다. 기록에는 창만 닫고 자식 프로세스는 남기는 일이 “이 저장소가 반복해서 겪은 좀비 사고의 재발 경로”라고 적혀 있어요.
그래서 끄는 길을 새로 정했어요. 지금은 화면을 닫아도 중계기는 트레이에서 계속 일하고, 끌 때는 트레이 메뉴의 ‘종료’나 관리 화면 위쪽의 ⏻ 버튼을 써요. 두 길 모두 중계기의 같은 종료 창구를 불러요. 중계기는 “종료합니다”라는 응답부터 보낸 뒤에 정리를 시작해요. 정리부터 하느라 연결을 먼저 끊어 버리면, 누른 사람 눈에는 “눌렀는데 아무 일도 없었다”로 보이거든요. 정리가 어딘가에서 멈춰도 11초가 지나면 강제로 끝내고, 트레이 쪽도 11초를 기다렸는데 서버가 아직 살아 있으면 나무째 꺼요.
② 설치 프로그램이 ‘실행 중인 앱 닫는 중’에서 멈췄다 (8월 20일) 중계기를 켜 둔 채 새 판 설치 프로그램을 돌렸더니, 설치가 ‘실행 중인 앱 닫는 중’ 단계에서 멈췄어요. 기록에 따르면 윈도의 ‘실행 중인 앱 닫기’ 도우미가 창이 없는 중계기 서버 프로그램을 닫지 못했어요. 다시 시도하자 트레이만 꺼지고 서버는 고아로 남아 설치 파일을 계속 붙들었고, 설치는 몇 번을 해도 실패했습니다. 부모가 끝나도 자식은 남는다는 2절의 규칙이 그대로 다시 나타난 거예요.
여기서 두 가지를 더했어요.
- 설치 프로그램이 직접 치운다 — 설치 폴더 안에서 실행 중인 프로그램을 경로로 찾아 끄고 나서 파일을 복사해요. 이름으로 찾아 나무째 끄지 않는 데도 이유가 있어요. 자동 업데이트 때는 설치 프로그램이 트레이의 자식으로 실행되기 때문에, 트레이의 나무를 통째로 끄면 설치 프로그램 자신까지 꺼지거든요
- 부모가 떠나면 자식도 함께 끄도록 윈도에 묶어 둔다 — 윈도에는 여러 프로세스를 한 묶음(잡 오브젝트, Job Object)으로 묶고, “묶음의 손잡이가 닫히면 묶음 전체를 끈다”고 정해 두는 기능이 있어요. 트레이는 중계기 서버를 이 묶음에 넣고 손잡이를 쥐고 있어요. 트레이가 정상 종료든, 강제로 꺼지든, 갑자기 죽든, 트레이가 사라지면 운영체제가 손잡이를 닫으면서 서버와 그 자식 AI 프로그램들까지 함께 끕니다. 기록은 이걸 ‘마지막 안전망’이라고 불러요

③ 끄는 순간에도 경주가 있다 (9월 29일) 설치 프로그램을 만들 때 앱이 제대로 켜지는지 확인하는 검사가 있어요. 앱을 짧은 점검 모드로 켰다가 바로 끄는 검사인데, 여기서 앱이 6번 중 6번 끄는 순간에 비정상 종료했어요. 응답마다 버전 정보를 덧붙이는 한 줄이 들어간 뒤부터 나타났고, 그 줄을 빼면 사라졌어요. 그렇다고 그 줄이 잘못된 건 아니었어요.
원인은 끄는 순간의 경주였어요. 점검 모드의 앱은 서버가 켜졌는지 “살아 있니?” 하고 물어본 직후 바로 꺼져요. 그런데 이때 쓴 도구가 받은 답을 읽는 부품을 더 빠르게 만드는 작업을 뒤에서 이어 가고 있었어요. 답에 실린 정보가 늘어나자 그 작업이 하필 끄는 순간과 겹쳤고, 작업을 마친 쪽이 “다 했어요”라고 알리러 오다가 이미 닫히고 있는 문에 부딪혀 넘어진 셈이에요. 기록에는 코드 결함이 아니라 중계기 서버를 돌리는 실행 환경(Node.js)의 윈도판에서 생기는 종료 경쟁이라고 적혀 있어요.
해결은 뒤에 일을 남기지 않는 더 단순한 방법으로 묻는 것이었어요. 같은 재현 시험에서 6번 중 0번이 됐고, 켜서 끄기까지 걸린 시간도 1.6초에서 0.85초로 줄었습니다.
📋 6. 남긴 규칙 — 끄기도 켜기처럼 설계한다
켜는 쪽에는 워밍업, 연결 확인, 준비 상태 표시까지 단계가 많았어요. 이 사고들은 끄는 쪽에도 그만큼의 설계가 필요하다는 걸 보여 줬습니다. 그래서 남은 규칙은 이래요.
- 끄는 길을 전부 적는다 — 트레이 메뉴, 신호, 창 닫기, 버튼, 설치 프로그램, 그리고 갑자기 죽는 경우까지
- 모든 길을 정리 담당 한 곳으로 모은다 — 길마다 정리를 따로 짜면 하나는 빠집니다. 실제로 빠졌고요
- 정리 순서와 시간 상한을 정한다 — 응답 먼저, 창구 닫기, 자식은 나무째, 끝날 때까지 기다리되 무한정 기다리지 않기
- ‘작동하는가’와 ‘불리는가’를 따로 검사한다 — 두 검사는 지금도 남아 있어요. 하나는 저장 전에 돌리기를 권하는 기본 검사 묶음에, 다른 하나는 배포 전에 꼭 돌리는 전체 묶음에 들어 있습니다. 껍데기가 바뀐 지금 종료 경로 검사는 트레이의 새 종료 길을 봐요
- 틀린 “고쳤다”는 고쳐 적는다 — 첫날 밤의 기록은 지우지 않았어요. 다음 날의 기록이 그 옆에 “틀렸다”고 적어 두었을 뿐이에요
정리
① 부모 프로세스가 끝나도 자식은 저절로 끝나지 않아요. 중계기를 꺼도 Codex 앱 서버가 고아로 남아 문 번호를 붙들었고, 다음 실행이 실패했어요 ② 첫날 밤 정리 담당을 세우고 “고쳤다”고 적었지만, 창 X는 정리를 부르지 않는 다른 길이었어요. 다음 날 “틀렸다”고 고쳐 적고 모든 길을 한 곳으로 모았고, 정리가 ‘작동하는가’와 ‘불리는가’를 따로 검사해요 ③ 그 뒤로 종료 버튼, 설치 프로그램의 직접 정리, 부모가 사라지면 자식까지 함께 끄는 윈도의 묶음 기능, 끄는 순간의 경주 해결까지 안전망을 더했어요
다음 22편은 「‘준비 중’이 끝나지 않는 화면」이에요. 실제로는 연결이 끝났는데 화면은 ‘워밍업 중’에서 영영 넘어가지 않던 이야기를 할게요.
2026년 10월 1일 기준입니다. 제가 만든 중계기의 개발 기록(저장 기록의 시각과 메모)을 바탕으로 썼고, 특정 기업·서비스의 공식 입장이 아닙니다.