에이전트를 껐는데도 터미널에 뭔가 계속 돌고 있을 때

프리랜서로 넘어온 지 얼마 안 됐을 때라, 큰 이미지 변환 스크립트를 밤새 돌려두고 저녁을 먹으러 나갔습니다.
돌아와서 노트북을 닫으려는데 팬 소리가 여전히 요란했습니다. 작업은 이미 끝났다고 떴는데도 그랬습니다.

작업이 끝났다는데 노트북 팬이 계속 돌 때, 좀비 태스크의 정체

bash
1$ ps aux | grep node
2user 4821 98.2 node convert-images.js
3user 4822 0.0 node convert-images.js <defunct>
검색해보니 뒤쪽 줄의 <defunct> 표시가 핵심이었습니다. 작업 자체는 끝났지만, 그 결과를 최종적으로 정리해줄 부모 프로세스가 아직 그 사실을 확인하지 않은 상태로 남아 있었습니다. 일은 끝났는데 뒷정리만 안 된 채로 자리를 차지하고 있는 상태였습니다.
왜 하필 "좀비"라는 이름이 붙었는지도 그제야 이해가 됐습니다. 죽었는데 완전히 사라지지는 않고 자리만 차지하고 있는 모습이, 어디서 온 표현인지 몰랐을 땐 그냥 섬뜩한 은어처럼만 들렸습니다.
작업은 끝났지만 부모 프로세스가 결과를 회수하지 않아 자리만 남아 있는 상태
작업은 끝났지만 부모 프로세스가 결과를 회수하지 않아 자리만 남아 있는 상태
재우고 나서도 며칠 뒤 다시 ps aux를 돌려봤더니, 이번엔 좀 다른 게 보였습니다.

터미널 창을 닫았는데도 프로세스가 살아있을 때, 고아 프로세스가 남는 이유

bash
1$ ps aux | grep node
2user 5310 1 node long-crawler.js # PPID가 1
PPID(부모 프로세스 번호)가 1이라는 게 이상해서 로그를 다시 살펴봤습니다. 1번은 원래 시스템이 제일 처음 띄우는 프로세스 번호였습니다. 며칠 전 터미널 창을 그냥 닫아버렸던 크롤러 작업이, 원래 부모였던 터미널이 사라지자 시스템에 그대로 입양되어 계속 돌고 있었습니다.
부모가 먼저 사라져도 자식 프로세스는 알아서 끊기지 않고, 시스템이 대신 떠맡아 계속 실행됩니다. 부모를 잃었다고 같이 죽는 게 아니라, 조용히 다른 보호자 밑에서 계속 일하고 있었던 셈입니다.
문제는 이게 눈에 안 띈다는 점이었습니다. 터미널을 닫았으니 당연히 같이 끝났으려니 했는데, 크롤러는 그 뒤로 사흘을 더 돌면서 디스크를 채우고 있었습니다.
터미널이 사라진 뒤에도 자식 프로세스만 시간이 지나며 계속 실행되는 흐름
터미널이 사라진 뒤에도 자식 프로세스만 시간이 지나며 계속 실행되는 흐름
ps aux 명령 결과 창을 들여다보며 PPID 숫자를 손가락으로 짚어보는 장면
혼자 일하다 보니 이런 걸 물어볼 사람이 마땅치 않았는데, 마침 예전 회사 동료가 요즘 뭘 하고 지내냐며 연락해왔습니다.

터미널에 프로세스가 우르르 뜰 때, 에이전트 스웜

제 화면을 캡처해서 보여줬더니, 동료가 웃으면서 말했습니다. "나도 요즘 그거 자주 봐. 큰 리팩토링 할 때 일부러 여러 개 띄워두는 거야."
동료 설명으로는, 파일이 수백 개로 나뉜 작업을 하나의 에이전트가 순서대로 처리하는 대신, 여러 개를 동시에 띄워서 구역을 나눠 맡기는 방식이 있다고 했습니다. 벌떼처럼 여러 에이전트가 동시에 각자 몫을 처리하고 끝나면 흩어지는 구조라, 프로세스가 한꺼번에 여러 개 보이는 게 이상한 일이 아니라고 했습니다.
혼자 일할 땐 프로세스가 여러 개 뜨면 일단 뭔가 잘못된 줄 알았는데, 일부러 그렇게 띄우는 방식이 따로 있다는 걸 그날 처음 알았습니다.
하나의 작업을 여러 에이전트가 동시에 구역별로 나눠 처리하는 구조
하나의 작업을 여러 에이전트가 동시에 구역별로 나눠 처리하는 구조
혼자 일하면서 처음 마주치는 개념이 많다 보니, 궁금한 게 생기면 공식 문서부터 뒤지는 버릇이 붙었습니다. 크롤러가 다운로드해온 파일 하나를 열어보다가 또 막혔습니다.

