C++ Python과 C++의 만남 | pybind11으로 고성능 엔진 만들기 [#35-1]
이 글의 핵심
C++ Python과 C++의 만남에 대한 실전 가이드입니다. pybind11으로 고성능 엔진 만들기 [#35-1] 등을 예제와 함께 상세히 설명합니다.
들어가며: Python은 편한데, 이 루프만 C++로 돌리고 싶어요
”데이터 전처리 루프가 30분 걸려요”
AI·데이터 사이언스 실무에서 자주 겪는 상황입니다. Pandas로 1억 행을 처리하는 루프가 30분 넘게 걸리고, NumPy 벡터화로도 한계가 있는 복잡한 조건문·중첩 루프가 있습니다. 문제: Python의 인터프리터 특성상 순수 루프는 C에 비해 수십 배 느립니다. 해결: 병목 구간만 C++로 옮기고, Python에서 import 해서 쓰는 패턴입니다. pybind11은 이 과정을 가장 쉽게 만들어 주는 도구입니다.
추가 문제 시나리오
시나리오 1: 실시간 추론 지연
딥러닝 모델의 전처리(이미지 리사이즈, 정규화, 배치 구성)가 Python 루프로 구현되어 있어 추론 지연이 100ms를 넘습니다. C++로 옮기면 10ms 이하로 줄일 수 있습니다.
시나리오 2: 몬테카를로 시뮬레이션
금융 옵션 가격 시뮬레이션에서 1000만 회 반복이 필요합니다. Pure Python으로는 수 시간, C++ 확장으로는 수 분 내에 완료됩니다.
시나리오 3: 대용량 이미지 배치 처리
OpenCV Python 바인딩은 편하지만, 커스텀 필터·복잡한 파이프라인에서는 C++ 직접 호출이 5~10배 빠릅니다.
시나리오 4: 레거시 C++ 라이브러리 활용
기존 C++ 엔진(검색, 랭킹, 암호화 등)을 Python 서비스에서 호출해야 할 때, pybind11으로 래핑하면 최소 코드로 연동할 수 있습니다.
pybind11으로 해결
flowchart LR
subgraph before["문제 상황"]
P1[Python 루프] --> P2[30분 대기]
P2 --> P3[병목]
end
subgraph after["pybind11 적용"]
A1[Python] --> A2[import engine]
A2 --> A3[C++ 확장]
A3 --> A4[2분 완료]
end
pybind11은 C++ 코드를 Python에서 import 가능한 확장 모듈로 만들어 주는 헤더 전용 라이브러리입니다. Boost.Python보다 가볍고, C++11/14 문법을 잘 활용하며, NumPy 배열과의 연동도 지원해서 AI·데이터 사이언스 쪽에서 수요가 매우 높습니다.
이 글에서 다루는 것:
- pybind11이란 무엇인지, 왜 쓰는지
- 최소 예제: C++ 함수·클래스를 Python 모듈로 노출
- 빌드: setuptools / CMake로 확장 모듈 빌드
- NumPy 연동:
py::array_t로 넘기고 받기, stride 처리 - 문제 시나리오와 완전한 예제
- 자주 발생하는 에러와 해결법
- 성능 비교 (Pure Python vs NumPy vs pybind11)
- 프로덕션 패턴 (GIL, 예외, wheel 배포)
요구 환경: Python 3.6+(개발 헤더 필요: python3-dev 등), pybind11(pip pybind11 또는 vcpkg/FetchContent), CMake 3.4+ 또는 setuptools. C++11 이상. NumPy 연동 시 numpy 패키지 설치.
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
목차
- pybind11이란
- 최소 예제: 함수와 클래스 노출
- 빌드: CMake로 확장 모듈 만들기
- NumPy와 연동하기
- 완전한 pybind11 예제
- 자주 발생하는 에러와 해결법
- 성능 비교
- 프로덕션 패턴
- 실무 팁과 주의점
1. pybind11이란
역할
- C++ 타입(함수, 클래스, enum 등) 을 Python 쪽에서 쓸 수 있게 바인딩해 줍니다.
- 헤더 전용: 헤더만 포함하면 되고, Python이 제공하는 C API와 결합해 네이티브 확장 모듈(.pyd / .so) 을 빌드합니다.
- C++11 이상을 전제로 하며, STL 컨테이너(
vector,map등)를 자동으로 Pythonlist,dict등으로 변환해 줍니다.
아키텍처 개요
flowchart TB
subgraph python["Python"]
P[Python 스크립트]
P --> |import| M[확장 모듈 .pyd/.so]
end
subgraph cpp["C++"]
M --> |pybind11 바인딩| C[C++ 함수/클래스]
C --> |NumPy 버퍼| N["py array_t"]
end
N --> |복사 없이| P
왜 pybind11인가
- Boost.Python 대비 가볍고, 컴파일이 빠르며, 최신 C++ 스타일과 잘 맞습니다.
- NumPy의 버퍼 프로토콜을 지원해, 배열을 복사 없이 C++에 넘기거나 받을 수 있어, 행렬 연산·이미지 처리 같은 무거운 연산에 적합합니다.
- AI·데이터 파이프라인에서 “학습/추론의 핵심 루프만 C++”, 나머지는 Python” 구조를 만들 때 표준적으로 쓰입니다.
2. 최소 예제: 함수와 클래스 노출
함수 하나 노출
PYBIND11_MODULE(example, m)에서 example은 Python에서 import example로 불러올 때의 모듈 이름이고, m은 그 모듈 객체입니다. m.def(“add”, &add, …)로 C++ 함수 add를 Python 이름 “add”에 연결하면, 빌드된 확장 모듈(.pyd 또는 .so)을 로드한 뒤 example.add(1, 2)처럼 호출할 수 있습니다.
// example.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int a, int b) {
return a + b;
}
PYBIND11_MODULE(example, m) {
m.doc() = "minimal pybind11 example";
m.def("add", &add, "Add two integers");
}
- PYBIND11_MODULE(example, m): Python에서
import example했을 때의 모듈 이름이example이고,m이 그 모듈 객체입니다. - m.def(“add”, &add, …): C++ 함수
add를 Python 이름"add"로 등록합니다.
빌드 후 Python에서:
import example
print(example.add(1, 2)) # 3
클래스 노출
py::class_
class Calculator {
public:
int mul(int a, int b) const { return a * b; }
};
PYBIND11_MODULE(example, m) {
py::class_<Calculator>(m, "Calculator")
.def(py::init<>())
.def("mul", &Calculator::mul);
}
- py::class_
(m, “Calculator”) : C++ 클래스Calculator를 Python 클래스Calculator로 등록. - .def(py::init<>()): 기본 생성자.
- .def(“mul”, &Calculator::mul): 메서드
mul을 노출.
Python에서:
import example
calc = example.Calculator()
print(calc.mul(3, 4)) # 12
3. 빌드: CMake로 확장 모듈 만들기
CMakeLists.txt 예시
cmake_minimum_required(VERSION 3.15)
project(example LANGUAGES CXX)
find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
find_package(pybind11 CONFIG REQUIRED)
pybind11_add_module(example example.cpp)
target_link_libraries(example PRIVATE pybind11::embed)
- pybind11_add_module(example example.cpp):
example.cpp를 빌드해example확장 모듈을 만듭니다. (Windows:example.pyd, Linux/macOS:example.cpython-3xx.so) - 빌드 결과물을 Python의 sys.path에 넣거나,
import하는 디렉터리에 두면import example로 사용할 수 있습니다.
setuptools로 빌드 (pyproject.toml)
[build-system]
requires = ["setuptools", "pybind11"]
build-backend = "setuptools.build_meta"
[project]
name = "my_engine"
version = "0.1.0"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-dir]
"" = "src"
# setup.py (또는 pyproject.toml의 [tool.setuptools] 섹션)
from setuptools import setup, Extension
ext = Extension(
"my_engine",
sources=["src/my_engine.cpp"],
include_dirs=[],
language="c++",
)
setup(ext_modules=[ext])
pyproject.toml또는setup.py에서 pybind11을 빌드 의존성으로 두고, Extension으로 소스와 include 경로를 지정해pip install .로 빌드·설치하는 방식입니다. 팀 공유·패키지 배포 시 유용합니다.
4. NumPy와 연동하기
배열을 C++에 넘기기
py::array_t
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
// NumPy 배열을 받아서 합을 구하는 예
double sum_array(py::array_t<double> arr) {
auto buf = arr.request();
double* ptr = static_cast<double*>(buf.ptr);
size_t n = buf.size;
double s = 0;
for (size_t i = 0; i < n; ++i) s += ptr[i];
return s;
}
PYBIND11_MODULE(example, m) {
m.def("sum_array", &sum_array);
}
- request() 로 버퍼 정보(ptr, size, shape, stride)를 얻고, 연속(contiguous) 가정이 맞는지 확인한 뒤 사용하는 것이 안전합니다.
- 반환할 때는
py::array_t<T>를 만들어 반환하면 Python 쪽에서 NumPy 배열로 받을 수 있습니다.
배열 반환하기
py::array_t<double> create_array(size_t n) {
auto result = py::array_t<double>(n);
auto buf = result.request();
double* ptr = static_cast<double*>(buf.ptr);
for (size_t i = 0; i < n; ++i) ptr[i] = static_cast<double>(i);
return result;
}
PYBIND11_MODULE(example, m) {
m.def("create_array", &create_array);
}
import example
import numpy as np
arr = example.create_array(10)
print(arr) # [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
GIL 해제 (긴 연산 시)
GIL(Global Interpreter Lock)을 유지한 채로 오래 걸리는 C++ 연산을 하면, 다른 Python 스레드가 블로킹됩니다. py::gil_scoped_release로 GIL을 해제하세요.
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
double heavy_compute(py::array_t<double> arr) {
py::gil_scoped_release release; // GIL 해제
auto buf = arr.request();
double* ptr = static_cast<double*>(buf.ptr);
size_t n = buf.size;
double s = 0;
for (size_t i = 0; i < n; ++i) s += ptr[i];
return s; // 반환 시 자동으로 GIL 재획득
}
- GIL을 풀었을 때는 Python 객체를 건드리지 않도록 주의합니다. 순수 C++ 데이터만 다루세요.
실무 활용 사례
AI/ML 파이프라인: PyTorch나 TensorFlow로 학습한 모델의 전처리·후처리만 C++로 옮겨 속도를 10배 이상 올린 사례가 많습니다.
이미지/비디오 처리: OpenCV C++ 코드를 pybind11로 감싸 Python 스크립트에서 호출하면, Jupyter 노트북에서 빠르게 실험하면서도 핵심 연산은 C++의 SIMD·멀티스레드로 가속할 수 있습니다.
금융/시뮬레이션: 몬테카를로 시뮬레이션처럼 반복 연산이 많은 로직을 C++로 짜고, Python에서 파라미터를 넘겨 결과를 받으면 분석·시각화는 Python(pandas, matplotlib)으로, 연산은 C++로 분리할 수 있습니다.
5. 완전한 pybind11 예제
예제 1: 벡터 내적 (NumPy 연동 + GIL 해제)
// dot_product.cpp
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
double dot_product(py::array_t<double> a, py::array_t<double> b) {
auto buf_a = a.request();
auto buf_b = b.request();
if (buf_a.size != buf_b.size) {
throw std::runtime_error("배열 크기가 일치하지 않습니다");
}
double* pa = static_cast<double*>(buf_a.ptr);
double* pb = static_cast<double*>(buf_b.ptr);
size_t n = buf_a.size;
py::gil_scoped_release release;
double result = 0;
for (size_t i = 0; i < n; ++i) {
result += pa[i] * pb[i];
}
return result;
}
PYBIND11_MODULE(dot_product, m) {
m.doc() = "NumPy 배열 내적 계산";
m.def("dot_product", &dot_product, "Compute dot product of two arrays");
}
# test_dot.py
import numpy as np
import dot_product
a = np.random.rand(10_000_000)
b = np.random.rand(10_000_000)
result = dot_product.dot_product(a, b)
expected = np.dot(a, b)
print(f"pybind11: {result}, NumPy: {expected}, diff: {abs(result - expected)}")
예제 2: 사용자 정의 예외 등록
#include <pybind11/pybind11.h>
namespace py = pybind11;
class ValidationError : public std::exception {
public:
const char* what() const noexcept override {
return "Validation failed";
}
};
void validate_positive(int x) {
if (x <= 0) {
throw ValidationError();
}
}
PYBIND11_MODULE(validation, m) {
py::register_exception<ValidationError>(m, "ValidationError");
m.def("validate_positive", &validate_positive);
}
import validation
try:
validation.validate_positive(-1)
except validation.ValidationError as e:
print("Caught:", e)
예제 3: STL 컨테이너 자동 변환
#include <pybind11/pybind11.h>
#include <vector>
#include <map>
#include <string>
namespace py = pybind11;
std::vector<int> get_primes(int n) {
std::vector<int> primes;
for (int i = 2; i <= n; ++i) {
bool is_prime = true;
for (int p : primes) {
if (p * p > i) break;
if (i % p == 0) { is_prime = false; break; }
}
if (is_prime) primes.push_back(i);
}
return primes;
}
std::map<std::string, int> count_chars(const std::string& s) {
std::map<std::string, int> m;
for (char c : s) {
m[std::string(1, c)]++;
}
return m;
}
PYBIND11_MODULE(containers, m) {
m.def("get_primes", &get_primes);
m.def("count_chars", &count_chars);
}
import containers
primes = containers.get_primes(20)
print(primes) # [2, 3, 5, 7, 11, 13, 17, 19]
counts = containers.count_chars("hello")
print(counts) # {'h': 1, 'e': 1, 'l': 2, 'o': 1}
6. 자주 발생하는 에러와 해결법
문제 1: “ModuleNotFoundError: No module named ‘example’”
원인: 빌드된 .pyd 또는 .so 파일이 Python이 찾는 경로에 없음.
해결법:
# 빌드 결과물 위치 확인
ls build/*.so # Linux/macOS
ls build/*.pyd # Windows
# 현재 디렉터리에서 실행하거나 PYTHONPATH 설정
export PYTHONPATH="${PYTHONPATH}:$(pwd)/build"
python -c "import example"
또는 sys.path에 추가:
import sys
sys.path.insert(0, "/path/to/build")
import example
문제 2: “ImportError: … undefined symbol: PyInit_example”
원인: 모듈 이름 불일치. PYBIND11_MODULE(example, m)의 첫 인자와 import example이 일치해야 함. 또는 Python 버전/ABI 불일치.
해결법:
- 모듈 이름 확인:
PYBIND11_MODULE의 첫 인자 =import이름 - Python 버전 확인:
python3 --version과 빌드 시 사용한 Python이 동일한지 확인
문제 3: “find_package(pybind11) failed”
원인: pybind11이 CMake에서 찾을 수 없음.
해결법:
# pip로 설치 후 CMAKE_PREFIX_PATH 설정
pip install pybind11
export CMAKE_PREFIX_PATH=$(python3 -c "import pybind11; print(pybind11.get_cmake_dir())")
cmake ..
또는 FetchContent로 pybind11 포함:
include(FetchContent)
FetchContent_Declare(
pybind11
GIT_REPOSITORY https://github.com/pybind/pybind11.git
GIT_TAG v2.11.1
)
FetchContent_MakeAvailable(pybind11)
문제 4: NumPy 배열 “buffer is not contiguous”
원인: arr.request()로 받은 버퍼가 C-contiguous가 아님 (예: arr.T, arr[:, ::2]).
해결법 1 — Python에서 연속 배열로 변환:
import numpy as np
arr = np.arange(12).reshape(3, 4).T # Fortran order
arr = np.ascontiguousarray(arr) # C order로 변환
result = example.sum_array(arr)
해결법 2 — C++에서 stride 처리:
double sum_array_strided(py::array_t<double> arr) {
auto buf = arr.request();
if (buf.ndim != 1) {
throw std::runtime_error("1D array only");
}
double* ptr = static_cast<double*>(buf.ptr);
size_t stride = buf.strides[0] / sizeof(double);
size_t n = buf.shape[0];
double s = 0;
for (size_t i = 0; i < n; ++i) {
s += ptr[i * stride];
}
return s;
}
문제 5: “Fatal Python error: PyThreadState_Get: no current thread”
원인: GIL을 해제한 상태에서 Python API를 호출함.
해결법: GIL 해제 구간에서는 Python 객체에 접근하지 말 것. 순수 C++ 포인터(buf.ptr 등)만 사용.
// ❌ 잘못된 예
double bad(py::array_t<double> arr) {
py::gil_scoped_release release;
py::print("hello"); // ❌ Python API 호출 - 크래시!
// ...
}
// ✅ 올바른 예
double good(py::array_t<double> arr) {
auto buf = arr.request(); // GIL 유지 상태에서 버퍼 정보 획득
double* ptr = static_cast<double*>(buf.ptr);
size_t n = buf.size;
py::gil_scoped_release release;
double s = 0;
for (size_t i = 0; i < n; ++i) s += ptr[i];
return s; // 반환 시 GIL 자동 재획득
}
문제 6: “TypeError: … incompatible function arguments”
원인: Python에서 넘긴 타입이 C++ 함수 시그니처와 맞지 않음 (예: float 넘기고 int 기대).
해결법: py::arg()로 인자 힌트 추가:
m.def("add", &add, py::arg("a"), py::arg("b") = 0);
또는 Python에서 올바른 타입으로 변환:
example.add(int(x), int(y))
문제 7: Windows에서 “LINK : fatal error LNK1104: cannot open file ‘python311.lib’”
원인: Python 개발 라이브러리 경로를 찾지 못함.
해결법:
- python.org에서 “Windows embeddable package”가 아닌 전체 설치판 사용
- 또는
py -3.11 -m pip install pybind11후 해당 환경의 Python 사용
7. 성능 비교
벤치마크 설정
동일 연산(1억 개 double 합산)을 Pure Python, NumPy, pybind11 C++로 비교합니다.
# benchmark.py
import time
import numpy as np
def pure_python_sum(n):
total = 0.0
for i in range(n):
total += 1.0 / (i + 1)
return total
def numpy_sum(n):
arr = np.arange(1, n + 1, dtype=np.float64)
return np.sum(1.0 / arr)
# pybind11 모듈 (sum_array 또는 유사 함수)
# import engine
# def pybind11_sum(n):
# arr = np.arange(1, n + 1, dtype=np.float64)
# return engine.sum_array(1.0 / arr)
n = 100_000_000
t0 = time.perf_counter()
r1 = pure_python_sum(n)
t1 = time.perf_counter()
print(f"Pure Python: {t1-t0:.3f}s, result={r1:.6f}")
t0 = time.perf_counter()
r2 = numpy_sum(n)
t1 = time.perf_counter()
print(f"NumPy: {t1-t0:.3f}s, result={r2:.6f}")
결과 요약 (참고치, 환경에 따라 다름)
| 방식 | 1억 원소 합산 | 상대 속도 |
|---|---|---|
| Pure Python | ~25초 | 1x (기준) |
| NumPy | ~0.5초 | ~50x |
| pybind11 C++ | ~0.15초 | ~165x |
해석:
- NumPy는 C로 구현된 루프를 사용하므로 이미 매우 빠릅니다.
- pybind11은 NumPy보다 추가로 2~5배 정도 빠를 수 있으며, NumPy로 표현하기 어려운 복잡한 로직에서 차이가 큽니다.
- 조건문, 중첩 루프, 커스텀 알고리즘에서는 pybind11이 10~100배 이득을 보는 경우가 많습니다.
메모리 사용량
| 방식 | 1억 double (800MB) 처리 시 |
|---|---|
| Pure Python | 루프 변수만 사용, 배열 없음 |
| NumPy | 800MB (배열) + 오버헤드 |
| pybind11 | NumPy와 동일 (버퍼 공유, 복사 없음) |
pybind11은 복사 없이 NumPy 버퍼를 그대로 사용하므로 메모리 오버헤드가 거의 없습니다.
8. 프로덕션 패턴
패턴 1: 에러 처리와 로깅
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <spdlog/spdlog.h> // 또는 다른 로깅 라이브러리
namespace py = pybind11;
double safe_sum(py::array_t<double> arr) {
if (!arr) {
throw std::invalid_argument("배열이 None입니다");
}
auto buf = arr.request();
if (buf.ndim != 1) {
throw std::invalid_argument("1차원 배열만 지원합니다");
}
double* ptr = static_cast<double*>(buf.ptr);
size_t n = buf.size;
double s = 0;
for (size_t i = 0; i < n; ++i) s += ptr[i];
return s;
}
패턴 2: 버전 및 ABI 호환성
PYBIND11_MODULE(example, m) {
m.attr("__version__") = "1.0.0";
m.def("add", &add);
}
배포 시 Python 버전별 wheel 제공:
# manylinux로 Linux wheel 빌드
docker run --rm -v $(pwd):/io quay.io/pypa/manylinux_2_28_x86_64 \
/io/scripts/build_wheels.sh
패턴 3: 초기화/정리 훅
PYBIND11_MODULE(example, m) {
m.def("init", {
// 전역 리소스 초기화
return true;
});
m.def("shutdown", {
// 정리
});
}
패턴 4: 설정 주입
struct Config {
int num_threads = 4;
bool use_gpu = false;
};
Config g_config;
void set_config(int num_threads, bool use_gpu) {
g_config.num_threads = num_threads;
g_config.use_gpu = use_gpu;
}
PYBIND11_MODULE(example, m) {
m.def("set_config", &set_config);
}
패턴 5: pyproject.toml 기반 패키지 구조
my_engine/
├── pyproject.toml
├── src/
│ └── my_engine/
│ ├── __init__.py
│ └── _native.cpp
└── tests/
└── test_engine.py
# src/my_engine/__init__.py
from ._native import add, sum_array # noqa: F401
__all__ = ["add", "sum_array"]
패턴 6: 다차원 배열 처리 (2D 행렬)
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
// 2D 행렬의 각 행 합계 반환
py::array_t<double> row_sums(py::array_t<double> arr) {
auto buf = arr.request();
if (buf.ndim != 2) {
throw std::runtime_error("2D 배열만 지원합니다");
}
size_t rows = buf.shape[0];
size_t cols = buf.shape[1];
auto result = py::array_t<double>(rows);
auto rbuf = result.request();
double* ptr = static_cast<double*>(buf.ptr);
double* out = static_cast<double*>(rbuf.ptr);
size_t stride = buf.strides[0] / sizeof(double);
for (size_t i = 0; i < rows; ++i) {
double s = 0;
for (size_t j = 0; j < cols; ++j) {
s += ptr[i * stride + j];
}
out[i] = s;
}
return result;
}
PYBIND11_MODULE(matrix_ops, m) {
m.def("row_sums", &row_sums);
}
import numpy as np
import matrix_ops
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float64)
result = matrix_ops.row_sums(arr)
print(result) # [6. 15.]
프로덕션 배포 체크리스트
- Python 버전별 wheel 빌드 (3.8, 3.9, 3.10, 3.11 등)
- manylinux/macOS/Windows 플랫폼 지원
- 사용자 정의 예외 등록 (
py::register_exception) - GIL 해제 구간에서 Python API 미호출 확인
- NumPy 배열 연속성 검사 또는
np.ascontiguousarray문서화 -
__version__속성 노출 - 에러 메시지 한글/영문 일관성
- CI에서 빌드·테스트 자동화
9. 실무 팁과 주의점
GIL (Global Interpreter Lock)
- Python C API를 쓰는 확장은 GIL의 영향을 받습니다. 순수하게 숫자만 돌리는 C++ 구간에서는 py::gil_scoped_release 로 GIL을 풀어 두면, 다른 스레드가 Python을 실행할 수 있어 병렬화 시 유리할 수 있습니다. GIL을 풀었을 때는 Python 객체를 건드리지 않도록 주의합니다.
예외
- C++ 예외는 pybind11이 Python 예외로 변환해 전파합니다.
std::exception→RuntimeError등. 사용자 정의 예외도 등록해 두면 Python에서except로 잡을 수 있습니다.
ABI·버전
- 확장 모듈은 특정 Python 버전·컴파일러와 붙어 빌드되므로, 배포 시 같은 환경에서 쓰거나, wheel을 버전별로 여러 개 두는 방식이 필요합니다. 가상환경·conda와 함께 쓰면 관리가 수월합니다.
빌드 실패 시 체크리스트
- Python 버전:
find_package(Python3)또는 pip/conda 환경의 Python과 같은 버전·경로를 쓰는지 확인. 다른 Python을 쓰면 모듈이 import 시 로드되지 않을 수 있음. - pybind11 경로:
find_package(pybind11 CONFIG)가 실패하면, pybind11을 설치했는지, CMAKE_PREFIX_PATH나 pybind11_DIR에 올바른 경로가 잡혀 있는지 확인. - 컴파일러: MSVC는 /std:c++14 이상 권장. GCC/Clang은 -std=c++14 이상. pybind11은 C++11 이상을 요구.
- 확장자·이름: Windows에서는
.pyd, Linux/macOS에서는.so. 모듈 이름(PYBIND11_MODULE(이름, m)의 첫 인자)과import 이름이 일치해야 함. - 경로: 빌드 결과물(.pyd 또는 .so)이 Python이 찾는 경로(현재 디렉터리, PYTHONPATH, site-packages 등)에 있어야
import가능.
자주 하는 실수 요약
| 실수 | 해결법 |
|---|---|
| GIL 유지한 채 긴 연산 | py::gil_scoped_release 사용 |
| NumPy 배열 연속성 미확인 | np.ascontiguousarray() 또는 stride 처리 |
| 사용자 정의 예외 미등록 | py::register_exception<> 호출 |
| 모듈 이름 불일치 | PYBIND11_MODULE 첫 인자 = import 이름 |
| GIL 해제 후 Python API 호출 | 해제 구간에서는 C++ 데이터만 사용 |
단계별 실습: 첫 pybind11 모듈 만들기
1단계: 환경 준비 (5분)
# Python 개발 헤더 설치
sudo apt install python3-dev # Ubuntu/Debian
# 또는 brew install [email protected] # macOS
# pybind11 설치
pip install pybind11
2단계: 간단한 C++ 함수 작성 (3분)
// mymodule.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
int multiply(int a, int b) { return a * b; }
PYBIND11_MODULE(mymodule, m) {
m.def("multiply", &multiply);
}
3단계: 빌드 (2분)
c++ -O3 -Wall -shared -std=c++11 -fPIC \
$(python3 -m pybind11 --includes) \
mymodule.cpp -o mymodule$(python3-config --extension-suffix)
4단계: 테스트 (1분)
import mymodule
print(mymodule.multiply(6, 7)) # 42
소요 시간: 총 11분이면 첫 pybind11 모듈을 만들고 실행할 수 있습니다.
다음 단계로 나아가기
이 글을 마스터했다면:
- NumPy 고급 연동: stride, 다차원 배열, 메모리 레이아웃 최적화
- 멀티스레딩: GIL 관리, 병렬 처리 패턴
- 패키지 배포: PyPI에 wheel 배포, manylinux 빌드
관련 글: 멀티스레드 기초(#7-1), 성능 최적화(#15-1)
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출 [#40-1]
- C++ 제약된 환경에서의 C++: Exception과 RTTI 없이 안전한 코드 짜기 [#42-1]
- C++ vs Python 비교 | “어떤 언어를 배워야 할까?” 완벽 가이드
이 글에서 다루는 키워드 (관련 검색어)
pybind11 튜토리얼, Python C++ 연동, C++ Python 바인딩, NumPy C++ 연동, Python 확장 모듈, C++ Python 성능 최적화, 머신러닝 C++ 가속, 데이터 사이언스 C++ 등으로 검색하시면 이 글이 도움이 됩니다.
정리
- pybind11은 C++ 코드를 Python 확장 모듈로 만들어 주는 헤더 전용 라이브러리로, 고성능 엔진을 C++로 구현하고 Python에서
import해서 쓰는 패턴에 적합합니다. - m.def로 함수, py::class_ 로 클래스를 노출하고, CMake 또는 setuptools로 빌드합니다.
- py::array_t로 NumPy 배열을 넘기고 받으면, 복사 없이 대량 데이터를 C++에서 처리할 수 있어 AI·데이터 사이언스 워크플로에서 수요가 높습니다.
- 실무에서는 GIL 해제 구간, 예외 변환, ABI·Python 버전을 고려해 사용하면 됩니다.
- 문제 시나리오를 먼저 파악하고, 자주 발생하는 에러와 프로덕션 패턴을 적용하면 안정적으로 활용할 수 있습니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. AI·데이터 사이언스 시대에 무거운 연산은 C++로, Python에서 import 해서 쓰는 방법. pybind11으로 모듈 빌드·바인딩·NumPy 연동까지 실무 관점으로 정리합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. NumPy만으로 부족한가요?
A. NumPy 벡터화로 충분한 경우가 많습니다. 하지만 조건문이 복잡하거나, 중첩 루프가 많거나, 커스텀 알고리즘을 쓸 때는 pybind11이 10~100배 빠를 수 있습니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 pybind11 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: pybind11으로 C++을 Python에서 불러 쓸 수 있어 고성능 엔진 연동에 적합합니다. 다음으로 Wasm·Emscripten(#35-2)를 읽어보면 좋습니다.
다음 글: [C++ 실무 융합 #35-2] WebAssembly(Wasm)와 Emscripten: C++을 브라우저에서 돌리기
이전 글: [C++ 면접 심화 #34-2] 캐시 히트(Cache Hit)를 높이는 C++ 메모리 정렬과 패딩
관련 글
- C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]
- C++ Python 스크립팅 완벽 가이드 | pybind11 모듈·클래스·NumPy·예외 처리 [실전]
- C++ Data Race |
- C++ 캐시 히트(Cache Hit)를 높이는 메모리 정렬과 패딩 | False Sharing 해결
- C++ Lock-Free 프로그래밍 실전 | CAS·ABA·메모리 순서·고성능 큐 [#34-3]