50년 묵은 방식, 슬슬 한계가 보여요
리눅스나 유닉스에서 새 프로그램을 실행할 때 내부적으로 거의 항상 거치는 길이 있어요. 바로 fork()로 지금 프로세스를 복제한 다음, 그 복제본 위에서 exec()를 호출해서 원하는 프로그램으로 갈아끼우는 방식이거든요. 우리가 셸에서 명령어 하나 칠 때마다, 웹 서버가 자식 프로세스를 띄울 때마다 이 과정이 돌아가고 있어요.
이게 뭐냐면, fork()는 "나 자신을 똑같이 하나 더 복사해줘"라는 시스템 콜이에요. 그러면 부모와 똑같은 자식 프로세스가 생기죠. 그리고 그 자식이 exec()를 불러서 "이제 나는 ls라는 프로그램이 될래" 하고 자기 몸통을 통째로 바꿔버리는 거예요. 단순하고 우아해서 수십 년간 잘 써왔어요.
그런데 왜 문제가 될까요?
첫 번째 문제는 덩치 큰 프로세스에서 fork가 비싸다는 점이에요. 옛날에는 fork가 메모리를 진짜로 다 복사했지만, 요즘은 COW(Copy-On-Write)라는 기법을 써요. 이게 뭐냐면, 일단 복사하는 척만 하고 같은 메모리를 공유하다가 누군가 쓰기를 시도하는 순간에만 진짜 복사하는 똑똑한 방식이에요. 그런데 COW를 쓰더라도 페이지 테이블(메모리 주소를 관리하는 표)은 통째로 복사해야 해요. 그래서 메모리를 수십 GB씩 쓰는 거대한 프로세스가 fork를 하면, 정작 자식은 곧바로 exec로 다 갈아엎을 건데도 그 무거운 표를 복사하느라 시간을 까먹어요.
두 번째는 멀티스레드 환경에서의 지뢰밭이에요. fork는 호출한 스레드 하나만 복제하고 나머지 스레드는 사라지게 해요. 문제는 사라진 스레드가 잠가둔 락(lock)은 그대로 잠긴 채로 남는다는 거죠. 만약 자식이 fork와 exec 사이에서 그 락이 필요한 함수를 호출하면 영원히 풀리지 않는 데드락에 빠질 수 있어요. 그래서 "fork 이후 exec 전까지는 async-signal-safe한 함수만 호출하라"는 까다로운 규칙이 생긴 거예요.
대안들은 이미 나와 있어요
사실 이 문제를 풀려는 시도는 계속 있었어요. posix_spawn()은 "복제하고 갈아끼우기"를 하나의 호출로 묶어서, 중간 단계의 위험을 줄여줘요. 내부적으로는 vfork나 clone을 활용하면서도 개발자에게는 깔끔한 인터페이스를 주는 거죠. vfork()는 메모리 복사를 아예 생략해서 빠르지만 제약이 많아 다루기 까다로워요. 더 현대적으로는 clone3() 시스템 콜이 플래그를 정교하게 줄 수 있게 해주고, 최근에는 io_uring을 통해 프로세스 생성까지 비동기로 처리하려는 흐름도 나오고 있어요.
흥미로운 건 다른 운영체제들은 처음부터 다른 길을 택했다는 거예요. 윈도우는 CreateProcess()로 "새 프로그램을 바로 띄운다"는 한 방 접근을 쓰고, fork 같은 복제 개념이 기본이 아니에요. 그래서 리눅스 진영에서도 "우리도 spawn 중심으로 가야 하는 것 아니냐"는 논의가 주기적으로 올라오는 거죠.
한국 개발자에게 주는 시사점
대부분의 백엔드 개발자는 프로세스를 직접 fork할 일이 많지 않아요. 하지만 컨테이너 런타임, 서버리스 콜드 스타트, CI 빌드 시스템처럼 짧은 프로세스를 초당 수천 개씩 만들고 죽이는 환경이라면 이 비용이 실제 지연으로 다가와요. 람다 함수가 느리게 뜨거나, 빌드 워커가 자식 프로세스를 폭발적으로 만들 때 성능 프로파일을 떠보면 의외로 fork/exec 구간이 잡히기도 하거든요.
실무 팁을 하나 드리면, 언어 표준 라이브러리의 프로세스 실행 함수가 내부적으로 무엇을 쓰는지 한 번쯤 들여다보세요. 예를 들어 Go의 os/exec, 파이썬의 subprocess는 플랫폼에 따라 posix_spawn이나 vfork 기반으로 최적화되어 있어요. 메모리를 많이 쓰는 프로세스에서 외부 명령을 자주 호출한다면, posix_spawn 경로를 타는지 확인하는 것만으로도 체감 성능이 달라질 수 있어요.
마무리
fork() + exec()는 단순함이라는 미덕 때문에 반세기를 살아남았지만, 메모리가 거대해지고 멀티스레드가 기본이 된 시대에는 슬슬 비용이 보이기 시작했어요. posix_spawn 같은 대안이 점점 표준 자리를 넘보는 중이고요.
여러분은 어떤가요? 익숙한 fork/exec를 계속 쓰는 게 좋을까요, 아니면 spawn 중심의 새 모델로 넘어가는 게 맞을까요? 직접 프로세스 생성 성능 때문에 고생해본 경험이 있다면 댓글로 나눠주세요.
🔗 출처: Hacker News
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공