확장자도 없는 파일이 실행될 때, 바이너리가 하는 일

크롤러가 이미지 변환용으로 받아온 파일 하나를 텍스트 편집기로 열었더니 알아볼 수 없는 글자만 잔뜩 나왔습니다. 문서를 찾아보니 이유가 명확했습니다.
사람이 읽는 글자로 저장된 파일이 아니라, 컴퓨터가 바로 실행하거나 처리할 수 있는 0과 1로만 이루어진 파일이었습니다. 메모장으로 요리 레시피는 읽을 수 있어도, 압축된 기계어 그 자체는 사람 눈으로 못 읽는 것과 같습니다. 실행 파일, 이미지 처리 도구, 압축된 라이브러리 대부분이 이런 형태로 다운로드되고 있었습니다.
사람이 읽을 수 있는 정도를 기준으로 보면 바이너리는 반대쪽 끝에 있는 형태
사람이 읽을 수 있는 정도를 기준으로 보면 바이너리는 반대쪽 끝에 있는 형태
파일 정체는 알아냈는데, 막상 이 도구를 실행하려니 또 다른 에러가 났습니다.

도구 하나 실행했더니 다른 게 줄줄이 걸릴 때, 의존성이 딸려오는 구조

text
1Error: sharp module version mismatch
2Expected: 0.33.x, Found: 0.30.x
같은 도구를 쓰는 다른 사람의 지난 커밋을 찾아보다가, package.json에 적힌 버전이 제 컴퓨터에 깔린 버전과 다르다는 걸 발견했습니다.
이 도구 하나가 제대로 돌아가려면, 그 안에서 또 다른 여러 도구가 정해진 버전으로 같이 깔려 있어야 했습니다. 요리 하나를 하려면 레시피뿐 아니라 정해진 브랜드의 조미료까지 맞춰야 하는 것과 비슷합니다. 버전을 맞춰 다시 설치하고 나서야 변환 작업이 제대로 돌아갔습니다.
내 스크립트가 쓰는 도구가 그 안에서 또 다른 버전을 요구하는 겹겹의 구조
내 스크립트가 쓰는 도구가 그 안에서 또 다른 버전을 요구하는 겹겹의 구조
여러 개의 작은 상자가 하나의 큰 상자 안에 정확히 맞물려 들어가는 모습을 손으로 확인하는 장면
남아 있던 프로세스를 전부 정리하고 나서야 노트북 팬이 조용해졌습니다. 그날 만난 말들은 제각각이었지만 묻고 있는 건 하나였습니다. 내가 끝냈다고 생각한 작업이 정말 끝났는가입니다.

자주 묻는 질문

좀비 태스크나 고아 프로세스를 그냥 두면 위험한가요?

하나둘 정도는 크게 문제되지 않지만, 쌓이면 메모리와 CPU를 계속 잡아먹습니다. <code>kill</code> 명령으로 정리하거나, 작업이 끝나면 터미널을 바로 닫지 않고 정상 종료 메시지를 확인하는 습관을 들이는 게 안전합니다.

에이전트 스웜은 항상 여러 개를 띄우는 게 유리한가요?

파일 수가 많고 서로 독립적인 작업일 때는 유리하지만, 작업끼리 결과를 주고받아야 하는 경우엔 오히려 조율 비용이 늘어납니다. 무조건 여러 개보다는 작업 성격에 맞춰 판단하는 게 낫습니다.

의존성 버전이 안 맞으면 항상 에러가 나나요?

항상은 아닙니다. 작은 버전 차이는 무시되고 넘어가는 경우도 있어서, 겉으로는 멀쩡히 돌아가다가 특정 기능에서만 조용히 다르게 동작하는 경우가 더 골치 아픕니다.

<a href="/glossary/code-generation" class="glossary-link" title="AI가 자연어 설명이나 기존 코드 문맥을 바탕으로 프로그래밍 코드를 자동으로 작성하는 기술입니다.">AI 코딩</a> 도구 용어 사전 연작의 3편입니다. 다음 편 예고 · 4편 사수 없이 첫 배포를 앞둔 인턴이 마주치는 낯선 말들 킬 스위치 · 백오프 · 레포지토리 · 컴파일(빌드) · 런타임

관련 글