이번 실습에서는 ROS 2 Humble 환경에서 C++로 서비스 서버와 서비스 클라이언트를 작성합니다.
패키지는 이미 생성되어 있다고 가정합니다.
사용할 패키지 이름은 다음과 같습니다.
my_cpp_package
인터페이스 정의는 다음 패키지를 사용합니다.
my_first_package_msgs
이번 예제에서는 모바일 로봇 제어에서 자주 필요한 구조를 단순화하여, 두 개의 숫자와 명령 문자열을 서버에 보내고 결과를 응답받는 형태로 실습합니다.
예제 자체는 계산기 형태이지만, 실제 모바일 로봇에서는 다음과 같은 구조로 확장할 수 있습니다.
속도 제한값 변경
이동 거리 계산
배터리 상태 확인
센서 초기화
오도메트리 리셋
모터 제어 파라미터 변경
이번 목표는 다음과 같습니다.
C++로 서비스 서버와 서비스 클라이언트를 만들고,
클라이언트가 두 숫자와 연산 명령을 보내면
서버가 계산 결과를 응답하도록 만들기
이번 예제에서는 계산 요청을 처리하는 서비스 서버와 클라이언트를 만듭니다.
구조는 다음과 같습니다.
math_client 노드
└── 두 숫자와 연산자를 서비스 서버에 요청
math_server 노드
└── 요청을 받아 계산한 뒤 결과를 응답
서비스 이름은 다음과 같이 사용합니다.
/calculate_two_numbers
서비스 요청에는 다음 값이 들어갑니다.
x
y
operator
서비스 응답에는 다음 값이 들어갑니다.
result
success
message
예를 들어 클라이언트가 다음과 같이 요청합니다.
x = 12.0
y = 3.0
operator = "divide"
서버는 다음과 같이 응답합니다.
result = 4.0
success = true
message = "calculation completed"
0으로 나누는 잘못된 요청이 들어오면 다음과 같이 응답합니다.
result = 0.0
success = false
message = "division by zero is not allowed"
단순히 계산값만 보내는 것보다 success와 message를 같이 보내는 방식이 좋습니다.
모바일 로봇 제어에서도 명령이 성공했는지, 실패했다면 왜 실패했는지를 같이 전달해야 디버깅이 쉽습니다.
2. 작업할 패키지 구조 확인하기
이번 실습은 이미 생성된 my_cpp_package 패키지에서 진행합니다.
워크스페이스 구조는 다음과 같다고 가정합니다.
ros2_ws/
└── src/
├── my_cpp_package/
└── my_first_package_msgs/
패키지가 이미 있으므로 ros2 pkg create 명령은 실행하지 않습니다.
작업할 패키지 폴더로 이동합니다.
cd ~/ros2_ws/src/my_cpp_package

최종 구조는 다음과 비슷해집니다.
my_cpp_package/
├── CMakeLists.txt
├── package.xml
└── src/
├── math_server.cpp
└── math_client.cpp
이번 실습에서는 my_cpp_package 안에 srv 폴더를 만들지 않습니다.
인터페이스는 다음 패키지에 이미 정의되어 있다고 가정합니다.
my_first_package_msgs/
└── srv/
└── CalculateTwoNumbers.srv
인터페이스 내용은 다음 구조여야 합니다.
float64 x
float64 y
string arithmetic_operator
---
float64 result
bool success
string message
3. 서비스 서버 코드 작성하기
다음 파일을 만듭니다.
touch src/math_server.cpp
또는 VS Code를 사용한다면 다음처럼 열어도 됩니다.
code src/math_server.cpp
내용은 다음과 같이 작성합니다.
#include <memory>
#include <string>
#include <functional>
#include "rclcpp/rclcpp.hpp"
#include "my_first_package_msgs/srv/calculate_two_numbers.hpp"
class MathServer : public rclcpp::Node
{
public:
using CalculateTwoNumbers = my_first_package_msgs::srv::CalculateTwoNumbers;
MathServer()
: Node("math_server")
{
service_ = this->create_service<CalculateTwoNumbers>(
"calculate_two_numbers",
std::bind(
&MathServer::handle_request,
this,
std::placeholders::_1,
std::placeholders::_2));
RCLCPP_INFO(this->get_logger(), "Math service server is ready.");
}
private:
void handle_request(
const std::shared_ptr<CalculateTwoNumbers::Request> request,
std::shared_ptr<CalculateTwoNumbers::Response> response)
{
const double x = request->x;
const double y = request->y;
const std::string op = request->arithmetic_operator;
response->success = true;
response->message = "calculation completed";
if (op == "add") {
response->result = x + y;
} else if (op == "subtract") {
response->result = x - y;
} else if (op == "multiply") {
response->result = x * y;
} else if (op == "divide") {
if (y == 0.0) {
response->result = 0.0;
response->success = false;
response->message = "division by zero is not allowed";
} else {
response->result = x / y;
}
} else {
response->result = 0.0;
response->success = false;
response->message = "unknown operator";
}
RCLCPP_INFO(
this->get_logger(),
"Request: %.2f %s %.2f -> Result: %.2f, Success: %s",
x,
op.c_str(),
y,
response->result,
response->success ? "true" : "false");
}
rclcpp::Service<CalculateTwoNumbers>::SharedPtr service_;
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MathServer>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}

