# rclcpp와 rclpy 혼합 사용 시 주의사항

#### 혼합 아키텍처 개요

ROS2 Humble에서는 C++ 기반 클라이언트 라이브러리인 rclcpp와 Python 기반 클라이언트 라이브러리인 rclpy를 동시에 활용하는 상황이 빈번히 발생한다. 예를 들어, 고성능 처리가 필요한 센서 데이터 파이프라인은 C++(rclcpp)로 작성하고, 사용자 인터페이스나 빠른 프로토타이핑을 위해서는 Python(rclpy)을 사용하는 식이다. 그러나 두 라이브러리가 내부적으로 사용하는 실행 모델이 다르기 때문에 특정한 상황에서 충돌, 메모리 누수, 콜백 순서 꼬임 등이 발생할 수 있다. 이러한 문제를 미연에 방지하고 올바른 혼합 아키텍처를 구성하려면 다음 사항들을 숙지해야 한다.

#### 커뮤니케이션 레이어 차이점

rclcpp와 rclpy 모두 DDS(Domain Participant)와 연결되는 rcl 레이어를 활용한다. 그러나 C++ 쪽과 Python 쪽의 러닝타임 환경이 다르므로, 노드 인스턴스 생성 방식부터 서로 다른 관리가 필요하다.

* **C++(rclcpp) 노드**: C++ 노드는 주로 `std::shared_ptr` 등의 스마트 포인터를 통해 노드를 관리하며, 노드를 생성할 때 `rclcpp::NodeOptions` 등을 활용해 QoS나 초기화 파라미터를 세팅한다. C++ 실행 컨텍스트(Context)는 `rclcpp::init()` -> 노드 생성 -> `rclcpp::spin() 혹은 멀티스레딩 실행`의 과정을 거친다.
* **Python(rclpy) 노드**: Python 노드는 `rclpy.init()` 후 `rclpy.create_node()`를 통해 생성하고, `rclpy.spin()` 함수가 blocking 콜을 통해 이벤트를 처리한다. Python GIL(Global Interpreter Lock)이 존재하므로 멀티스레드 처리가 복잡하다.

따라서, 한 프로세스 내에서 C++ 노드와 Python 노드를 따로 두고자 한다면, **rclcpp::init**과 **rclpy.init** 간에 충돌이 생길 여지가 없는지 먼저 확인해야 한다. 일반적으로 하나의 프로세스 안에서 각각 따로 init하는 것은 권장되지 않는다. 만약 같은 프로세스에서 단일 DDS 레이어를 공유해야 한다면, 노드를 확실히 분리하거나 별도 바인딩 계층을 통해 접근하도록 설계해야 한다.

#### 콜백 스핀 처리: Python vs C++

C++ 노드는 기본적으로 `rclcpp::spin()` 또는 `rclcpp::executors::SingleThreadedExecutor` 혹은 `MultiThreadedExecutor`로 콜백을 수행한다. 반면 Python에서는 `rclpy.spin()`이 단일 스레드로 콜백을 순서대로 처리한다. 따라서 다음과 같은 경우 문제가 발생하기 쉽다.

* Python 콜백에서 C++ 함수를 직접 호출하는 구조: Python 측에서 콜백이 도착했을 때 C++ 쪽에 작업을 요청하면, C++ 스레드는 이미 다른 콜백을 처리 중일 수 있다.
* C++ 노드에서 메시지를 publish 후 곧바로 Python 콜백 결과를 참조하는 구조: Python 쪽에서 콜백 처리가 지연되면, C++ 측에서는 의도와 다른 시점에 데이터를 읽어옴으로써 동기화가 어긋날 수 있다.

다만 Python rclpy도 다중 스레딩 처리가 가능하도록 `MultiThreadedExecutor`를 지원하기는 하지만, GIL 문제가 있기 때문에 C++만큼 원활한 병렬 처리가 되지 않는다. 이러한 차이를 이해하고 메시지 통신 설계를 해야 한다.

#### 실행 모델 주의사항

C++과 Python 코드를 단일 프로세스에서 구동할 때는 다음 사항을 유의해야 한다.

