
FIFO를 버리면서 OS에 가까워졌다
요약
병렬 AI 에이전트 기반인 cekernel의 설계 과정에서 FIFO(named pipe)를 제거하며 시스템을 최적화한 기록입니다. 불필요한 의존성을 식별하고 OS의 프로세스 모델을 도입하여 더 견고한 구조로 개선하는 엔지니어링 의사결정 과정을 다룹니다.
핵심 포인트
- FIFO가 수행하던 숨겨진 3가지 역할(통지, 병렬 관리, 프로세스 식별) 분석
- 작동 중인 기능에서 불필요한 요소를 제거하는 설계 판단 근거 제시
- 상태 파일과 생존 확인을 통한 완료 감지 메커니즘의 단순화
- 의존성 전수 조사를 통한 시스템 복잡도 감소 및 OS 모델 도입
FIFO를 버리면서 OS에 가까워졌다
Claude Code 위에 cekernel이라는 병렬 AI 에이전트 기반을 만들고 있다. 그 핵심으로 소개했던 FIFO (named pipe)를 내 손으로 전부 지웠다. 이 기사는 "작동하고 있는 것에서 불필요한 것을 지운다"라는 설계 판단을, 어떤 근거로, 어떤 순서로 내렸는지에 대한 기록이다.
상정 독자:
- Claude Code나 AI 코딩 에이전트 위에 자신만의 메커니즘을 만들고 있거나, 만들고 싶은 사람
- "작동하고 있는 것에서 불필요한 것을 지운다"라는 판단을 어떻게 언어화하는지에 관심이 있는 사람
- 지금까지 계속 덧셈(addition) 방식으로 엔지니어링을 해온 사람
TL;DR:
- "쉘 스크립트와 FIFO만으로 동작한다"라고 소개했던 cekernel에서, 그 FIFO (named pipe)를 내 손으로 전부 지웠다
- 완료 통지용으로 만들었던 FIFO는 사실 3가지 역할을 짊어지고 있었다. 숨겨진 의존성은 지우려고 시도할 때 비로소 보인다
- 지워도 된다는 근거는 5가지 사실이 제공했다. "어떻게 고칠 것인가"를 선택하게 한 것은 OS의 프로세스 모델이었다
- 이행 후의 완료 감지는 2가지 정보원으로 정리했다. 상태 파일이 "무슨 일이 일어났는가"를, 생존 확인이 "아직 동작 중인가"를 나타낸다
"쉘 스크립트와 FIFO만으로 동작한다"라고 썼다
첫 번째 기사에서 cekernel의 특징으로 썼던 FIFO를, v2.1에서 전부 지웠다.
cekernel은 Claude Code 위에 구축한 병렬 AI 에이전트 기반이다. OS의 프로세스 모델을 도입하여, GitHub의 issue를 여러 Worker에게 병렬로 처리하게 한다. 설계 사상은 첫 번째 기사에 정리해 두었다.
그 기사에서 나는 이렇게 썼다. "쉘 스크립트와 FIFO만으로 동작한다". Worker가 완료를 FIFO에 쓰고, Orchestrator가 읽기 측에서 대기한다. 지연이 거의 없는 프로세스 간 통신 (IPC). 이런 코드도 실었다.
# Worker가 완료를 통지
echo '{"issue":4,"status":"merged","pr":42}' > /tmp/cekernel-ipc/session/worker-4
이 줄은 이제 없다. FIFO의 생성도, 쓰기도, 읽기도, v2.1에서 모두 삭제했다 (ADR-0020).
내가 특징으로 내세웠던 것을 내 손으로 지운 이야기다. 하지만 이것은 퇴보가 아니다. 지우기로 결정하기까지 무엇을 계산하고, 무엇을 재검토했는가. 이 글은 그 기록이다.
FIFO가 짊어지고 있던 3가지 역할
숨겨진 의존성은 지우려고 할 때 비로소 보인다.
FIFO는 "완료의 즉시 통지"를 목적으로 만들었다. 하지만 퇴역을 검토하며 의존성을 전수 조사했더니, 실제로는 3가지 역할을 짊어지고 있었다.
완료의 즉시 통지— 만들 때의 의도. Worker가 쓰고, Orchestrator가 읽음 -
병렬 수의 상한 관리— Orchestrator는 파이프의 개수를 세어 빈 자리를 판정하고 있었다. FIFO는 통지 경로인 동시에, 병렬 수를 관리하는 토큰 그 자체였다 -
Worker를 찾는 표식— orchctl ls 등의 관리 커맨드는 파이프를 열거하여 가동 중인 Worker를 발견하고 있었다. 파이프가 없다면 그 Worker는 목록에 나오지 않으며, 조작 대상이 될 수도 없다
2와 3은 설계 어디에도 적혀 있지 않았다. "완료 통지 메커니즘"이라는 라벨 아래에, 스케줄링과 프로세스 관리가 공존하고 있었다. 작동하는 동안에는 아무도 눈치채지 못한다. 지우려고 하는 순간, 의존성의 전체상이 비로소 떠오른다.
지우기로 결정한 5가지 사실
"왠지 불필요함"으로는 지울 수 없다. 사실을 계산했다.
| # | 사실 | 의미 |
|---|---|---|
| 1 | 이미 임의적(optional)이었다 | FIFO가 없어도 상태 파일로의 폴백(fallback)을 통해 완료를 검출할 수 있었다. 게다가 FIFO가 없는 완료 경로에는 issue의 락(lock)을 해제하지 못하는 결함이 잠재되어 있었다 |
| ... |
우선 1번. FIFO는 어느샌가 없어도 동작하는 것이 되어 있었다. 아무도 확인하지 않게 된 메커니즘의 그늘에서 2나 3 같은 실체가 자라난다. 생각해보면, 임의적인 것이 되었을 때부터 FIFO는 부패하기 시작했다.
핵심은 4번이다. 이 덩어리가 까다로운 이유는 issue의 락을 쥔 채로 멈춰버리는 것이다. 완료는 영원히 도달하지 않고, 락은 해제되지 않으며, 다음 Worker도 기동할 수 없다. 게다가 이것이 발생하는 것은 Orchestrator의 강제 종료라는 "이미 무언가가 망가진 후"에 한정된다.
그리고 다섯 번째. 첫 번째 기사에서 「지연(Latency)은 거의 제로」라고 자랑했던 속도는, 추산해 보니 오차에 불과했다. 그 속도가 정말 필요한가 하는 질문에 대한 답은 레이턴시 (Latency) 추산이 알려준다.
자식 프로세스는 자신의 종료를 보고하지 않는다
다섯 가지 사실은 「이대로는 안 된다」는 것을 가르쳐 주었다. 하지만 사실은 고치는 방법을 하나로 좁혀주지 않는다. 빠른 경로로 남겨둘 것인가, 타임아웃 (Timeout)으로 감쌀 것인가, 다른 통지 수단으로 대체할 것인가. 모두 선택할 수 있었다. 그중 하나를 선택하게 만든 것이 cekernel 설계의 축인 「OS의 프로세스 모델 (Process Model)을 본보기로 삼는다」는 방침이었다.
v1 버전의 cekernel에는 부모 역할을 하는 메커니즘이 없었다. claude -p로 자식 프로세스를 기동하는 것뿐인 모델에서는, 완료를 알 수 있는 수단을 스스로 만들어야만 했다. 그래서 파이프 (Pipe)를 직접 만든 것은 당시로서는 솔직한 구현이었다.
v2 버전에서 상황이 바뀌었다. claude --bg가 백그라운드 세션 (Background Session)을 관리하게 되었고, claude agents --json으로 세션의 생존을 확인할 수 있게 되었다. 토대가 부모의 역할을 수행하기 시작한 것이다.
진짜 OS를 떠올려 보길 바란다. 부모 프로세스는 자식 프로세스의 종료를 wait()나 SIGCHLD로 알게 된다. 커널 (Kernel)이 종료를 기록하고, 부모는 커널에 문의한다. 종료하는 자식 프로세스가 스스로 파이프에 써서 알리는 것이 아니다. 크래시 (Crash)된 프로세스는 아무것도 쓸 수 없기에, 커널 측의 기록이 진실의 근원이 된다.
FIFO 그 자체는 mkfifo로 만들 수 있는, OS의 유서 깊은 도구다. 문제는 도구가 아니라 부여한 역할에 있다. cekernel의 FIFO는 진짜 OS의 구도와 반대로 동작하고 있었다. 종료 직전의 자식이 스스로 보고하는 설계는, 자식이 정상일 때만 기능한다.
더 나아가 말하자면, 파이프의 개수를 세어서 병렬 수를 제한하는 OS 같은 것은 존재하지 않는다. 돌아가고 있는 프로세스의 수는 커널 자신이 파악하고 있는 것이지, 통신로의 개수로부터 도출하는 것이 아니다.
즉, OS의 프로세스 모델에 비추어 볼 때, 선택해야 할 해결책은 「남겨서 보강하는 것」이 아니라 「지우고, 마땅한 형태로 맞추는 것」이었다. 무엇이 일어났는지는 영속적인 기록 (exit status에 해당하는 상태 파일)으로. 아직 동작 중인지는 커널에 대한 문의 (claude agents --json)로. 파이프는 토대에 부모가 없던 시대의 대용품이었다.
본보기는 설계의 도구이지, 지켜야 할 교리가 아니다. 편리한 부분만 OS를 흉내 내는 것도, 본보기를 위해 메커니즘을 남겨두는 것도 아니다. 본보기와 구현이 어긋난다면, 어긋난 쪽을 고친다. 이번에 어긋난 쪽은 cekernel 측이었다.
이행 후의 메커니즘
완료 검출은 두 가지 정보원으로 정리했다.
이행의 상세한 내용은 깊게 다루지 않겠다. 가져갈 수 있는 원칙만을 적어둔다.
- 정보원은 두 개, 축은 별개 — 상태 파일이 「무엇이 일어났는가」를,
claude agents --json이 「아직 동작 중인가」를 담당한다. 하나의 정보원에 여러 의미를 부여하지 않는다. - 종료 기록은 한 번 쓰면 덮어쓰지 않는다 — 관리 명령도 GC (Garbage Collection)도, 기존의 종료 기록이 있다면 그것을 존중한다. 기록이 덮어쓰기로 사라지면, 나중에 무엇이 일어났는지 추적할 수 없게 된다.
- 생존을 확인할 수 없을 때는 빈 슬롯을 반환하지 않는다 — 더 오래 기다리는 것은 스루풋 (Throughput)의 저하로 끝나지만, 더 많이 기동하면 병렬 상한을 돌파해 버린다. 의심스러울 때는 안전한 쪽을 택한다.
- 삭제는 쓰기 측부터 먼저 — 읽기 측을 먼저 지우면, 아무도 읽지 않는 파이프에 모든 Worker가 쓰게 되어, 사실 4의 위험이 정상 계통 전체로 퍼진다. 퇴역 순서 그 자체가 설계 판단이었다.
참고로, 이 ADR (Architecture Decision Record)은 리뷰를 15번 거듭한 끝에, 마지막에 전체를 다시 써서 Accepted (승인) 되었다. 구현은 그 후 2일 만에 끝났다. 지난 기사에서 「속도는 설계를 대체하지 않는다」고 썼는데, 그 답을 확인하는 것과 같은 숫자다. 판단에 시간을 들였기에 구현은 망설임 없이 빠르다. 그 반대가 아니다.
맺음말
「있어도 없어도 돌아가는 메커니즘은 부패한다」
임의의 메커니즘은 지우거나, 필수적인 것으로 되돌리거나 둘 중 하나다. 방치라는 선택지는 없다.
cekernel은 과도기의 산물이다. 토대가 동일한 기능을 제공하기 시작하면, 직접 만든 부분을 버린다. 그것은 처음부터 정해두었던 설계 방침이며, 이번에는 그것을 실행했을 뿐이라고도 할 수 있다. 그럼에도 지우겠다는 판단은 남기겠다는 판단보다 더 많은 언어를 요구한다. 숨겨진 역할의 전수 조사, 위험의 구체화, 레이턴시 (Latency)의 추산, 그리고 설계의 축과의 정합성. 당신의 시스템에도 「있어도 없어도 돌아가는 메커니즘」이 하나쯤은 있지 않은가.
vibe coding (분위기에 맞춘 코딩)을 하고 있었던 것은 아니다. 그럼에도 책임은 자연스럽게 늘어나고 있었다. 그것을 덜어내기 위해 수행한 것은 당연한 작업들이다.
- 실제 코드를 읽고, 숨겨진 역할을 전수 조사(Inventory)한다
- 실행하고, 측정하고, 사실을 수치화한다
- 모범 사례(Best Practice)에 비추어, 수정 방법을 선택한다
FIFO를 버리면서 OS에 가까워졌다. 그뿐인 이야기다.
퇴역에 관한 모든 기록은 ADR-0020과 구현 PR에 남겨두었다. 흥미가 있다면 들여다봐 주길 바란다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기