4. 서버 코드 분석
1) 헤더 파일 포함
#include <memory>
#include <string>
#include <functional>
C++ 표준 라이브러리 헤더입니다.
memory는 std::shared_ptr를 사용하기 위해 필요합니다.
ROS 2에서는 요청과 응답 객체를 스마트 포인터로 다루는 경우가 많습니다.
string은 문자열 처리를 위해 사용됩니다.
이 코드에서는 연산자 값을 저장하는 std::string op에 사용됩니다.
functional은 std::bind를 사용하기 위해 필요합니다.
클래스 내부의 멤버 함수인 handle_request()를 서비스 콜백 함수로 연결할 때 사용됩니다.
2) ROS 2 및 서비스 메시지 헤더
#include "rclcpp/rclcpp.hpp"
#include "my_first_package_msgs/srv/calculate_two_numbers.hpp"
rclcpp/rclcpp.hpp는 ROS 2 C++ 노드를 만들기 위한 기본 헤더입니다.
노드 생성, 서비스 생성, 로그 출력, 노드 실행 등에 사용됩니다.
calculate_two_numbers.hpp는 사용자가 정의한 서비스 파일에서 자동 생성된 헤더입니다.
이 서비스는 대략 다음과 같은 구조를 가진다고 볼 수 있습니다.
float64 x
float64 y
string arithmetic_operator
---
float64 result
bool success
string message
요청에는 계산에 필요한 값이 들어가고, 응답에는 계산 결과와 성공 여부가 들어갑니다.
3) MathServer 클래스 선언
class MathServer : public rclcpp::Node
MathServer는 ROS 2 노드를 클래스로 만든 것입니다.
rclcpp::Node를 상속받기 때문에 이 클래스는 ROS 2 노드처럼 동작할 수 있습니다.
이 클래스 안에는 다음 기능이 들어 있습니다.
- 서비스 서버 생성
- 요청 처리 함수 등록
- 계산 처리
- 결과 로그 출력
4) 서비스 타입 별칭 선언
using CalculateTwoNumbers = my_first_package_msgs::srv::CalculateTwoNumbers;
서비스 타입 이름이 길기 때문에 짧은 이름으로 줄여서 사용합니다. 원래 타입 이름은 다음과 같습니다.
my_first_package_msgs::srv::CalculateTwoNumbers
이후 코드에서는 간단히 CalculateTwoNumbers라고 사용할 수 있습니다. 코드 가독성을 높이기 위한 문법입니다.
5) 생성자에서 노드 이름 설정
MathServer()
: Node("math_server")
MathServer 객체가 생성될 때 실행되는 생성자입니다.
: Node("math_server")는 ROS 2 노드 이름을 math_server로 설정합니다.
실행 후 다음 명령어로 노드를 확인할 수 있습니다.
ros2 node list
목록에는 다음과 같은 이름이 표시됩니다.
/math_server
6) 서비스 서버 생성
service_ = this->create_service<CalculateTwoNumbers>(
"calculate_two_numbers",
std::bind(
&MathServer::handle_request,
this,
std::placeholders::_1,
std::placeholders::_2));
이 부분에서 서비스 서버를 생성합니다.
서비스 이름은 다음과 같습니다.
"calculate_two_numbers"
따라서 실제 서비스 이름은 보통 다음과 같이 표시됩니다.
/calculate_two_numbers
서비스 목록은 다음 명령어로 확인할 수 있습니다.
ros2 service list
create_service()의 두 번째 인자에는 요청이 들어왔을 때 실행할 함수가 들어갑니다.
여기서는 handle_request() 함수가 실행됩니다.
std::bind(
&MathServer::handle_request,
this,
std::placeholders::_1,
std::placeholders::_2)
std::bind는 클래스의 멤버 함수를 콜백으로 연결할 때 사용합니다.
&MathServer::handle_request는 요청을 처리할 함수입니다.
this는 현재 객체 자신을 의미합니다.
std::placeholders::_1은 요청 객체입니다.
std::placeholders::_2는 응답 객체입니다.
즉, 클라이언트 요청이 들어오면 내부적으로 다음과 비슷하게 실행됩니다. placeholders는 std::bind()에서 사용하는 자리 표시자 모음입니다.
handle_request(request, response);
7) 서비스 준비 로그 출력
RCLCPP_INFO(this->get_logger(), "Math service server is ready.");
서비스 서버가 준비되었음을 터미널에 출력합니다.
노드가 정상적으로 실행되면 다음과 같은 로그가 출력됩니다.
Math service server is ready.
로봇 프로그램에서는 이런 로그를 통해 노드가 정상적으로 실행되었는지 확인할 수 있습니다.
8) 요청 처리 함수
void handle_request(
const std::shared_ptr<CalculateTwoNumbers::Request> request,
std::shared_ptr<CalculateTwoNumbers::Response> response)
이 함수는 클라이언트가 서비스를 호출했을 때 실행됩니다.
request에는 클라이언트가 보낸 값이 들어 있습니다. response에는 서버가 돌려줄 값을 채워 넣습니다.
이 코드에서 요청값은 다음과 같습니다.
request->x
request->y
request->arithmetic_operator
응답값은 다음과 같습니다.
response->result
response->success
response->message
const double x = request->x;
const double y = request->y;
const std::string op = request->arithmetic_operator;
요청으로 받은 값을 지역 변수에 저장합니다.
x는 첫 번째 숫자입니다.
y는 두 번째 숫자입니다.
op는 연산자 문자열입니다.
예를 들어 클라이언트가 다음과 같이 요청하면
x: 10.0
y: 5.0
arithmetic_operator: "divide"
서버 내부에서는 다음 값으로 처리됩니다.
x = 10.0;
y = 5.0;
op = "divide";
response->success = true;
response->message = "calculation completed";
처음에는 계산이 성공한다고 가정하고 응답값을 설정합니다.
정상 연산이면 이 값이 그대로 사용됩니다.
하지만 0으로 나누거나 알 수 없는 연산자가 들어오면 아래 조건문에서 success와 message가 다시 변경됩니다.
11) 덧셈 처리
if (op == "add") {
response->result = x + y;
}
연산자가 "add"이면 덧셈을 수행합니다.
예를 들어 다음 요청이 들어오면
x: 3.0
y: 2.0
operarithmetic_operatorator_: "add"
결과는 다음과 같습니다.
result: 5.0
success: true
message: calculation completed
모바일 로봇에서는 좌우 바퀴 이동 거리 합산이나 센서값 누적 계산에 비슷한 구조를 사용할 수 있습니다.
12) 뺄셈 처리
else if (op == "subtract") {
response->result = x - y;
}
연산자가 "subtract"이면 뺄셈을 수행합니다.
예를 들어 다음 요청이 들어오면
x: 3.0
y: 2.0
arithmetic_operator: "subtract"
결과는 다음과 같습니다.
result: 1.0
success: true
message: calculation completed
모바일 로봇에서는 목표 위치와 현재 위치의 오차 계산, 좌우 바퀴 속도 차이 계산 등에 응용할 수 있습니다.
13) 곱셈 처리
else if (op == "multiply") {
response->result = x * y;
}
연산자가 "multiply"이면 곱셈을 수행합니다.
예를 들어 다음 요청이 들어오면
x: 4.0
y: 2.5
arithmetic_operator_: "multiply"
결과는 다음과 같습니다.
result: 10.0
success: true
message: calculation completed
모바일 로봇에서는 속도와 시간을 곱해 이동 거리를 계산하거나, 보정 계수를 적용할 때 사용할 수 있습니다.
14) 나눗셈 처리
else if (op == "divide") {
if (y == 0.0) {
response->result = 0.0;
response->success = false;
response->message = "division by zero is not allowed";
} else {
response->result = x / y;
}
}
연산자가 "divide"이면 나눗셈을 수행합니다.
단, y가 0.0이면 나눗셈을 할 수 없습니다.
그래서 먼저 다음 조건을 검사합니다.
if (y == 0.0)
y가 0이면 실패로 처리합니다.
response->success = false;
response->message = "division by zero is not allowed";
y가 0이 아니면 정상적으로 계산합니다.
response->result = x / y;
이런 예외 처리는 로봇 프로그램에서도 중요합니다.
잘못된 입력이 들어왔을 때 노드가 멈추지 않고 실패 이유를 응답할 수 있기 때문입니다.
15) 알 수 없는 연산자 처리
else {
response->result = 0.0;
response->success = false;
response->message = "unknown operator";
}
허용되지 않은 연산자가 들어오면 실패로 처리합니다.
이 코드에서 허용하는 연산자는 다음 네 가지입니다.
add
subtract
multiply
divide
예를 들어 "plus", "+", "ADD" 같은 값은 처리하지 않습니다.
이 경우 응답은 다음과 비슷합니다.
result: 0.0
success: false
message: unknown operator
RCLCPP_INFO(
this->get_logger(),
"Request: %.2f %s %.2f -> Result: %.2f, Success: %s",
x,
op.c_str(),
y,
response->result,
response->success ? "true" : "false");
요청값과 계산 결과를 로그로 출력합니다.
%.2f는 실수를 소수점 둘째 자리까지 출력합니다. %s는 문자열을 출력합니다.
op.c_str()은 std::string을 C 스타일 문자열로 변환합니다.
마지막 부분은 삼항 연산자입니다.
response->success ? "true" : "false"
success가 true이면 "true", false이면 "false"를 출력합니다.
예상 로그는 다음과 같습니다.
Request: 10.00 divide 5.00 -> Result: 2.00, Success: true
17) 서비스 객체 저장
rclcpp::Service<CalculateTwoNumbers>::SharedPtr service_;
생성된 서비스 서버 객체를 저장하는 멤버 변수입니다.
이 객체를 클래스 멤버로 보관해야 서비스 서버가 계속 유지됩니다.
생성자 안의 지역 변수로만 만들면 생성자가 끝난 뒤 객체가 사라질 수 있습니다.
따라서 서비스 서버 객체는 이렇게 멤버 변수로 선언하는 것이 일반적입니다.
18) main 함수
int main(int argc, char ** argv)
C++ 프로그램의 시작점입니다.
ROS 2 노드도 실행 파일이므로 main() 함수에서 시작합니다.
19) ROS 2 초기화
rclcpp::init(argc, argv);
ROS 2를 초기화합니다.
노드를 만들기 전에 반드시 실행해야 합니다.
20) 노드 객체 생성
auto node = std::make_shared<MathServer>();
MathServer 노드 객체를 생성합니다.
이 코드가 실행되면 생성자가 호출되고, 그 안에서 서비스 서버가 만들어집니다.
즉, 이 한 줄로 다음 작업이 진행됩니다.
- math_server 노드 생성
- calculate_two_numbers 서비스 생성
- 준비 완료 로그 출력
21) 노드 실행
rclcpp::spin(node);
노드를 계속 실행 상태로 유지합니다.
서비스 서버는 요청이 들어올 때까지 기다려야 하므로 spin()이 필요합니다.
클라이언트가 서비스를 호출하면 handle_request() 함수가 실행됩니다.
22) ROS 2 종료
rclcpp::shutdown();
노드 실행이 끝난 뒤 ROS 2를 종료합니다.
보통 사용자가 Ctrl + C로 종료하면 spin()이 끝나고, 이후 shutdown()이 실행됩니다.
23) 프로그램 종료
return 0;
프로그램이 정상적으로 종료되었음을 의미합니다.
5. 서비스 클라이언트 코드 작성하기
다음 파일을 만듭니다.
touch src/math_client.cpp