* **Executor 분리** C++에서 제공하는 `rclcpp::Executor`와 Python의 `rclpy.executor.Executor`는 서로 다른 실행 주체다. 한 프로세스 내에서 혼합 구동 시 의도하지 않은 간섭이 발생할 수 있으므로, 일반적으로는 한 프로세스에서는 한 쪽 언어의 Executor만 구동하는 것이 바람직하다.
* **런타임 초기화 순서** 일반적으로 `rclcpp::init()`이 먼저 호출된 뒤 `rclpy.init()`을 하면 DDS 레이어가 꼬일 수 있다는 보고가 있다. 반대 순서로 해도 문제가 발생할 수 있다. 따라서 혼합 사용 시에는 보통 한 언어에서만 ROS2 환경을 초기화하고, 다른 언어는 이를 참조하는 방식으로 동작시키는 전략을 많이 쓴다.
* **Dynamic Loading** Python에서 C++ 함수를 확장 모듈 형태로 import해 사용하는 경우나, C++에서 Python 해석기를 임베디드하는 경우가 있다. 이런 경우에는 각 언어의 글로벌/정적 리소스(예: 글로벌 변수, DDS Participant)에 대한 생명주기 관리가 매우 엄격하게 이뤄져야 한다.

#### 메모리 관리 이슈

C++은 RAII(Resource Acquisition Is Initialization) 기법을 활용하여 객체의 생명주기를 스마트 포인터로 관리한다. 반면 Python은 가비지 컬렉션과 참조 횟수(Reference Counting)를 기반으로 메모리를 관리한다. 이로 인해 다음과 같은 상황이 벌어질 수 있다.

* **유효하지 않은 포인터 참조** Python 객체가 C++로 전달된 후, Python 쪽에서 해당 객체에 대한 참조가 해제되어 메모리가 정리되었음에도 불구하고 C++ 코드에서 포인터를 그대로 참조하는 경우가 발생할 수 있다.
* **ROS 메시지 변환 시 메모리 누수** Python 메시지 타입과 C++ 메시지 타입을 상호 변환하는 과정에서, 임시로 생성한 메모리를 해제하지 않는다면 누수가 발생한다. 예를 들어 Python에서 생성한 NumPy 배열을 C++ 쪽 포인터로 변환하여 사용 후 올바르게 free되지 않는 케이스 등이 있다.
* **스마트 포인터 vs Python 객체 간 순환 참조** C++의 `std::shared_ptr`가 Python 객체를 잡고, 동시에 Python 쪽에서도 C++ 객체를 참조하는 식으로 순환 참조가 생길 수 있다. 이 때 양쪽에서 참조 횟수가 0이 되지 않아 메모리가 해제되지 않는 문제가 발생한다.

#### 성능 이슈와 파이썬 GIL

Python의 GIL(Global Interpreter Lock)은 하나의 Python 해석기가 동시에 하나의 스레드만 Python 바이트코드를 실행하도록 제한한다. 그래서 C++와 Python이 동시에 콜백을 처리해야 하는 경우, 실제로는 C++ 스레드가 Python 측 콜백을 기다려야 하거나, Python 스레드는 GIL 획득을 위해 대기해야 하는 상황이 생긴다.

* **동시성 저하** C++에서 콜백 스레드가 많은 경우 성능이 우수하지만, Python GIL에 의해 병렬성이 제한되는 부분이 병목이 될 수 있다.
* **멀티프로세스 구성 권장** 이러한 성능 제약 때문에, 고성능 처리가 필요한 C++ 부분과 Python 부분을 서로 다른 프로세스로 구성하고 노드 간 메시지 통신을 통해 연계하는 방식이 더 권장된다. 이때, 각 프로세스에서 독립적으로 `rclcpp::init()`과 `rclpy.init()`을 해도 DDS 레이어가 충돌하지 않는다.

#### 특정 문제 사례 1: Init 충돌 에러

C++ 코드에서 이미 `rclcpp::init(argc, argv)`를 호출한 뒤, Python 스크립트로 넘어가서 `rclpy.init(args=None)`을 다시 호출하면, 내부적으로 DDS Participant가 재초기화되는 상황이 발생할 수 있다. 보통은 DDS Implementation이 중복 인스턴스 생성을 허용하지 않거나, 예상치 못한 Participant ID를 부여해버려서 에러 혹은 네임스페이스 충돌이 생긴다.

에러 예시 (Python 쪽):

```
RuntimeError: Failed to create node because the underlying implementation returned an error: participant already exists
```

에러 예시 (C++ 쪽):

