배경
ML 추론 파이프라인을 운영하다 보면 처리량(throughput)을 높이기 위해 병렬 처리를 시도하게 된다. 특히 3D 메시 데이터를 다루는 경우 bpy(Blender Python API)를 사용하는 경우가 많은데, 이 라이브러리는 병렬 처리와 관련해서 일반적인 Python 라이브러리와는 전혀 다른 특성을 가진다.
이 글에서는 bpy의 구조적 특성이 왜 병렬 처리를 어렵게 만드는지, 그리고 그 제약 속에서 단일 처리가 왜 성능적으로 최선인지를 실측 데이터와 함께 정리한다. 나아가 이 특성이 AWS Lambda와 얼마나 잘 맞는지도 다룬다.
bpy는 단순한 Python 라이브러리가 아니다
import bpy 한 줄이 실행되는 순간 일어나는 일을 먼저 이해해야 한다.
bpy는 Blender의 Python API 바인딩이다. 단순히 코드와 데이터를 메모리에 올리는 일반적인 라이브러리와 달리, bpy를 임포트하면 Blender 애플리케이션 전체가 헤드리스 모드로 초기화된다.
import bpy
→ Blender 렌더 엔진 초기화
→ OpenGL 컨텍스트 초기화
→ 내장 애드온 로딩
→ 씬 데이터 구조 초기화
→ Python API 바인딩
이 초기화 비용만으로 수 초~수십 초가 소요된다. 실측 기준으로 약 11초가 걸렸다.
bpy가 멀티스레딩과 멀티프로세싱을 거부하는 이유
멀티스레딩 — 공식적으로 지원하지 않음
Blender 공식 문서와 개발자 포럼에 명시된 내용이다.
"So far, no work has gone into making Blender's python integration thread safe."
— Blender Python API Documentation
bpy는 내부적으로 전역 상태(global state)를 프로세스 단위로 관리한다. bpy.data, bpy.context, bpy.ops 모두 단일 전역 인스턴스를 공유하기 때문에, 여러 스레드에서 동시에 접근하면 메모리 충돌, 크래시, 예측 불가능한 결과가 발생한다. 실제로 Blender 개발자 포럼에는 멀티스레딩 시도 후 UI 프리즈, 랜덤 크래시가 발생했다는 보고가 다수 존재한다.
멀티프로세싱 — sys.path 충돌 문제
멀티프로세싱은 각 프로세스가 독립된 메모리 공간을 가지므로 전역 상태 충돌은 피할 수 있다. 하지만 bpy는 임포트 시점에 sys.path를 수정하기 때문에, bpy를 임포트한 후 multiprocessing.Pool을 생성하면 워커 프로세스 부트스트래핑 과정에서 충돌이 발생한다.
bpy 임포트 → sys.path 변경
multiprocessing.Pool 생성 → 변경된 sys.path 복사
워커 프로세스 시작 → bpy 재임포트 시도 → ModuleNotFoundError
이 문제를 우회하려면 bpy 임포트 전 sys.path를 보존하고, 멀티프로세싱 인스턴스 생성 직전에 복원하는 번거로운 작업이 필요하다. 구조적으로 깨끗하지 않다.
subprocess 방식의 함정 — 초기화 비용이 매 요청마다 반복된다
bpy의 멀티프로세싱 제약을 우회하는 가장 일반적인 방법은 subprocess로 완전히 독립된 프로세스를 띄우는 것이다. 이렇게 하면 sys.path 충돌도 피하고, 전역 상태 격리도 보장된다.
문제는 비용이다. subprocess 방식에서는 매 요청마다 새 Python 프로세스가 시작되고, 그 안에서 bpy 초기화와 AI 모델 로딩이 처음부터 다시 발생한다.
요청 N: 새 프로세스 → bpy 초기화(~11초) + 모델 로딩(~수초) → 추론 → ~17초
요청 N+1: 새 프로세스 → bpy 초기화(~11초) + 모델 로딩(~수초) → 추론 → ~17초
실측 결과가 이를 증명했다. subprocess 방식에서 매 요청이 17초 내외로 일정하게 느렸다.
단일 프로세스가 성능적으로 최선인 이유
단일 프로세스 방식으로 전환하면 bpy 초기화와 모델 로딩이 프로세스 시작 시점에 딱 한 번만 발생한다.
프로세스 시작: bpy 초기화(~11초) + 모델 로딩(~수초) → 1회
첫 번째 요청: bpy 재사용 + 모델 재사용 → 추론 + S3 I/O → ~6초
두 번째 요청: bpy 재사용 + 모델 재사용 → 추론 + S3 I/O → ~6초
| 방식 | 첫 번째 요청 | 두 번째 요청~ |
|---|---|---|
| subprocess | ~17초 | ~17초 |
| 단일 프로세스 | ~17초 (초기화 포함) | ~6초 |
두 번째 요청부터는 subprocess 방식 대비 약 3배 빨라진다. 병렬화를 포기하는 대신 단일 요청의 latency를 크게 줄인 것이다.
bpy처럼 초기화 비용이 지배적인 라이브러리를 사용할 때는, 병렬 처리를 고민하기 전에 단일 요청의 비용 구조를 먼저 파악해야 한다. 초기화 비용이 추론 비용보다 압도적으로 크다면, 병렬화는 오히려 역효과다.
그렇다면 Lambda가 자연스러운 이유
단일 프로세스 방식의 한계는 명확하다. 동시 요청이 늘어날수록 큐에 요청이 쌓이고, 처리량이 선형으로 제한된다. 스케일 아웃이 필요한 시점이 온다.
로컬 서버에서 스케일 아웃을 구현하려면 별도의 관리 코드가 필요하다.
로컬 서버 스케일 아웃
→ 프로세스 풀 관리 코드 직접 구현
→ bpy 초기화 상태 관리
→ 워커 간 동시성 제어
→ 프로세스 모니터링 및 재시작 로직
Lambda는 이 모든 것을 구조적으로 해결한다.
단일 처리 관점 — 각 Lambda 인스턴스는 하나의 요청만 처리한다. bpy 전역 상태 충돌 걱정이 없고, Warm Start 시 bpy 초기화와 모델 로딩이 이미 완료된 상태로 요청을 받는다.
스케일 아웃 관점 — 트래픽이 늘어나면 Lambda 인스턴스가 자동으로 늘어난다. 각 인스턴스는 독립된 프로세스이므로 bpy 상태 격리가 자연스럽게 보장된다. 로컬 서버에서 subprocess로 해결하려 했던 격리 문제를 Lambda는 아키텍처 수준에서 해결한다.
Lambda 스케일 아웃
→ 동시 요청 증가 → 인스턴스 자동 확장
→ 각 인스턴스: bpy 초기화 완료 상태로 대기 (Warm)
→ 인스턴스 간 전역 상태 격리 자동 보장
→ 관리 포인트 없음
AWS Lambda는 리전별로 10초마다 최대 1,000개의 실행 환경 인스턴스를 추가할 수 있다. bpy처럼 단일 프로세스 제약이 있는 워크로드에서 이 자동 확장은 개발자가 직접 구현하기 어려운 수준의 스케일 아웃을 가능하게 한다.
실측 비교
| Lambda (Warm) | 로컬 subprocess | 로컬 단일 프로세스 | |
|---|---|---|---|
| bpy 초기화 | init 단계 1회 | 요청마다 반복 | 시작 시점 1회 |
| 모델 로딩 | init 단계 1회 | 요청마다 반복 | 시작 시점 1회 |
| 단일 요청 latency | ~2초 | ~17초 | ~6초 (2번째~) |
| 스케일 아웃 | 인스턴스 자동 확장 | 직접 구현 필요 | 직접 구현 필요 |
| bpy 상태 격리 | 인스턴스 단위 자동 보장 | subprocess 격리 | 순차 처리로 회피 |
Lambda와 로컬 단일 프로세스의 latency 차이(~2초 vs ~6초)는 S3 네트워크 경로 차이로 설명된다. Lambda는 AWS 내부 네트워크를 사용하고, 로컬 서버는 인터넷을 경유한다. 추론 성능 자체의 차이가 아니다.
정리
bpy는 구조적으로 멀티스레딩과 멀티프로세싱 모두 제약이 있다. 멀티스레딩은 thread-safe하지 않고, 멀티프로세싱은 sys.path 충돌로 추가 작업이 필요하다. subprocess 방식은 격리는 보장되지만 bpy 초기화 비용이 매 요청마다 반복되는 치명적인 단점이 있다.
결론적으로 bpy를 사용하는 워크로드에서는 단일 프로세스로 초기화 비용을 1회로 줄이고, 스케일 아웃은 Lambda의 인스턴스 확장에 맡기는 구조가 가장 깔끔하다. Lambda는 bpy의 단일 프로세스 제약을 아키텍처 수준에서 자연스럽게 해결한다.
병렬화가 항상 답은 아니다. 초기화 비용이 실행 비용보다 클 때는 단일 처리가 더 빠르다.