ROS 2 Jazzy C++ 패키지 만들기: rclcpp Publisher·Subscriber 실습
이번에는 같은 /chatter 통신을 C++로 구현합니다. ament_cmake 패키지, rclcpp::Node, 명시적 QoS, CMake 의존성, 설치 규칙을 함께 작성해 “빌드는 되는데 ros2 run이 못 찾는” 문제까지 예방합니다.
1. ament_cmake 패키지 생성
source /opt/ros/jazzy/setup.bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake \
--license Apache-2.0 \
--dependencies rclcpp std_msgs \
jazzy_cpp_pubsub
주요 파일은 다음과 같습니다.
jazzy_cpp_pubsub/
├── CMakeLists.txt
├── package.xml
├── include/jazzy_cpp_pubsub/
└── src/
rclcpp 예제를 작성할 때 C++17을 프로젝트 기본값으로 명시하면 컴파일러·CI 환경에 따른 표준 차이를 줄일 수 있습니다.2. Publisher 구현
src/publisher_node.cpp를 작성합니다.
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
using namespace std::chrono_literals;
class TextPublisher : public rclcpp::Node
{
public:
TextPublisher()
: Node("text_publisher"), count_(0)
{
const auto period = this->declare_parameter<double>(
"publish_period", 1.0);
const auto qos = rclcpp::QoS(rclcpp::KeepLast(10)).reliable();
publisher_ = this->create_publisher<std_msgs::msg::String>(
"chatter", qos);
timer_ = this->create_wall_timer(
std::chrono::duration<double>(period),
std::bind(&TextPublisher::publish_message, this));
RCLCPP_INFO(
this->get_logger(),
"text_publisher started: period=%.2fs", period);
}
private:
void publish_message()
{
std_msgs::msg::String message;
message.data = "Hello Jazzy: " + std::to_string(count_++);
publisher_->publish(message);
RCLCPP_INFO(this->get_logger(), "Published: %s", message.data.c_str());
}
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
std::size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<TextPublisher>());
rclcpp::shutdown();
return 0;
}
3. Subscriber 구현
src/subscriber_node.cpp를 작성합니다. callback 인자는 읽기 전용 ConstSharedPtr로 받습니다.
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
class TextSubscriber : public rclcpp::Node
{
public:
TextSubscriber()
: Node("text_subscriber")
{
const auto qos = rclcpp::QoS(rclcpp::KeepLast(10)).reliable();
subscription_ = this->create_subscription<std_msgs::msg::String>(
"chatter",
qos,
[this](std_msgs::msg::String::ConstSharedPtr message) {
RCLCPP_INFO(
this->get_logger(), "Received: %s", message->data.c_str());
});
RCLCPP_INFO(this->get_logger(), "text_subscriber started");
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<TextSubscriber>());
rclcpp::shutdown();
return 0;
}
메시지를 수정하지 않는 Subscriber라면 mutable pointer보다 const pointer가 의도를 정확히 표현합니다. 객체를 멤버 변수에 저장해 노드가 실행되는 동안 subscription 수명을 유지합니다.
4. CMakeLists.txt 설정
생성된 CMakeLists.txt를 아래 핵심 구성으로 정리합니다.
cmake_minimum_required(VERSION 3.8)
project(jazzy_cpp_pubsub)
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(publisher src/publisher_node.cpp)
ament_target_dependencies(publisher rclcpp std_msgs)
add_executable(subscriber src/subscriber_node.cpp)
ament_target_dependencies(subscriber rclcpp std_msgs)
install(TARGETS
publisher
subscriber
DESTINATION lib/${PROJECT_NAME}
)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
endif()
ament_package()
add_executable()만 작성하면 컴파일은 될 수 있지만 ros2 run이 실행 파일을 찾지 못합니다. 반드시 install(TARGETS ... DESTINATION lib/${PROJECT_NAME})가 필요합니다.5. package.xml 확인
ros2 pkg create --dependencies가 의존성을 추가하지만, 직접 import한 패키지가 manifest에도 있는지 확인합니다.
<?xml version="1.0"?>
<package format="3">
<name>jazzy_cpp_pubsub</name>
<version>0.0.0</version>
<description>ROS 2 Jazzy rclcpp pub/sub example</description>
<maintainer email="you@example.com">Your Name</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
6. 빌드와 실행
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src --ignore-src -y
colcon build --packages-select jazzy_cpp_pubsub \
--cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
ros2 pkg executables jazzy_cpp_pubsub
두 터미널에서 각각 실행합니다.
# 터미널 1
source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 run jazzy_cpp_pubsub publisher
# 터미널 2
source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 run jazzy_cpp_pubsub subscriber
Parameter override와 Topic 검증을 실행합니다.
ros2 run jazzy_cpp_pubsub publisher \
--ros-args -p publish_period:=0.25
ros2 topic info /chatter -v
ros2 topic hz /chatter
ros2 topic echo /chatter --once
7. rclcpp 코드의 핵심
| 코드 | 역할 |
|---|---|
rclcpp::init | ROS 인자 처리와 런타임 초기화 |
rclcpp::spin | executor가 callback을 실행하도록 대기 |
create_wall_timer | wall clock 기준으로 주기 callback 생성 |
RCLCPP_INFO | 노드 logger를 통한 구조화된 로그 출력 |
SharedPtr | publisher·subscription·timer 객체의 수명 관리 |
callback에서 오래 걸리는 계산을 하면 SingleThreadedExecutor의 다른 callback도 지연됩니다. 실제 시스템에서는 callback group과 MultiThreadedExecutor, worker thread, 별도 노드 중 어떤 경계를 사용할지 처리 시간과 공유 상태를 기준으로 정합니다.
8. 데이터에 맞는 QoS 사용
// 카메라, LiDAR처럼 최신 데이터가 중요한 경우
auto sensor_qos = rclcpp::SensorDataQoS();
// 명령, 상태처럼 신뢰성이 중요한 경우
auto command_qos = rclcpp::QoS(rclcpp::KeepLast(10)).reliable();
// 정적 지도처럼 late joiner가 마지막 값을 받아야 하는 경우
auto map_qos = rclcpp::QoS(rclcpp::KeepLast(1))
.reliable()
.transient_local();
| Publisher | Subscriber | 연결 |
|---|---|---|
| RELIABLE | RELIABLE | 가능 |
| RELIABLE | BEST_EFFORT | 가능 |
| BEST_EFFORT | BEST_EFFORT | 가능 |
| BEST_EFFORT | RELIABLE | 불가 |
9. 빌드·실행 오류 해결
| 증상 | 확인할 것 |
|---|---|
| 헤더를 찾지 못함 | find_package, ament_target_dependencies, package.xml 의존성 확인 |
| undefined reference | 필요 library가 target에 연결됐는지 확인 |
| 빌드 성공, 실행 파일 없음 | install(TARGETS ...)와 overlay source 확인 |
| 코드 수정이 반영 안 됨 | 노드를 종료하고 대상 패키지를 다시 빌드 |
| 메시지가 수신되지 않음 | Topic 타입·이름·domain·QoS를 ros2 topic info -v로 비교 |
# 상세 빌드 로그
colcon build --packages-select jazzy_cpp_pubsub \
--event-handlers console_direct+
# ROS 환경 점검
ros2 doctor --report
- C++17과 경고 옵션을 명시했다.
- 모든 ROS 의존성을 CMakeLists.txt와 package.xml에 선언했다.
- 실행 target을
lib/${PROJECT_NAME}에 설치한다. - callback 인자의 const 의도를 분명히 했다.
- 데이터 성격에 맞는 QoS를 명시했다.
마무리
rclcpp 패키지는 소스 코드뿐 아니라 CMake target, 의존성, install 규칙이 함께 완성되어야 합니다. 다음 편에서는 Topic·Service·Action을 기능별로 비교하고 인터페이스를 잘못 선택했을 때 생기는 설계 문제를 정리합니다.