```
rclcpp::exceptions::RCLError: Failed to create node: participant already exists
```

이러한 에러가 발생하는 주된 원인은 한 프로세스 내에서 서로 다른 계층에서 ROS2 환경을 중복으로 초기화하기 때문이다. 특히 Python 스크립트에서 C++ 라이브러리를 import해 쓸 때, `rclcpp::init()`이 암묵적으로 동작한 뒤 다시 `rclpy.init()`이 호출되는 순서 문제가 흔하게 발생한다.

해결 방안

* 한 언어(주로 Python)에서만 init을 수행하고, C++ 코드는 노드 구성만 담당하도록 설계한다.
* 만약 C++과 Python 각각의 init을 모두 호출해야 하는 구조라면, 독립된 프로세스로 분리해서 실행하거나, 별도의 DDS Participant를 사용하도록 명시적 QoS 설정을 달리한다.

#### 특정 문제 사례 2: Executor 충돌

C++과 Python 모두에서 `spin()`을 병렬로 돌리면, 콜백 처리 루프가 서로 경쟁하는 문제가 생길 수 있다.

* Python `rclpy.spin()`은 기본적으로 단일 스레드에서 콜백을 실행한다.
* C++ `rclcpp::spin()`은 Executor 종류에 따라 여러 스레드를 활용할 수 있다.

하나의 노드 인스턴스를 공유하고 있을 때, Python과 C++이 동시에 콜백을 빼앗아가는 형태가 발생한다면, 콜백 큐 상태가 꼬일 가능성이 크다.

* **디버깅 포인트**
  * C++ 콜백 안에서 Python 메서드를 호출하면, Python 쪽에서 콜백을 받을 때 GIL이 잠겨 있어서 교착 상태(Deadlock)가 생길 수 있다.
  * Python 콜백에서 C++ 측 노드에 대한 API를 호출할 때, 이미 C++이 spin 중인 상태라면 재진입 불가 상황에 빠질 수 있다.
* **주의 사항**
  * 한 노드에 대해 다중 언어 Executor를 동시에 사용하지 않는다.
  * 필요한 경우 C++ 노드와 Python 노드를 아예 분리해 Executor를 각각 운영한다.

#### 특정 문제 사례 3: 메시지 타입 변환 시 참조 이상

Python에서 `geometry_msgs.msg` 같은 메시지 객체를 생성하고, C++로 넘겨서 처리하는 경우, 다음과 같은 단계에서 문제가 발생할 수 있다.

1. Python 노드에서 메시지 생성: `msg = SomeMsg()`
2. C++로 메시지 포인터 전달: 예를 들어, `pybind11`을 통해 `msg` 객체 포인터를 C++ 함수로 넘김
3. C++ 함수 내부에서 메시지 사용 후, 별도의 스마트 포인터로 관리
4. Python 가비지 컬렉터가 `msg` 참조를 회수하여 메모리를 해제
5. C++에서 해제된 메시지 주소에 접근해 에러 발생

* 예방법
  * Python에서 C++로 객체를 넘길 때, Python 측에서 해제되지 않도록 참조를 유지해야 한다.
  * C++ 측에서 메시지를 복제(deep copy)하여 자체 관리하도록 하는 편이 안전하다.

#### 특정 문제 사례 4: Python 임포트 에러와 ROS2 워크스페이스

C++ 기반 패키지와 Python 기반 패키지를 하나의 워크스페이스에서 빌드할 때, Python 패키지가 설치되는 경로(`install/lib/pythonX.Y/site-packages`)와 C++ 라이브러리가 설치되는 경로(`install/lib`)가 서로 다르다. 아래와 같은 상황이 생길 수 있다.

Python 스크립트에서 C++로 만든 ROS2 메시지 타입이나 서비스를 임포트하려고 하는데, `PYTHONPATH`가 제대로 설정되지 않아 ImportError가 발생한다.

C++ 노드에서 Python 노드를 임베디드 실행하려고 할 때, ROS2 패키지 경로(`ROS_PACKAGE_PATH`, `AMENT_PREFIX_PATH` 등)가 누락되어 모듈을 찾지 못한다.

**에러 예시**

```
ModuleNotFoundError: No module named 'my_msgs'
```

또는

```
ImportError: cannot import name 'MyService' from partially initialized module 'my_pkg.srv' ...
```

