동기 방식 메인 코드에서 파일 업로드를 하는 비동기 모듈을 만들어서 쓰려고 할 때 고민한 내용을 정리한 글입니다.
의도(현 구현 방향)
- 목표: 동기 코드(메인 앱)에서 비동기 작업자(업로더 등)를 완전 별도 프로세스로 실행하고, 빠른 취소/종료, 환경변수 주입, 로그 분리, 장애 격리를 쉽게 달성한다.
- 우선순위: 운영 단순성(격리·배포·재시작) > 세밀한 IPC > 최적화 미세튜닝.
- 가정: 작업자는 자체 이벤트 루프/네트워크 스택을 갖고, 성공/실패를 자체적으로 처리/보고할 수 있다.
언제 subprocess가 맞는가?
다음 조건을 2개 이상 만족하면 **subprocess.Popen**이 보통 더 깔끔합니다.
- 작업자가 독립 실행 스크립트 형태다.
- 환경변수/CLI 인자만으로 설정 전달이 충분하다.
- 실패해도 부모 프로세스를 오염시키지 않고 재기동이 쉬워야 한다.
- 로그 파일로 표준 출력을 남기고 외부 툴(systemd/supervisor)로 관리하고 싶다.
- 신호(SIGTERM/SIGUSR1) 로 빠르게 취소·종료를 걸고 싶다.
- 나중에 배포/롤백/핫스왑을 단순하게 가져가고 싶다(venv/스크립트 교체만으로).
장점 요약
- 완전 격리(새 파이썬 인터프리터): 이벤트 루프/소켓/상태 충돌 리스크 낮음
- 운영 친화: env/cwd/stdout 한 줄로 설정, 프로세스 그룹 분리(start_new_session=True)
- 장애 격리/재시작 용이, systemd 연동 쉬움
단점 요약
- 파이썬 객체 단위의 풍부한 IPC가 번거롭다(주로 stdout/파일/소켓/REST로 해결)
- 프로세스 기동 오버헤드(보통 수백 ms)는 짧고 잦은 호출에선 느껴질 수 있음
언제 multiprocessing이 맞는가?
다음 조건을 2개 이상 만족하면 **multiprocessing.Process**가 더 자연스럽습니다.
- 파이썬 함수 자체를 병렬로 실행하고 싶다.
- Queue/Pipe/Manager로 파이썬 객체를 풍부하게 주고받아야 한다.
- 워커를 오래 띄워두고 많은 소작업을 저지연으로 처리해야 한다(풀링).
- 이벤트·진행률·중간결과를 빈번하게 주고받아야 한다.
장점 요약
- 파이썬 객체 직렬화 기반의 쉽고 풍부한 IPC
- 동일 코드베이스 안에서 함수 단위 병렬화가 간편
단점 요약
- 시작 방식(fork/spawn)과 플랫폼 차이에 따른 초기화 규율 필요
(특히 spawn에서 if __name__ == "__main__": 가드, import 순서, 전역실행 주의) - fork 기반에선 부모의 핸들/루프 상태 상속으로 미묘한 버그 가능 → 보수적으로는 spawn 권장(오버헤드↑)
- 외부 서비스처럼 운영/감시/재시작하기엔 subprocess보다 절차가 복잡
결정 체크리스트
- 격리와 운영 단순성이 최우선 → subprocess ✅
- 실시간·빈번한 객체 교환이 핵심 → multiprocessing ✅
- 빠른 취소 반응성은 둘 다 가능(신호 or Event/Queue). 다만 안전한 정리까지 고려하면 subprocess가 더 단순.
- 로그/배포/롤백을 OS 수준에서 다루고 싶다 → subprocess ✅
실전 패턴 모음
1) subprocess 비블로킹 실행(환경변수·로그·신호)
import os, subprocess, signal
from pathlib import Path
APP_DIR = Path("/path/to/app")
PY = "/path/to/venv/bin/python3"
SCRIPT = str(APP_DIR / "worker.py")
env = os.environ.copy()
env["WORKER_PROGRESS"] = "1"
env["WORKER_INTERVAL"] = "0.5"
log = open(str(APP_DIR/"runlogs/worker.log"), "a+", buffering=1)
p = subprocess.Popen(
[PY, SCRIPT, "--archive", "tar.gz"],
cwd=str(APP_DIR), env=env,
stdout=log, stderr=subprocess.STDOUT,
start_new_session=True # 프로세스 그룹 분리 → 신호 제어/정리 용이
)
# … 부모는 즉시 다음 일 수행
# os.kill(p.pid, signal.SIGUSR1) # 취소(사용자)
# p.terminate(); p.wait(timeout=10) # 그레이스풀 종료 → 필요시 SIGKILL
결과 전달:
- 간단: 종료코드(0/비0)
- 중간: stdout 한 줄 JSON / 지정된 파일에 결과 기록
- 고급: 작업자가 서버에 직접 보고(부모는 상태만 확인)
2) multiprocessing으로 파이썬 콜러블 병렬화
from multiprocessing import Process, Queue
import time
def worker(cfg, q: Queue):
# … 작업 …
q.put({"result": "ok", "took": 1.23})
q = Queue()
p = Process(target=worker, args=({"archive": "tar.gz"}, q))
p.start() # 비블로킹
msg = q.get() # 필요 시 결과 수신(블로킹/타임아웃 조절)
p.join()
팁: 네트워크/이벤트루프 초기화는 자식 프로세스 내부에서 새로 열기(특히 fork 환경 주의).
흔한 함정 & 예방책
- 데드락: 파이프(stdout=PIPE)를 열었다면 **communicate()**로 비우거나 파일로 리다이렉트.
- 신호 전파: 리눅스에선 start_new_session=True 또는 os.setsid()로 그룹 분리 후 원하는 신호만 명확히 전송.
- spawn 규율(multiprocessing): 전역 실행 금지, __main__ 가드, import 순서, 전역 상태 초기화 유의.
- 깨끗한 종료: SIGTERM/취소 플래그를 받은 후 파일/소켓/세션을 확실히 정리(특히 비동기 루프).
최종 한 줄
- 독립 실행체(서비스처럼)로 돌리고, 운영·배포·격리를 단순화하려면 subprocess.
- 파이썬 객체를 촘촘히 교환하며 함수 단위 병렬화가 핵심이면 multiprocessing.
당장 깔끔하게 가려면 subprocess로 시작하고, 정말 밀도 높은 상호작용이 필요해지는 지점에서만 multiprocessing으로 전환을 검토하는 전략이 안전합니다.
'IT > python' 카테고리의 다른 글
| Loguru를 활용한 파이썬 로깅 시스템 정리: 중앙 설정 방식 연구 (0) | 2025.09.16 |
|---|---|
| Linux 네트워크 패킷 관리의 핵심: sk_buff 구조체와 메모리 레이아웃 (0) | 2025.04.17 |
| Valkey vs Redis: 2024 오픈소스 인메모리 DB 선택 가이드 및 기능 비교 (0) | 2025.03.27 |
| [python3][one-liner] 섹션 헤더 없는 설정 파일 값 바꾸기 (2) | 2024.12.19 |
| 윈도우 디펜더에서 바이러스라며 pyinstaller 로 생성한 exe 막는 문제 (2) | 2024.05.27 |