또는 VS Code를 사용한다면 다음처럼 열어도 됩니다.
code src/math_client.cpp
내용은 다음과 같이 작성합니다.
#include <chrono>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "my_first_package_msgs/srv/calculate_two_numbers.hpp"
using namespace std::chrono_literals;
class MathClient : public rclcpp::Node
{
public:
using CalculateTwoNumbers = my_first_package_msgs::srv::CalculateTwoNumbers;
MathClient()
: Node("math_client")
{
client_ = this->create_client<CalculateTwoNumbers>("calculate_two_numbers");
}
void send_request(double x, double y, const std::string & op)
{
while (!client_->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for service.");
return;
}
RCLCPP_INFO(this->get_logger(), "Waiting for service server...");
}
auto request = std::make_shared<CalculateTwoNumbers::Request>();
request->x = x;
request->y = y;
request->arithmetic_operator = op;
auto future = client_->async_send_request(request);
if (
rclcpp::spin_until_future_complete(
this->get_node_base_interface(),
future) == rclcpp::FutureReturnCode::SUCCESS)
{
auto response = future.get();
RCLCPP_INFO(
this->get_logger(),
"Result: %.2f, Success: %s, Message: %s",
response->result,
response->success ? "true" : "false",
response->message.c_str());
} else {
RCLCPP_ERROR(this->get_logger(), "Failed to call service.");
}
}
private:
rclcpp::Client<CalculateTwoNumbers>::SharedPtr client_;
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MathClient>();
node->send_request(12.0, 3.0, "divide");
rclcpp::shutdown();
return 0;
}