**해결 방법**

* 빌드 후 `source install/setup.bash`를 반드시 수행하여, Python 모듈 패스와 C++ 라이브러리 패스가 환경 변수에 반영되도록 한다.
* C++에서 Python을 임베디드하는 경우, `Py_SetProgramName()` 및 `Py_SetPath()`와 같은 API를 통해 명시적으로 Python 환경을 설정한다.

#### 디버깅 기법

rclcpp와 rclpy를 동시에 활용하는 환경에서 문제를 디버깅할 때는 다음 기법들을 활용한다.

* **ROS2 로깅**
  * C++에서는 `RCLCPP_INFO`, Python에서는 `rclpy.logging.get_logger().info()` 등의 방식을 통해 동일한 레벨의 로그를 찍어 시점이나 이벤트 흐름을 분석한다.
* **DDS 로깅 및 Wireshark 분석**
  * 문제 원인이 네트워크 트래픽이나 DDS Discovery 단에서 발생하는 경우, Wireshark나 Fast-DDS Monitor 등의 툴을 활용한다.
* **Python GDB 디버깅**
  * Python에서 C++ 확장 모듈을 사용할 때, 세그멘테이션 폴트(SEGFAULT)가 발생한다면, gdb를 통해 Python 프로세스를 디버깅하면서 C++ 스택 트레이스를 확인할 수 있다.
* **pybind11, ctypes, ROS2 C API 등 확인**
  * Python에서 C++ 코드를 호출하기 위해 사용하는 라이브러리에 따라 자료형 변환 규칙이 달라진다. 각 라이브러리별 디버깅 모드를 활성화하여 내부 함수 호출 순서를 추적할 수 있다.

#### 멀티스레딩 및 Executor 고급 구성

앞서 살펴본 문제 중 다수는 멀티스레드 환경에서 C++과 Python 사이의 실행 흐름이 꼬이거나, 하나의 Executor에 여러 스레드가 엉켜 버리는 경우에서 기인한다. 이를 방지하거나 최소화하기 위한 고급 구성 기법이 존재한다.

* **단일 Executor, 다중 Node**
  * 한 프로세스 내에서 여러 개의 rclcpp 노드와 rclpy 노드를 동시에 구성해야 한다면, 하나의 Executor만 운영하고 모든 노드를 그 Executor에 할당하는 방법을 고려할 수 있다.
  * 이 경우, Python 노드는 C++ 측 Executor에 의해 콜백이 호출되는 구조가 되는데, 내부적으로 Python 콜백에 들어갈 때 GIL을 획득해야 하므로 성능이 저하될 수 있다.
  * 또한 rclpy 자체가 제공하는 `spin()` 함수를 사용하지 못하고, rclcpp Executor가 Python 콜백을 어떻게 호출할지 별도 핸들러가 필요하므로 구현 난이도가 올라간다.
* **다중 Executor, 노드 분리**
  * C++ 노드는 C++ Executor에서 돌리고, Python 노드는 Python Executor에서 각각 돌리는 구조다.
  * 노드 간 메시지 통신은 DDS를 통해 이뤄지므로, 같은 프로세스라도 서로 다른 Executor에서 잘 주고받을 수 있다.
  * 단, 한 프로세스 안에서 Executor가 2개 이상 구동될 때 DDS 리소스가 중복 초기화되지 않도록, 한 언어 쪽에서만 `init()`을 담당하도록 설계하거나, DDS 소켓 바인딩에 충돌이 없도록 주의해야 한다.
* **멀티스레드 vs 멀티프로세스**
  * ROS2는 멀티프로세스 아키텍처를 통한 확장성을 권장하는 편이다. 멀티스레드는 메모리를 공유하여 오버헤드가 적은 이점이 있지만, Python GIL로 인해 예상만큼의 병렬성을 얻기 어렵다.
  * 고성능 처리가 필요한 C++ 코드를 별도 프로세스로 분리하고, Python 프로세스와는 토픽이나 서비스, 액션 등을 통해 통신하는 방식이 훨씬 안정적이다.

#### QoS 세팅 충돌

rclcpp와 rclpy 모두 QoS(품질 서비스)를 설정할 수 있으나, 언어 간 API 구조가 약간씩 다르다. 특히 다음과 같은 이슈들이 발생하기 쉽다.

