Python 호출 뒤에도 native 도구 계약이 있다
영상 처리나 추론 라이브러리는 Python에서 호출해도 내부에 C/C++로 빌드한 확장과 외부 라이브러리를 사용할 수 있다. native는 해당 실행 환경의 기계어로 번역된 코드라는 뜻으로 쓴다. 패키지 import 문제를 무조건 Python 소스 문법 문제로 취급하면 실제 실패 층위를 놓친다. C와 C++도 같은 빌드 모드가 아니며 확장을 만든 언어·컴파일러·표준 모드·외부 의존성을 구별해야 한다. 이 단위에서는 새 언어 문법을 배우는 대신 이미 만들어진 산출물과 소비자의 계약을 읽는다.
API는 호출자가 사용하는 함수·자료형·동작의 인터페이스다. ABI는 번역된 코드 사이의 호출·자료 배치·심벌 등 바이너리 연결에 관한 계약이다. 소스 수준의 함수 이름이 같다고 두 바이너리가 호환된다고 보장할 수 없다. 운영체제, CPU 아키텍처, Python 구현·버전, 컴파일 설정과 연계된 라이브러리 조건을 함께 살핀다. 패키지 버전 문자열 하나로 이 조건 전체가 대체되지는 않는다.
합성 manifest에서 실행은 CPython 3.12/aarch64인데 확장은 일반 CPython 3.11/x86_64용이라고 적혀 있다면 인터프리터 대상과 CPU 대상 둘 다 확인해야 한다. 확장 파일 이름을 바꾸어도 바이너리 계약은 변하지 않는다. Python의 Stable ABI 공식 설명은 Limited API라는 제한된 C API 범위를 사용한 경우 여러 Python 버전 사이 호환을 지원하는 경로를 설명한다. 모든 확장이 그 경로를 쓰는 것도, abi3 표시가 다른 운영체제·CPU·모든 native 의존성을 호환시키는 것도 아니다. 여기서는 Python 버전 차이만으로 모든 확장을 항상 불가능하다고 일반화하지 않는다.
Import 성공과 자료 계약 통과는 다르다
NumPy 2.0은 C API에 의존하는 배포 바이너리에 대해 호환성 변경을 알린다. 공식 migration guide를 확인하면 단순 Python 코드 수정과 native 확장 재빌드·호환 배포의 문제가 다름을 알 수 있다. 실제 해결에서는 해당 패키지가 지원하는 조합과 빌드 기록을 확인한다. 임의의 전체 업그레이드나 버전 내리기를 먼저 반복하면 원래 조합을 잃어 진단이 어려워진다.
오류 기록에는 실행 인터프리터, 확장의 실제 경로, CPU 대상과 의존성 버전, 원문 오류를 묶는다. 모듈을 못 찾음, 로더가 필요한 심벌을 못 찾음, import 뒤 잘못된 shape를 거부함은 서로 다른 지점이다. 첫 번째는 검색 경로·설치 범위를, 두 번째는 실제 로드된 바이너리와 의존성을, 세 번째는 입력 계약을 우선 조사하는 단서다. 문자열 하나로 최종 원인을 확정하지 않고 최소 재현 입력을 만든다. 빌드 단계 전체는 다음 단위에서 다룬다.
버퍼에는 주소 외에도 해석 정보가 필요하다
버퍼는 수치나 영상 원소를 담는 메모리 영역이며, 이를 보는 view는 같은 자료를 별도 복사 없이 해석하는 접근을 뜻한다. 주소만 전달하면 원소 크기·범위·축·단위를 알 수 없다. 소비자는 dtype, shape, stride, 읽기/쓰기 가능 여부와 보관 기간을 확인해야 한다. Python buffer protocol은 이 메타데이터와 버퍼 획득·해제의 계약을 제공한다. 이를 사용하지 않은 임의 native 주소 전달이 같은 보호를 자동으로 얻는 것은 아니다.
그림 M01.03-1. 각 칸은 서로 다른 역할/근거다. 아래 방향은 자료/처리의 연결이며 실제 성공을 자동으로 보장하지 않는다. 직접 제작한 개념도이며 본문의 조건과 함께 읽는다.
예를 들어 CPU의 C 순서 연속 float32 배열이 shape (2,3), 행=시간/열=x,y,z이고 단위 m라면 원소 크기는 4 byte다. NumPy/Python buffer의 byte stride는 (12,4)다. 인덱스는 0부터 세며 원소 [1,2]는 시작에서 1×12+2×4=20 byte 떨어져 있고 4 byte를 읽는다. 전체 논리 원소 자료량은 2×3×4=24 byte다. [2,0]은 첫 축 범위 밖이므로 계산한 주소처럼 보이는 숫자가 있어도 허용된 접근이 아니다.
같은 배열의 전치 view는 shape (3,2), byte stride (4,12)가 될 수 있다. 원소를 복사하지 않았어도 이제 C 순서 연속 (3,2) 배열의 stride (8,4)와 다르다. 소비자가 그 연속 배치만 지원한다면 적절한 변환·복사가 필요하거나 입력을 거부해야 한다. 원소 수 6만 확인해 연속 포인터처럼 읽으면 서로 다른 위치의 값을 사용할 수 있다. PyTorch tensor.stride는 원소 수 단위이므로 같은 표의 숫자를 byte로 넘기지 않는다.
소유자의 생존과 표본의 보존을 구분한다
소유자는 메모리를 언제 할당·해제할지 책임지는 주체다. 비소유 view를 받았다고 그 책임이 자동 이전되지는 않는다. binding은 복사·소유권 이전·참조 유지 정책을 정해야 한다. pybind11 반환 정책은 잘못된 소유권 해석이 이중 해제나 이미 해제된 객체 접근으로 이어질 수 있음을 설명한다. 이 단위는 그런 위험 동작을 실행해 보도록 요구하지 않는다.
해제 전이라도 표본 의미가 보존되지 않을 수 있다. 생산자가 소유한 버퍼에 frame 17의 [0.1,0.2] m가 있고 소비자가 view를 큐에 넣었다고 하자. 소비 전에 생산자가 같은 버퍼를 frame 18의 [0.8,0.9] m로 덮으면 주소는 유효해도 소비자는 frame 17의 값을 읽지 못한다. frame ID만 17로 남아 있으면 다른 표본을 잘못 붙인 기록이 된다. 생존 보장은 데이터 불변이나 동시 접근의 일관성 보장과 다르다.
해결은 계약에 따라 선택한다. 생산자가 쓰지 않는 동안 완성된 snapshot을 복사하고 소비가 끝날 때까지 그 복사본을 소유하게 할 수 있다. 또는 버퍼 대여 기간을 정해 소비 완료 전 재사용·해제를 막고 반환 신호를 받은 뒤 다음 표본에 쓸 수 있다. 둘 다 오류·취소 경로의 반환 책임까지 필요하다. 비동기 작업을 제출한 함수가 돌아온 시점이 실제 읽기 완료라는 보장도 없다. GPU를 포함한 장치 작업에서는 API의 완료·동기화 계약까지 확인해야 하며, 여기서는 실제 GPU 작업을 수행하지 않는다.
zero-copy는 특정 경계의 자료 복사를 피한다는 설계 속성이다. 수명 연장·불변 표본·스레드 안전·실시간 기한을 뜻하지 않는다. 먼저 정확한 표본과 보관 기간을 정한 뒤 복사 비용을 측정한다. 작은 안전한 복사 하나를 없애려다 원본 재사용의 시점 계약을 잃으면 같은 프로그램이 간헐적으로 다른 값을 쓰는 원인을 만들 수 있다.