6. 클라이언트 코드 분석
1) 헤더 파일 포함
#include "my_first_package_msgs/srv/calculate_two_numbers.hpp"
이 부분은 사용자가 직접 만든 서비스 메시지 타입을 포함합니다.
여기서는 CalculateTwoNumbers라는 서비스 타입을 사용합니다.
2) 시간 리터럴 사용 설정
using namespace std::chrono_literals;
이 코드는 1s, 500ms와 같은 시간 표현을 사용할 수 있게 해줍니다.
예를 들어 아래 코드에서 사용됩니다.
client_->wait_for_service(1s)
이 의미는 서비스 서버가 준비되었는지 1초 동안 기다린다는 뜻입니다.
3) MathClient 클래스 선언
class MathClient : public rclcpp::Node
MathClient 클래스는 ROS 2 노드를 만들기 위한 클래스입니다.
rclcpp::Node를 상속받기 때문에 이 클래스는 ROS 2 노드로 동작할 수 있습니다.
이 노드는 서비스 서버에게 두 숫자와 연산자를 보내고, 계산 결과를 받아오는 역할을 합니다.
모바일 로봇 예제로 보면, 로봇 내부에서 특정 계산이 필요할 때 계산 서버에 요청을 보내는 클라이언트 역할이라고 볼 수 있습니다.
예를 들어 속도 비율 계산, 거리 계산, 회전 시간 계산 같은 작업을 별도의 서비스 서버에 요청할 수 있습니다.
4) 서비스 타입 이름 간단히 정의
using CalculateTwoNumbers = my_first_package_msgs::srv::CalculateTwoNumbers;
서비스 타입 이름이 길기 때문에 짧은 이름으로 다시 정의한 부분입니다.
원래는 아래처럼 긴 이름을 계속 사용해야 합니다.
my_first_package_msgs::srv::CalculateTwoNumbers
하지만 위의 using을 사용하면 이후 코드에서는 간단히 다음처럼 쓸 수 있습니다.
CalculateTwoNumbers
코드를 더 읽기 쉽게 만들기 위한 처리입니다.
5) 생성자에서 노드 이름과 서비스 클라이언트 생성
MathClient()
: Node("math_client")
{
client_ = this->create_client<CalculateTwoNumbers>("calculate_two_numbers");
}
생성자는 MathClient 객체가 만들어질 때 자동으로 실행됩니다.
: Node("math_client")
이 부분은 ROS 2 노드 이름을 math_client로 설정합니다.
즉, 이 프로그램을 실행하면 ROS 2 시스템 안에서 math_client라는 이름의 노드가 생성됩니다.
client_ = this->create_client<CalculateTwoNumbers>("calculate_two_numbers");
이 코드는 서비스 클라이언트를 생성합니다.
서비스 이름은 다음과 같습니다.
"calculate_two_numbers"
즉, 이 클라이언트는 calculate_two_numbers라는 이름의 서비스 서버를 찾아 요청을 보냅니다.
서비스 서버 쪽에서도 같은 서비스 이름을 사용해야 정상적으로 연결됩니다.
6) send_request 함수 역할
void send_request(double x, double y, const std::string & op)
이 함수는 실제로 서비스 요청을 보내는 함수입니다.
입력값은 세 개입니다.
double x
double y
const std::string & op
x와 y는 계산할 두 숫자입니다. op는 연산자를 의미합니다.
예를 들어 다음과 같이 호출하면
node->send_request(12.0, 3.0, "divide");
서비스 서버에 12.0, 3.0, "divide" 값을 보냅니다.
즉, 서버에게 “12.0을 3.0으로 나누어 달라”는 요청을 보내는 구조입니다.
7) 서비스 서버가 준비될 때까지 대기
while (!client_->wait_for_service(1s)) {
이 코드는 서비스 서버가 실행 중인지 확인하는 부분입니다.
서비스 클라이언트가 요청을 보내려면 먼저 서비스 서버가 준비되어 있어야 합니다.
wait_for_service(1s)는 1초 동안 서비스 서버를 기다립니다.
서버가 없으면 false를 반환하고, while문이 반복됩니다.
RCLCPP_INFO(this->get_logger(), "Waiting for service server...");
서비스 서버가 아직 준비되지 않았을 때 로그를 출력합니다.
8) ROS 2가 종료되었는지 확인
if (!rclcpp::ok()) {
RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for service.");
return;
}
서비스 서버를 기다리는 도중에 사용자가 프로그램을 종료하거나 ROS 2가 정상 상태가 아니게 되면 이 조건문이 실행됩니다.
rclcpp::ok()는 ROS 2가 정상적으로 동작 중인지 확인합니다.
정상 상태가 아니라면 에러 로그를 출력하고 함수 실행을 종료합니다.
9) 서비스 요청 객체 생성
auto request = std::make_shared<CalculateTwoNumbers::Request>();
서비스 서버에 보낼 요청 객체를 생성합니다.
ROS 2 서비스 요청은 보통 shared_ptr 형태로 생성합니다.
이 요청 객체 안에 서버로 보낼 데이터를 저장합니다.
10) 요청 데이터 설정
request->x = x;
request->y = y;
request->arithmetic_operator_ = op;
이 부분은 요청 메시지에 값을 넣는 코드입니다.
request->x = x;
첫 번째 숫자를 저장합니다.
request->y = y;
두 번째 숫자를 저장합니다.
request->arithmetic_operator_ = op;
연산자 문자열을 저장합니다.
11) 비동기 방식으로 서비스 요청 전송
auto future = client_->async_send_request(request);
이 코드는 서비스 서버에 요청을 보냅니다.
함수 이름에 async가 들어가 있으므로 비동기 방식입니다.
즉, 요청을 보낸 뒤 바로 결과가 나오는 것이 아니라, 나중에 서버가 응답하면 그 결과를 받을 수 있는 구조입니다.
응답 결과는 future 객체에 저장됩니다.
12) 서비스 응답이 올 때까지 대기
rclcpp::spin_until_future_complete(
this->get_node_base_interface(),
future)
이 코드는 서비스 서버의 응답이 올 때까지 노드를 실행 상태로 유지합니다.
future에 응답이 들어올 때까지 기다립니다.
서비스 응답이 정상적으로 도착하면 다음 값과 비교됩니다.
rclcpp::FutureReturnCode::SUCCESS
즉, 서비스 호출이 성공했는지 확인하는 부분입니다.
13) 서비스 응답 처리
auto response = future.get();
서비스 서버로부터 받은 응답 데이터를 꺼내는 부분입니다.
응답 안에는 다음과 같은 값들이 있다고 볼 수 있습니다.
response->result
response->success
response->message
result는 계산 결과입니다.
success는 계산이 성공했는지 여부입니다.
message는 서버에서 전달한 설명 메시지입니다.
14) 결과 로그 출력
RCLCPP_INFO(
this->get_logger(),
"Result: %.2f, Success: %s, Message: %s",
response->result,
response->success ? "true" : "false",
response->message.c_str());
이 부분은 서비스 서버로부터 받은 결과를 출력합니다.
"Result: %.2f"
계산 결과를 소수점 둘째 자리까지 출력합니다.
response->success ? "true" : "false"
success 값이 참이면 "true"를 출력하고, 거짓이면 "false"를 출력합니다.
response->message.c_str()
std::string 타입의 메시지를 C 스타일 문자열로 변환해서 출력합니다.
15) 서비스 호출 실패 처리
else {
RCLCPP_ERROR(this->get_logger(), "Failed to call service.");
}
서비스 서버로부터 정상적인 응답을 받지 못하면 이 부분이 실행됩니다.
예를 들어 서버가 중간에 종료되었거나, 통신 문제가 발생했을 때 실패 로그를 출력합니다.
16) 서비스 클라이언트 멤버 변수
rclcpp::Client<CalculateTwoNumbers>::SharedPtr client_;
이 변수는 서비스 클라이언트 객체를 저장합니다.
클래스 내부에서 계속 사용해야 하므로 멤버 변수로 선언되어 있습니다.
이 클라이언트를 통해 calculate_two_numbers 서비스 서버에 요청을 보냅니다.
17) MathClient 노드 생성
auto node = std::make_shared<MathClient>();
MathClient 객체를 생성합니다.
이 시점에서 생성자가 실행되고, 노드 이름은 math_client로 설정됩니다.
또한 생성자 내부에서 서비스 클라이언트도 함께 생성됩니다.
18) 서비스 요청 실행
node->send_request(12.0, 3.0, "divide");
생성된 노드를 이용해 서비스 요청을 보냅니다.
이 코드는 서버에게 다음 값을 전달합니다.
x = 12.0
y = 3.0
operator = "divide"
서비스 서버가 정상적으로 구현되어 있다면 12.0 / 3.0 계산을 수행하고 결과로 4.0을 반환할 수 있습니다.
모바일 로봇 예제로 보면, 특정 계산 기능을 서비스 서버에 맡기고 클라이언트 노드는 필요한 입력값만 보내는 구조입니다.
19) ROS 2 종료
rclcpp::shutdown();
ROS 2 사용을 종료하는 코드입니다.
노드 실행이 끝난 뒤에는 shutdown()을 호출해 ROS 2 관련 자원을 정리합니다.
7. CMakeLists.txt 수정하기
my_cpp_package/CMakeLists.txt를 다음처럼 수정합니다.
중요한 점은 my_cpp_package에서 직접 .srv를 생성하지 않는다는 것입니다.
인터페이스는 my_first_package_msgs 패키지에서 가져옵니다.
cmake_minimum_required(VERSION 3.8)
project(my_cpp_package)
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(my_first_package_msgs REQUIRED)
add_executable(math_server src/math_server.cpp)
ament_target_dependencies(math_server rclcpp my_first_package_msgs)
add_executable(math_client src/math_client.cpp)
ament_target_dependencies(math_client rclcpp my_first_package_msgs)
install(TARGETS
math_server
math_client
DESTINATION lib/${PROJECT_NAME}
)
ament_package()