* **RMW 별 이름 규칙**
  * ROS2에서 사용되는 RMW(예: Fast-DDS, Cyclone DDS 등)에 따라 Topic Discovery 규칙이 달라질 수 있다.
  * Python에서 만든 토픽 이름과 C++에서 만든 토픽 이름이 전혀 같아도, QoS 설정이 안 맞으면 한쪽에서 Subscribing이 되지 않을 수 있다.
* **Reliability, Durability 불일치**
  * Python 노드가 `ReliabilityPolicy.RELIABLE`, C++ 노드가 `rmw_qos_profile_default`(BEST\_EFFORT)로 설정되면 데이터 손실 또는 Sub/Pub 실패가 일어날 수 있다.
  * Durability 설정(`TransientLocal`, `Volatile` 등) 또한 언어별 default 값이 다를 수 있으므로, 명시적으로 통일해야 한다.
* **해결 가이드**
  * Python 메시지 생성 시 `qos_profile`을 지정하고, C++ 측에서도 `rclcpp::QoS`를 통해 동일한 설정을 선언한다.
  * 언어 간 통일된 QoS 설정을 위해 `.yaml` 형태의 파라미터 파일(ROS2 Launch에서 사용 가능)을 만들어서 두 노드가 공통된 QoS를 참조하도록 하는 방법도 있다.

#### Launch 시스템 활용

ROS2 Launch 시스템에서 C++ 노드와 Python 노드를 한꺼번에 띄울 수 있다. 이때 다음 사항을 유의한다.

* **Python Launch 파일 예시**

  ```python
  from launch import LaunchDescription
  from launch_ros.actions import Node

  def generate_launch_description():
      return LaunchDescription([
          Node(
              package='my_cpp_package',
              executable='my_cpp_node_exe',
              name='cpp_node'
          ),
          Node(
              package='my_py_package',
              executable='my_py_node',
              name='py_node'
          )
      ])
  ```

  위와 같이 한 Launch 파일에서 C++ 노드와 Python 노드를 동시에 띄우면, 프로세스는 서로 다른 두 개가 실행된다. 즉, C++ 프로세스와 Python 프로세스가 분리되어서 rclcpp와 rclpy 각각 독립적으로 init을 진행한다.
* **하나의 프로세스 안에서 런치?**
  * 이론상 하나의 프로세스에서 C++과 Python 코드를 통합할 수도 있지만, Launch 시스템 자체가 노드를 별도 프로세스로 실행하는 방식을 기본으로 삼는다.
  * 여러 이유로(성능, 안정성, GIL 문제 등) 보통은 멀티프로세스 구조가 훨씬 낫다.

#### 단위 테스트와 CI

혼합 사용 시 단위 테스트와 CI(Continuous Integration) 환경 구축도 까다로울 수 있다.

* **Python pytest + C++ gtest 병행**
  * Python 노드 기능 테스트는 `pytest`를 통해 진행하고, C++ 노드는 `gtest` 프레임워크를 통해 테스트케이스를 작성한다.
  * 두 테스트를 동시에 수행해야 한다면, ROS2 Launch Testing 기능을 이용해 통합적으로 E2E 테스트를 구성할 수 있다.
* **실시간 통신 테스트**
  * C++에서 Publish하고 Python에서 Subscribe하는 흐름이 제대로 동작하는지, 반대로 Python에서 Publish하는 데이터를 C++에서 원하는 시점에 받을 수 있는지도 검증해야 한다.
  * 테스트 시 의도치 않은 QoS 불일치를 발견할 가능성이 많으므로, 명확히 QoS를 표시하고 로그를 남겨 두는 습관이 중요하다.

#### 혼합 노드 컴포지션?

ROS2의 node composition(동적 컴포넌트 로드) 기능은 주로 C++에서 `rclcpp_components`를 이용해 구현된다. Python 노드는 기본적으로 composition 기능을 지원하지 않는다. 따라서 C++ 컴포넌트와 Python 코드를 동일 컴포지션 안에 엮어서 사용하기는 현실적으로 어렵다.

* 권장 패턴
  * C++ 컴포넌트끼리는 한 프로세스 안에서 composition을 통해 구성
  * Python 노드는 별도의 프로세스 혹은 launch 파일에서 독립적으로 구동
  * 두 쪽을 Topic/Service/Action 등을 통해 연계
