본문으로 바로가기

ROS 2 C++ 서비스 프로그래밍 이해하기

category 강좌/ROS2 2026. 5. 15. 13:34

이번 실습에서는 ROS 2 Humble 환경에서 C++로 서비스 서버와 서비스 클라이언트를 작성합니다.

 

패키지는 이미 생성되어 있다고 가정합니다.

사용할 패키지 이름은 다음과 같습니다.

 
my_cpp_package
 

 

인터페이스 정의는 다음 패키지를 사용합니다.

 
my_first_package_msgs
 

 

이번 예제에서는 모바일 로봇 제어에서 자주 필요한 구조를 단순화하여, 두 개의 숫자와 명령 문자열을 서버에 보내고 결과를 응답받는 형태로 실습합니다.

 

예제 자체는 계산기 형태이지만, 실제 모바일 로봇에서는 다음과 같은 구조로 확장할 수 있습니다.

속도 제한값 변경
이동 거리 계산
배터리 상태 확인
센서 초기화
오도메트리 리셋
모터 제어 파라미터 변경
 

이번 목표는 다음과 같습니다.

C++로 서비스 서버와 서비스 클라이언트를 만들고,
클라이언트가 두 숫자와 연산 명령을 보내면
서버가 계산 결과를 응답하도록 만들기
 
 
 
 
1. 구조

이번 예제에서는 계산 요청을 처리하는 서비스 서버와 클라이언트를 만듭니다.

 

구조는 다음과 같습니다.

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
 
 
 
9) 요청 데이터 읽기
 
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";
 
 
 
10) 기본 응답값 설정
 
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
 
 
 
16) 처리 결과 로그 출력
 
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;
 
 
 
 
728x90
728x90