기존 원문처럼 my_cpp_package 안에서 직접 .srv를 생성하는 경우에는 다음 설정이 필요합니다.
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/CalculateTwoNumbers.srv"
)
하지만 이번 조건에서는 인터페이스 정의를 my_first_package_msgs에서 사용하므로 위 설정은 my_cpp_package에 넣지 않습니다.
이번 실습에서 my_cpp_package는 서비스 인터페이스를 생성하는 패키지가 아니라, 이미 생성된 인터페이스를 사용하는 실행 패키지입니다.

8. package.xml 수정하기
package.xml은 다음처럼 수정합니다.
<?xml version="1.0"?>
<package format="3">
<name>my_cpp_package</name>
<version>0.0.1</version>
<description>ROS 2 Humble C++ service client and server example for mobile robot practice</description>
<maintainer email="user@example.com">ros2 student</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>my_first_package_msgs</depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>

여기서 핵심은 다음입니다.
<depend>my_first_package_msgs</depend>
my_cpp_package에서 다음 설정은 사용하지 않습니다.
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
이 설정은 .srv, .msg, .action 파일을 직접 정의하는 인터페이스 패키지에 들어가는 설정입니다.
이번 실습에서는 인터페이스 정의를 my_first_package_msgs가 담당하므로, my_cpp_package는 my_first_package_msgs에 의존만 하면 됩니다.
그리고 ament_cmake 패키지에서는 다음 부분도 확인해야 합니다.
<export>
<build_type>ament_cmake</build_type>
</export>
이 부분이 빠지면 패키지 인식이나 빌드 과정에서 문제가 생길 수 있습니다.
9. 빌드하기
워크스페이스 최상위 폴더로 이동합니다.
cd ~/ros2_study
먼저 인터페이스 패키지와 실습 패키지를 함께 빌드합니다.
colcon build --packages-select my_first_package_msgs my_cpp_package
이미 my_first_package_msgs가 정상 빌드되어 있다면 다음처럼 my_cpp_package만 빌드해도 됩니다.
colcon build --packages-select my_cpp_package
빌드가 끝나면 환경 설정을 적용합니다.
source install/setup.bash
10. 실행하기
터미널을 두 개 엽니다.
터미널 1: 서버 실행
cd ~/ros2_study
source install/setup.bash
ros2 run my_cpp_package math_server
정상 실행되면 다음과 비슷한 로그가 나옵니다.
Math service server is ready.

