C++ ♥ Python: 두 언어를 하나로 연결하는 실전 가이드
📺 원본 영상: C++ ♥ Python - Alex Dathskovsky - CppCon 2025
🎙️ 채널: CppCon
📅 정리 일자: 2026-01-16
⚠️ Note: 본 리포트는 영상의 음성을 텍스트로 변환(STT)하여 정리한 것입니다. 화면에 표시된 자료(차트, 코드, 슬라이드 등)는 포함되지 않을 수 있습니다.
개요
CppCon 2025에서 Alex Dathskovsky가 발표한 이 강연은 C++와 Python이라는 두 언어가 왜 함께 사용되어야 하는지, 그리고 어떻게 효과적으로 결합할 수 있는지를 다룬다. 현대 소프트웨어 생태계에서 C++는 성능을, Python은 생산성을 책임지며 서로 보완적인 관계를 형성하고 있다. 강연자는 17년 이상의 시스템 소프트웨어 개발 경험을 바탕으로 PyBind11, Cython, cppyy 같은 바인딩 도구들의 실전 활용법을 소개하고, Python의 동적 타이핑 문제를 해결하는 현대적 접근법, 그리고 C++26의 Reflection 기능을 활용한 자동 바인딩 코드 생성까지 폭넓게 다룬다.
C++와 Python: 왜 함께 사용해야 하는가
설문조사가 증명한 현실
강연자는 발표에 앞서 흥미로운 설문조사 결과를 공개했다. LinkedIn에서 약 600명의 C++ 개발자를 대상으로 “두 번째로 많이 사용하는 언어가 무엇인가”를 물었을 때, Python이 압도적 1위를 차지했다. 더 놀라운 것은 C++ Standard Survey에서 약 1,200명을 대상으로 한 조사에서도 동일한 결과가 나왔다는 점이다. 이는 단순한 우연이 아니라, C++와 Python이 현대 소프트웨어 개발에서 사실상 한 쌍으로 기능하고 있음을 보여준다.
이러한 경향은 특히 머신러닝과 AI 분야에서 두드러진다. 강연자는 LLM(Large Language Model)을 예로 들며, 거의 모든 AI 워크플로우가 Python으로 작성되지만 그 내부는 C++와 CUDA로 구현되어 있다고 강조했다. Python은 사용자 인터페이스와 빠른 프로토타이핑을, C++는 실제 연산 성능을 담당하는 구조다.
C++가 필요한 이유
C++는 성능이 중요한 모든 곳에서 여전히 필수적이다. 강연자는 C++를 사랑하는 이유를 “고통을 즐기기 때문”이라고 농담했지만, 실제로는 시스템 레벨 제어, 메모리 최적화, 그리고 하드웨어와의 직접적인 상호작용이 가능하다는 점이 핵심이다. 특히 실시간 시스템, 임베디드 환경, 고성능 컴퓨팅에서는 C++를 대체할 수 없다.
Python이 필요한 이유
반면 Python은 빠른 개발(Fast Development)과 간결한 인터페이스(Simpler Interfaces)를 제공한다. POC(Proof of Concept)를 작성할 때 Python만큼 빠른 언어는 없다. 알고리즘 연구자들은 Matlab과 유사한 구조 덕분에 프로그래밍 경험이 부족해도 코드를 작성할 수 있다. 무엇보다 인터프리터 언어라는 특성상 컴파일 시간이 없어, 노트북 환경에서도 45분씩 기다릴 필요 없이 즉각적인 피드백을 받을 수 있다. Jupyter Notebook을 통한 인터랙티브한 개발 방식은 데이터 분석과 알고리즘 프로토타이핑에서 혁명적이다.
C++가 Python을 닮아가는 방법
강연자는 C++가 점점 Python과 유사한 문법을 제공하기 시작했다며 여러 예시를 들었다. 이는 두 언어가 서로 영향을 주고받으며 발전하고 있음을 보여준다.
문자열 포매팅의 진화
Python에서는 오래전부터 f-string이나 .format() 메서드로 간결하게 문자열을 포매팅할 수 있었다. C++에서는 std::cout과 << 연산자로 출력했지만, 이는 가독성이 떨어졌다. 그러나 FMT 라이브러리와 C++23의 std::print가 도입되면서 상황이 달라졌다. 강연자는 튜플과 숫자를 포함한 문자열을 Python과 거의 동일한 방식으로 포매팅하는 예제를 보여주었다. 심지어 컨테이너(배열, unordered_map)를 직접 출력할 수 있게 되어, Python의 print(list) 스타일에 근접했다.
다중 반환값과 Structural Binding
Python에서는 함수가 여러 값을 반환하고 이를 동시에 언패킹하는 것이 자연스럽다. 예를 들어 a, b, c = func()처럼 작성할 수 있다. C++11부터는 std::tuple을 통해 다중 반환이 가능했지만, 사용법이 불편했다. C++17에서 도입된 Structural Binding 덕분에 auto [a, b, c] = func()와 같은 형태로 작성할 수 있게 되었다. 이는 Python과 거의 동일한 패턴이며, 코드 가독성을 크게 향상시킨다.
Range-Based For Loop와 Enumerate
Python의 for item in container 구문은 직관적이다. C++11의 Range-based for loop도 이와 유사하지만, Python의 enumerate() 함수처럼 인덱스와 값을 동시에 가져오는 것은 까다로웠다. 강연자는 C++20부터 for loop 내부에서 변수 초기화가 가능해졌다는 점을 지적하며, 이를 활용한 방법을 시연했다. 더 나아가 C++23의 Generator를 사용하면 Python의 enumerate와 동일한 동작을 하는 함수를 직접 구현할 수 있다. Coroutine의 co_yield를 활용해 인덱스와 값을 쌍으로 반환하는 제너레이터를 만들면, 사용 코드는 Python과 거의 구분이 되지 않는다.
값 무시하기: std::ignore와 Underscore
Python에서는 필요 없는 반환값을 언더스코어(_)로 무시하는 관습이 있다. C++11부터 std::ignore가 존재했지만, 강연자가 청중에게 물었을 때 손을 든 사람은 극소수였다. std::ignore는 튜플 언패킹 시 특정 값을 무시하도록 하는 유틸리티지만, 문법이 장황했다. C++26에서는 드디어 언더스코어를 직접 사용할 수 있게 되어, Python과 거의 동일한 형태로 코드를 작성할 수 있게 되었다.
Functional Programming과 Ranges
Python의 filter(), map(), range() 같은 함수형 프로그래밍 도구는 강력하다. C++20의 Ranges 라이브러리는 이와 유사한 기능을 제공한다. 강연자는 랜덤 숫자 20개를 생성하고, 5보다 크고 9보다 작은 값만 필터링하는 예제를 Python과 C++로 각각 보여주었다. C++의 파이프 연산자(|)를 사용한 체이닝은 Python의 함수 호출 스타일과 매우 유사하다. 더 복잡한 예제로, 30개의 숫자에서 홀수만 필터링하고, 그 중 처음 10개를 취하고, 이를 역순으로 정렬하는 작업도 단 세 줄로 구현할 수 있다. 이는 전통적인 Iterator 방식이라면 25줄 이상이 필요한 작업이다.
주의사항: std::views의 함정
그러나 강연자는 std::views의 위험성도 경고했다. Views는 실제 데이터를 복사하지 않고 참조만 유지하는 방식이기 때문에, 원본 데이터가 삭제되거나 수정되면 Undefined Behavior가 발생한다. 따라서 Views를 사용할 때는 데이터 생명주기를 신중하게 관리해야 한다.
Python의 한계와 현대적 해결책
인터프리터 언어의 근본적 한계
Python의 가장 큰 장점이자 가장 큰 단점은 인터프리터 언어라는 점이다. 이는 개발 속도를 높여주지만, 실행 성능은 느리다. 동적 타이핑(Dynamic Typing) 덕분에 유연하지만, 타입 안정성이 부족해 런타임 에러가 발생하기 쉽다. 강연자는 이러한 문제를 구체적인 예제로 보여주었다.
동적 타이핑의 위험성
강연자는 청중에게 간단한 Python 함수를 보여주며 문제점을 찾아보라고 요청했다. 함수는 두 개의 매개변수를 받아 합을 반환해야 하지만, return 문이 누락되어 있었다. Python은 이를 문법 오류로 간주하지 않고, 함수가 호출되기 전까지는 아무런 경고도 하지 않는다. 더 심각한 경우는 함수 내부에서 사용되어야 할 변수를 매개변수로 받지 않았는데, 상위 스코프에 같은 이름의 변수가 존재하면 그것을 사용해버린다는 점이다. 강연자는 이런 버그를 2일간 디버깅한 경험을 공유하며, Python의 동적 스코프 해석이 얼마나 위험할 수 있는지 강조했다.
또 다른 문제는 숫자 타입의 자동 확장이다. C++에서는 UINT32_MAX + 1이 오버플로우로 0이 되지만, Python은 자동으로 64비트로 확장해버린다. 이는 편리할 수 있지만, 예상치 못한 메모리 사용과 성능 저하를 초래할 수 있다. 비트 연산도 마찬가지다. Python의 bin() 함수는 이진수를 반환하지만, 실제로는 문자열이다. 비트 단위 작업을 하려는데 문자열로 처리되면, 메모리가 폭발적으로 증가하고 성능이 급격히 떨어진다.
Python 3.10 이후의 타입 힌팅
다행히 Python도 진화하고 있다. Python 3.10부터 도입된 타입 힌팅(Type Hinting)은 IDE가 코드 작성 시점에 타입 불일치를 경고할 수 있게 해준다. 함수 시그니처에 def add(a: int, b: int) -> int:처럼 타입을 명시하면, 잘못된 타입이 전달될 때 IDE가 경고를 표시한다. return 문이 누락되었을 때도 마찬가지다. 하지만 이는 어디까지나 경고일 뿐, 강제성은 없다.
Python 3.12의 제네릭 함수
Python 3.12는 한 걸음 더 나아가 제네릭 함수를 지원한다. C++의 템플릿과 유사하게, 함수가 특정 타입에 대해 파라메트릭하게 동작하도록 할 수 있다. 예를 들어 def foo[T: int | str](a: list[T], b: T) -> list[T]:처럼 정의하면, T는 int 또는 str만 허용된다. 리스트에 float를 추가하려 하면 IDE가 즉시 오류를 표시한다. type 키워드를 사용하면 복잡한 타입 표현을 간결하게 만들 수 있다.
mypy와 Ruff: 정적 분석의 필수 도구
하지만 타입 힌팅만으로는 부족하다. 경고를 무시하고 코드를 실행하면 런타임 에러가 발생한다. 이를 방지하려면 CI/CD 파이프라인에 mypy 같은 타입 체커를 통합해야 한다. mypy는 코드를 실행하지 않고도 타입 불일치를 감지하여 에러를 반환한다. 고객에게 배포되기 전에 CI 단계에서 오류를 잡는 것이 훨씬 낫다.
여기에 더해 Ruff라는 린터를 사용할 것을 권장했다. Ruff는 Rust로 작성되어 매우 빠르며, mypy가 놓칠 수 있는 로직 오류까지 잡아낸다. 예를 들어 += 연산을 지원하지 않는 타입에 대해 해당 연산을 수행하려 하면, mypy는 타입만 검사하지만 Ruff는 로직 자체의 모순을 지적한다.
UV: Conda의 느린 Resolve를 대체하다
강연자는 패키지 관리 도구에 대해서도 언급했다. pip를 사용하는 사람이 대다수였지만, conda를 사용하는 사람도 있었다. 강연자는 Conda의 Dependency Resolve가 얼마나 느린지 묻고, 청중들이 공감하자 “쓰레기통에 버리라”고 단호하게 말했다. 대신 UV라는 도구를 추천했다. UV는 Rust로 작성되어 pip보다 5배, conda보다 500배 이상 빠르다. 실제로 패키지 설치 시간이 35분에서 수 초로 단축되었다고 한다.
Python 성능 최적화의 실전
버전별 성능 향상
Python 자체도 버전이 올라가며 성능이 개선되고 있다. Python 3.11은 이전 버전 대비 상당한 성능 향상을 이루었고, Python 3.12는 버그 수정에 집중했지만, Python 3.13은 다시 5~6% 정도의 성능 개선을 달성했다. 심지어 GIL(Global Interpreter Lock)을 제거한 실험적 빌드도 제공되고 있다.
내부 메커니즘 이해의 중요성
하지만 Python을 빠르게 사용하려면 내부 동작 원리를 이해해야 한다. 강연자는 간단한 벤치마크를 보여주었다. 10만 개의 숫자를 담은 리스트를 합산하는 두 가지 방법을 비교했다. 첫 번째는 for 루프로 수동 합산, 두 번째는 내장 함수 sum() 사용이다. 청중에게 어느 쪽이 빠르냐고 물었을 때, 대다수가 sum()을 선택했고 정답이었다. sum()은 C로 구현되어 있고, 컬렉션의 특성을 이해하며, 캐싱과 핫스팟 최적화를 활용하기 때문에 약 4배 빠르다.
NumPy: 배열 연산의 게임 체인저
더 나아가 NumPy를 사용하면 성능이 극적으로 향상된다. 같은 10만 개 숫자 합산을 NumPy의 np.sum()으로 처리하면 불과 3초 만에 끝난다. 이는 내장 sum()보다 11배 빠르다. NumPy는 ND 배열(N-Dimensional Array)을 C/C++로 구현하여 동적 크기 조정이 없고, AVX 같은 SIMD 명령어를 활용해 병렬 연산을 수행한다. 또한 CPU 종류를 감지하여 최적화된 코드를 실행한다.
NumPy의 함정: “Array-like”의 오버헤드
하지만 NumPy의 편리한 API를 잘못 사용하면 오히려 성능이 떨어진다. NumPy는 “array-like” 객체를 받을 수 있다고 광고하며, Python의 일반 리스트도 이에 해당한다. 강연자는 내장 sum()과 일반 Python 리스트에 np.sum()을 적용한 경우를 비교했다. 청중에게 어느 쪽이 빠르냐고 물었을 때, 이번에는 내장 sum()이 정답이었다. 왜냐하면 NumPy는 Python 리스트를 받으면 이를 ND 배열로 복사하고, C 메모리로 옮긴 뒤 연산하고, 결과를 다시 Python 객체로 변환하는 오버헤드가 발생하기 때문이다.
교훈: 올바른 도구를 올바르게 사용하라
강연자는 몇 가지 교훈을 정리했다. 첫째, 이미 존재하는 라이브러리와 모듈을 활용하라. 둘째, 라이브러리의 내부 동작 원리를 이해하라. 셋째, 편리한 API를 남용하지 마라. Python은 비프로그래머도 쉽게 사용할 수 있도록 설계되었지만, 성능을 원한다면 그 편의성 뒤에 숨은 비용을 반드시 이해해야 한다.
Python과 C++를 연결하는 기술: 바인딩의 세계
왜 바인딩이 필요한가
이제 핵심 질문에 도달한다. 알고리즘 팀이 독점적인(Proprietary) 함수를 개발했고, 이를 Python에서 테스트해야 하지만 구현은 C++로 되어 있다면 어떻게 해야 할까? 강연자는 Python 인터프리터(CPython)가 본질적으로 C 프로그램이라는 점을 상기시켰다. CPython은 프로그램 상태를 유지하며 모든 것을 PyObject로 변환한다. 따라서 .so 파일(공유 라이브러리)을 만들어 Python에 연결하면, C++로 작성된 함수를 Python에서 호출할 수 있다.
CPython API: 순수 C 방식의 바인딩
가장 기초적인 방법은 Python.h 헤더를 사용하는 것이다. C++ 함수를 작성한 후, 이를 감싸는 래퍼 함수를 만든다. 이 래퍼는 PyObject*를 반환하고, self와 args를 받는다. args는 모든 인자를 하나의 튜플로 묶은 것이므로, PyArg_ParseTuple로 개별 인자를 추출한다. 그 다음 실제 C++ 함수를 호출하고, 결과를 PyFloat_FromDouble 같은 함수로 PyObject로 변환하여 반환한다.
이어서 PyMethodDef 배열을 정의해 함수 이름, 래퍼, 문서화 문자열을 등록하고, PyModuleDef로 모듈을 정의한다. 가장 중요한 함수는 PyInit_<modulename>이다. Python에서 import 할 때 이 함수가 호출되므로, 모듈 이름과 정확히 일치해야 한다. 마지막으로 setup.py에서 Extension을 정의하고 빌드한다.
하지만 강연자는 이 방법이 너무 많은 보일러플레이트 코드를 요구한다며, 단 하나의 함수를 바인딩하는데 이렇게 많은 코드를 작성하고 싶지 않다고 토로했다.
현대적 바인딩 도구들
다행히 더 나은 도구들이 존재한다. 강연자는 PyBind11과 Boost.Python을 언급했다. 이들은 오래된 라이브러리지만, C++ 코드를 작성하고 컴파일만 하면 Python에서 사용할 수 있도록 해준다. Cython은 Python과 유사한 문법으로 C/C++ 코드를 작성할 수 있는 풍부한 언어다. Python에서 C++를 호출하거나, 반대로 C++에서 Python을 호출하는 것도 가능하다. 마지막으로 cppyy는 동적 바인딩 라이브러리로, 가장 간단하고 빠르게 C++ 코드를 Python에서 실행할 수 있게 해준다.
성능 비교: 순수 Python vs CPython vs PyBind11
강연자가 보여준 벤치마크는 충격적이었다. 예제 함수를 순수 C++로 실행하면 4밀리초가 걸린다. 같은 함수를 순수 Python으로 실행하면 약 1초가 걸린다. 200배 이상 느린 것이다. CPython API로 바인딩한 경우도 1.1초로 거의 차이가 없다. 이는 당연한데, 바인딩 자체는 오버헤드가 거의 없고 Python 코드의 느림이 주요 원인이기 때문이다.
흥미로운 것은 PyBind11의 성능이다. PyBind11로 바인딩하면 약 5배 느리다. 청중 중 한 명이 인터프리터 시작 시간을 측정했는지 물었지만, 강연자는 그렇지 않다고 답했다. 오버헤드는 PyBind11의 설계 방식에서 비롯된다. PyBind11은 모든 데이터를 힙에 할당하고, PyObject와의 변환 과정에서 벡터의 할당/해제가 반복되며, 캐시 미스가 빈번하게 발생한다. CUDA 프로그래밍과 마찬가지로, 단일 연산을 위해 데이터를 PCI Express를 통해 GPU로 보내는 것은 비효율적이다.
성능 향상 전략
성능을 개선하려면 몇 가지 전략이 필요하다. 첫째, **배치 함수(Batch Functions)**를 만들어라. 많은 데이터를 한 번에 전달해 오버헤드를 줄이는 것이다. 둘째, 바인딩된 객체를 사용하라. 예를 들어 PyBind11의 py::array_t는 NumPy의 ND 배열과 이미 바인딩되어 있어 변환 비용이 없다. 셋째, 값 반환 시 스마트 포인터를 사용하라. Python은 가비지 컬렉션을 사용하므로, C++에서 반환한 포인터를 Python이 임의로 삭제할 수 있다. 메모리 손상을 방지하려면 std::shared_ptr로 소유권을 공유해야 한다.
강연자는 NumPy 배열을 직접 사용하는 C++ 코드 예제를 보여주었다. py::array_t를 받아 내부 포인터를 추출하고 순회하는 방식이다. 코드가 다소 복잡해 보이지만, 이 방법으로 PyBind11의 실행 시간을 1.6초로 단축할 수 있었다.
Cython: Python과 C++의 하이브리드 언어
Cython이란 무엇인가
Cython은 Python과 C를 섞어 놓은 듯한 언어로, 거의 Python처럼 보이지만 C/C++ 수준의 성능을 낸다. 가장 큰 장점은 C++에서 Python 코드를 실행할 수 있다는 점이다. 이는 인터럽트나 콜백이 필요한 경우 매우 유용하다. 강연자는 많은 사람들이 Cython 코드를 보고 겁먹는다고 했지만, 실제로는 그렇게 무섭지 않다고 강조했다.
C++ 클래스를 Cython으로 바인딩하기
예제로 가상 함수를 가진 C++ 추상 클래스와 이를 상속한 구체 클래스를 바인딩하는 과정을 보여주었다. Cython은 두 가지 파일 타입을 사용한다. .pxd 파일은 C++의 헤더 파일에 해당하며, 선언만 포함한다. cdef extern from "class.hpp"로 C++ 헤더를 가져오고, cdef cppclass로 C++ 클래스의 인터페이스를 정의한다.
.pyx 파일은 C++의 .cpp 파일에 해당하며, 실제 구현을 담는다. from mymodel cimport *로 .pxd의 선언을 가져온 후, cdef class로 Python에서 사용할 클래스를 정의한다. 이 클래스는 shared_ptr로 C++ 객체를 멤버로 가진다. 함수 호출 시 dereference(self.c_real.get())로 포인터를 역참조한다. 왜 역참조에 * 연산자를 쓰지 않냐는 질문에, 강연자는 Python에서 *는 이미 언패킹 연산자로 사용되기 때문이라고 설명했다.
CMake를 통한 자동 빌드
Cython 코드를 컴파일하려면 .pyx를 C++ 코드로 변환해야 한다. 강연자는 CMake를 사용해 이를 자동화하는 방법을 보여주었다. find_package(Python3)로 Python 라이브러리를 찾고, add_custom_command로 Cython 컴파일을 정의한다. 출력된 .cpp 파일을 add_library로 링크하되, PREFIX를 제거해야 한다는 점이 중요하다. Python은 모듈 이름과 파일 이름이 정확히 일치해야 하는데, CMake가 자동으로 lib 접두사를 붙이면 import가 실패한다.
C++에서 Python 코드 실행하기
반대 방향도 가능하다. 에어갭 환경(Air-gapped Environment)에서 Conan 같은 패키지 매니저를 사용할 수 없지만 Python은 설치되어 있다면, Python의 JSON 라이브러리를 C++에서 사용할 수 있다. cdef public으로 Python 함수를 C++에서 호출 가능하게 만들고, C++ 코드에서 Py_Initialize()로 인터프리터를 초기화한 후 해당 함수를 호출하면 된다. Python으로 JSON 파싱을 처리하면 C++에서 직접 구현하는 것보다 훨씬 간단하다.
예외 전파 문제
한 가지 주의할 점은 예외 전파가 자동으로 이루어지지 않는다는 것이다. Python 예외를 C++로, 혹은 그 반대로 전파하려면 명시적 처리가 필요하다. 간단한 해결책은 C++ 함수를 만들어 throw std::runtime_error를 발생시키고, 이를 Cython으로 바인딩한 후, Python에서 호출하는 것이다. 그러면 Python 코드에서 try-except로 잡을 수 있다.
GIL: Python의 고질적 문제와 해결책
GIL이란 무엇인가
GIL(Global Interpreter Lock)은 Python의 가장 악명 높은 제약이다. 멀티스레딩을 사용해도 한 번에 하나의 스레드만 Python 코드를 실행할 수 있어, 실질적으로 순차 실행이 된다. 이는 멀티코어를 활용할 수 없게 만든다. Python 3.13에서 GIL을 제거한 실험적 빌드가 나왔지만, 강연자는 버그가 많아 당분간은 GIL이 유지될 것이라고 예상했다.
CPython과 Cython에서 GIL 해제하기
CPython API에서는 Py_BEGIN_ALLOW_THREADS와 Py_END_ALLOW_THREADS 매크로로 GIL을 해제할 수 있다. Cython에서는 훨씬 간단하다. 함수 선언에 nogil만 추가하면 된다. 예를 들어 cdef void my_function() nogil로 선언하면, 이 함수는 GIL 없이 실행된다. 코드 블록 단위로도 가능하다. with nogil:로 블록을 감싸면, 그 안의 코드는 GIL 없이 병렬 실행된다.
주의사항: PyObject 접근 금지
GIL을 해제한 상태에서는 PyObject에 접근할 수 없다. Python 메모리와 상호작용하려면 GIL이 필요하기 때문이다. 따라서 nogil 블록 안에서는 순수 C/C++ 메모리에서만 작업해야 한다. 데이터를 미리 C++ 변수로 복사하고, 연산을 수행한 후, 다시 PyObject로 변환해야 한다.
cppyy: 동적 바인딩의 혁명
cppyy의 작동 원리
cppyy는 Cling이라는 C++ 인터프리터를 Python에 통합한 라이브러리다. Cling은 LLVM과 Clang을 기반으로 하며, 실시간으로 C++ 코드를 해석하고 실행한다. cppyy를 설치하면 Cling도 함께 설치되므로, 별도로 Clang을 설치할 필요가 없다.
간단한 사용 예제
사용법은 놀라울 정도로 간단하다. Python 코드에서 import cppyy를 하고, cppyy.cppdef에 C++ 함수를 문자열로 정의한다. 그 다음 cppyy.gbl.function_name으로 함수를 호출하면 끝이다. gbl은 전역 네임스페이스를 의미한다. 별도의 컴파일이나 바인딩 코드 작성 없이, 즉시 C++ 함수를 Python에서 실행할 수 있다.
벤치마크 결과도 인상적이다. 이전에 PyBind11이 5배 정도 느렸던 것과 달리, cppyy는 1초 이하로 성능을 끌어올렸다. 이는 동적 바인딩의 오버헤드를 극도로 최소화했기 때문이다.
템플릿과 상속 지원
cppyy는 C++의 고급 기능도 지원한다. 템플릿을 사용하는 예제에서, std::invoke를 활용해 함수 포인터와 가변 인자를 받는 템플릿 함수를 정의했다. 놀랍게도 Python 함수를 C++의 콜백으로 전달할 수 있다. 단, 타입 힌팅으로 템플릿 인스턴스화를 도와야 한다. call_it[int, int, float](python_func, 1, 2)처럼 명시하면, cppyy가 올바른 템플릿 인스턴스를 생성한다.
추상 클래스를 C++에서 정의하고, 이를 Python에서 상속받아 구체 클래스를 만드는 것도 가능하다. cppyy.include("header.hpp")로 헤더를 가져오고, from cppyy.gbl import AbstractClass로 클래스를 임포트한다. 그 다음 Python의 class로 이를 상속하면, Python에서 C++ 가상 함수를 오버라이드할 수 있다.
외부 라이브러리 사용
기존의 C++ 라이브러리를 사용하는 것도 간단하다. .so 파일이 있다면 cppyy.load_library("libfoo.so")로 로드하고, cppyy.include("foo.h")로 인터페이스를 가져온 후 함수를 호출하면 된다. Static Library(.a 파일)는 직접 로드할 수 없지만, 이를 래핑하는 더미 .so를 만들어 링크하면 된다. CMake에서 add_library로 빈 라이브러리를 만들고 .a를 링크하면 모든 심볼이 .so로 이동한다.
GIL 해제
cppyy에서도 GIL 해제가 가능하다. 함수 객체에 __release_gil__ = True 속성을 설정하면, 해당 함수는 GIL 없이 실행된다. 멀티스레딩 환경에서 이 설정만으로도 성능이 크게 향상된다.
한계: Bit Fields
모든 것이 완벽하지는 않다. cppyy의 알려진 한계 중 하나는 Bit Field를 지원하지 않는다는 것이다. C++ 구조체에서 비트 필드를 사용하면, Python에서 이를 출력할 때 값이 제대로 표시되지 않는다. 강연자는 cppyy의 제작자가 “이 날이 올 줄 알았다”고 농담했다는 이슈 트래커를 보여주었다. 해결책은 C++ 함수를 작성해 구조체를 바이트 문자열로 변환한 후 Python에서 출력하는 것이다.
C++26 Reflection을 활용한 자동 바인딩
Reflection의 등장
C++26에서 도입될 Reflection은 컴파일 타임에 타입 정보를 조회하고 조작할 수 있게 해준다. 이는 바인딩 코드를 자동 생성하는 데 혁명적이다. 강연자는 Reflection의 핵심 연산자를 소개했다.
^연산자: 타입이나 함수의 메타 정보를 반환한다.[: ... :]연산자: 메타 정보로부터 실제 인스턴스를 생성한다.template for: Constexpr 컨텍스트에서 반복문을 전개한다.
수동 바인딩의 문제
네임스페이스에 두 개의 함수가 있고, 이를 PyBind11로 바인딩한다고 가정하자. 모듈을 만들고, 서브모듈을 정의하고, 각 함수에 대해 .def()를 호출해야 한다. 이는 함수가 추가될 때마다 반복해야 하는 보일러플레이트다.
Reflection을 사용한 자동 바인딩
Reflection을 사용하면 이를 자동화할 수 있다. std::meta::members_of(^namespace)로 네임스페이스의 모든 멤버를 메타 정보 배열로 가져온다. template for (auto r : symbols)로 각 멤버를 순회하며, std::meta::is_function(r)로 함수인지 확인한다. 함수라면 std::meta::identifier_of(r)로 이름을 추출하고, m.def(name, [:r:])로 바인딩한다. [:r:]는 메타 정보를 실제 함수 포인터로 변환한다.
매개변수 이름까지 추출하기
PyBind11은 py::arg로 매개변수 이름을 명시할 수 있다. Reflection을 사용하면 이것도 자동화된다. std::meta::parameters_of(r)로 함수의 매개변수 메타 정보를 가져오고, 람다로 이를 문자열 배열로 변환한다. 그 다음 m.def에서 이 배열을 전개하여 매개변수 이름을 전달한다.
이 코드는 현재로서는 다소 복잡해 보이지만, Reflection이 표준으로 자리 잡으면 훨씬 간결해질 것이다. 무엇보다 중요한 것은, 함수를 추가할 때 바인딩 코드를 수정할 필요가 없다는 점이다. 컴파일 타임에 모든 것이 자동으로 생성된다.
마무리: Python과 C++의 공생
핵심 요약
강연자는 마지막으로 몇 가지 핵심 메시지를 전달했다. 첫째, 바인딩을 적극 활용하라. 때로는 Cython으로 Python 코드를 컴파일하는 것만으로도 성능이 향상된다. 둘째, API를 이해하고 남용하지 마라. 셋째, 배치 함수를 사용해 오버헤드를 줄여라. 넷째, Dangling Pointer를 방지하기 위해 스마트 포인터를 사용하라. 다섯째, Reflection을 주목하라. C++26의 Reflection은 바인딩 작업을 근본적으로 변화시킬 것이다.
질의응답: 디버깅과 버전 관리
청중으로부터 디버깅 경험에 대한 질문이 나왔다. 강연자는 PyBind11과 Boost.Python이 디버깅이 가장 쉽다고 답했다. Cython은 전용 디버거가 유료지만, 일반적으로는 프린트 디버깅으로 충분하다고 했다. cppyy는 오류 발생 시 Clang의 오류 메시지를 직접 보여주기 때문에 문제 파악이 비교적 쉽다.
또 다른 질문은 Python 버전마다 재컴파일해야 하는 문제였다. 정적 바인딩(PyBind11 등)은 Python 버전이 바뀌면 .so 파일을 다시 빌드해야 한다. 이는 Python의 심볼과 링크되기 때문이다. 강연자는 이 문제를 피하려면 cppyy를 사용하라고 권했다. cppyy는 동적으로 바인딩하므로 Python 버전에 무관하게 작동한다.
미래 전망
Python과 C++는 더 이상 별개의 언어가 아니다. 두 언어는 서로의 강점을 흡수하며 진화하고 있다. C++는 Python의 간결함과 사용 편의성을 받아들이고 있고, Python은 타입 안정성과 성능을 개선하고 있다. 바인딩 도구들은 점점 더 쉽고 강력해지고 있으며, Reflection 같은 새로운 기능은 두 언어의 경계를 더욱 허물 것이다.
최종적으로, C++ 개발자라면 Python을 배워야 하고, Python 개발자라면 C++를 이해해야 한다. 두 언어를 함께 사용할 때 비로소 진정한 생산성과 성능을 동시에 얻을 수 있다.
📚 출처 및 참고
| 구분 | 내용 |
|---|---|
| 원본 영상 | C++ ♥ Python - Alex Dathskovsky - CppCon 2025 |
| 채널 | CppCon |
| 발표자 | Alex Dathskovsky (17년+ 시스템 소프트웨어 개발 경험, CILADB 소속, C++ 표준화 위원회 참여) |
| 리포트 작성 | 2026-01-16 |
| 작성 방식 | YouTube 영상 STT 변환 후 에디팅 |
💬 본 리포트는 영상 내용을 바탕으로 재구성되었습니다. 코드 예제 및 벤치마크 수치 등 정확한 내용은 원본 영상을 참조해 주세요.