서버는 이제 요청을 기다리는 상태입니다.
터미널 2: 클라이언트 실행
cd ~/ros2_study
source install/setup.bash
ros2 run my_cpp_package math_client
정상 실행되면 클라이언트 쪽에 다음과 비슷한 로그가 나옵니다.
Result: 4.00, Success: true, Message: calculation completed
서버 쪽에는 다음과 비슷한 로그가 나옵니다.
Request: 12.00 divide 3.00 -> Result: 4.00, Success: true


11. ROS 2 CLI로 확인하기
서비스가 제대로 등록되었는지 확인합니다.
ros2 service list
결과에 다음 이름이 있어야 합니다.
/calculate_two_numbers

서비스 타입도 확인합니다.
ros2 service type /calculate_two_numbers
예상 결과는 다음과 같습니다.
my_first_package_msgs/srv/CalculateTwoNumbers

인터페이스 구조도 확인합니다.
ros2 interface show my_first_package_msgs/srv/CalculateTwoNumbers
예상 결과는 다음과 비슷합니다.
float64 x
float64 y
string operator
---
float64 result
bool success
string message

12. CLI로 직접 서비스 호출하기
클라이언트 노드를 실행하지 않아도 터미널에서 직접 서비스를 호출할 수 있습니다.
먼저 서버는 실행 중이어야 합니다.
ros2 run my_cpp_package math_server
다른 터미널에서 다음 명령을 실행합니다.
ros2 service call /calculate_two_numbers my_first_package_msgs/srv/CalculateTwoNumbers "{x: 8.0, y: 2.0, arithmetic_operator: 'multiply'}"
예상 응답은 다음과 같습니다.
result: 16.0
success: true
message: calculation completed


나눗셈도 테스트합니다.
ros2 service call /calculate_two_numbers my_first_package_msgs/srv/CalculateTwoNumbers "{x: 8.0, y: 2.0, arithmetic_operator: 'divide'}"
예상 응답입니다.
result: 4.0
success: true
message: calculation completed


0으로 나누는 예외 상황도 테스트합니다.
ros2 service call /calculate_two_numbers my_first_package_msgs/srv/CalculateTwoNumbers "{x: 8.0, y: 0.0, arithmetic_operator: 'divide'}"
예상 응답입니다.
result: 0.0
success: false
message: division by zero is not allowed


알 수 없는 연산자도 테스트합니다.
ros2 service call /calculate_two_numbers my_first_package_msgs/srv/CalculateTwoNumbers "{x: 8.0, y: 2.0, arthmetic_operator: 'power'}"
예상 응답입니다.
result: 0.0
success: false
message: unknown operator


13. 이번 예제에서 반드시 기억할 것
요청과 응답 구조는 my_first_package_msgs에 정의된 .srv 파일에서 정합니다.
float64 x
float64 y
string arithmetci_operator
---
float64 result
bool success
string message
서버와 클라이언트는 같은 서비스 타입을 사용해야 합니다.
my_first_package_msgs::srv::CalculateTwoNumbers
서버와 클라이언트는 같은 서비스 이름을 사용해야 합니다.
"calculate_two_numbers"
응답은 return으로 주는 것이 아니라 response 객체에 채워 넣습니다.
response->result = x + y;
response->success = true;
response->message = "calculation completed";
서버는 spin()이 있어야 요청을 처리합니다.
rclcpp::spin(node);
이번 실습에서 my_cpp_package는 인터페이스를 직접 만들지 않습니다.
인터페이스는 다음 패키지에서 가져옵니다.
my_first_package_msgs
따라서 include와 타입 이름은 반드시 다음 형태를 사용합니다.
#include "my_first_package_msgs/srv/calculate_two_numbers.hpp"
using CalculateTwoNumbers = my_first_package_msgs::srv::CalculateTwoNumbers;
'강좌 > ROS2' 카테고리의 다른 글
| ROS 2 C++ 파라미터 실습 (0) | 2026.05.15 |
|---|---|
| ROS 2 C++ Action Server / Client 실습 #2 (0) | 2026.05.15 |
| ROS 2 C++ 토픽 통신 기초: 센서 데이터를 보내고 받는 퍼블리셔와 서브스크라이버 만들기 (0) | 2026.05.15 |
| ROS2 launch 작성 (0) | 2026.05.10 |
| 소스에서 파라미터 사용하기 (0) | 2026.05.10 |