서비스의 개념

ROS2의 서비스는 요청(request)과 응답(response)을 주고받는 통신 메커니즘을 제공한다. 서비스는 클라이언트가 서버에 요청을 보내고, 서버가 요청을 처리한 후 결과를 응답하는 구조로 이루어진다. 이는 일종의 원격 프로시저 호출(Remote Procedure Call, RPC)로 이해할 수 있다.

서비스 서버와 클라이언트의 구성 요소

  1. 서비스 서버: 요청을 처리하는 역할을 한다. 클라이언트가 보낸 요청을 받아 처리한 후 결과를 응답한다.
  2. 서비스 클라이언트: 서버에 요청을 보내고, 서버로부터 응답을 기다린다.

메시지 타입

서비스는 메시지 타입을 기반으로 요청과 응답을 주고받는다. ROS2에서는 요청과 응답 메시지가 .srv 파일 형식으로 정의되며, 해당 파일에는 요청과 응답 필드가 명시된다.

예를 들어, 서비스 정의 파일은 아래와 같이 구성될 수 있다.

int64 a
int64 b
---
int64 sum

위 정의에서는 클라이언트가 두 개의 int64ab를 서버에 보내면, 서버는 두 값을 더한 후 sum으로 응답하는 구조다.

서비스 서버의 구현

서비스 서버는 요청을 수신하고 이를 처리한 후 응답하는 역할을 한다. Python과 C++에서 각각의 서비스 서버를 구현할 수 있다.

Python 서비스 서버 예제

import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class ServiceServer(Node):

    def __init__(self):
        super().__init__('add_two_ints_server')
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

    def add_two_ints_callback(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info(f'Incoming request: a={request.a}, b={request.b}')
        return response

def main(args=None):
    rclpy.init(args=args)
    node = ServiceServer()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()
  1. 노드 생성: ServiceServer 클래스에서 Node 객체를 상속받아 노드를 생성한다.
  2. 서비스 생성: create_service 메서드를 사용하여 서비스를 생성한다. 이 메서드는 서비스 타입(AddTwoInts), 서비스 이름(add_two_ints), 그리고 콜백 함수(add_two_ints_callback)을 인자로 받는다.
  3. 콜백 함수: 요청이 들어오면 add_two_ints_callback이 호출되며, 요청 데이터를 처리한 후 응답 데이터를 반환한다.

C++ 서비스 서버 예제

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

using std::placeholders::_1;
using std::placeholders::_2;

class ServiceServer : public rclcpp::Node
{
public:
    ServiceServer()
    : Node("add_two_ints_server")
    {
        service_ = this->create_service<example_interfaces::srv::AddTwoInts>(
            "add_two_ints", std::bind(&ServiceServer::add_two_ints_callback, this, _1, _2));
    }

private:
    void add_two_ints_callback(
        const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
        std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
    {
        response->sum = request->a + request->b;
        RCLCPP_INFO(this->get_logger(), "Incoming request: a=%ld, b=%ld", request->a, request->b);
    }

    rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<ServiceServer>());
    rclcpp::shutdown();
    return 0;
}
  1. 노드 생성: C++에서도 ServiceServer 클래스에서 Node 객체를 상속받아 노드를 생성한다.
  2. 서비스 생성: create_service 메서드를 통해 서비스를 생성하며, 서비스 타입과 콜백 함수를 지정한다.
  3. 콜백 함수: Python과 동일하게 요청을 처리하고 응답을 생성하는 콜백 함수가 구현된다.

서비스 클라이언트의 구현

서비스 클라이언트는 서버에 요청을 보내고 응답을 기다리는 역할을 한다.

Python 서비스 클라이언트 예제

import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class ServiceClient(Node):

    def __init__(self):
        super().__init__('add_two_ints_client')
        self.client = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('Service not available, waiting again...')
        self.request = AddTwoInts.Request()

    def send_request(self, a, b):
        self.request.a = a
        self.request.b = b
        future = self.client.call_async(self.request)
        return future

def main(args=None):
    rclpy.init(args=args)
    node = ServiceClient()
    response = node.send_request(2, 3)
    rclpy.spin_until_future_complete(node, response)
    if response.result() is not None:
        node.get_logger().info(f'Result: {response.result().sum}')
    else:
        node.get_logger().error('Service call failed')
    rclpy.shutdown()

if __name__ == '__main__':
    main()
  1. 클라이언트 생성: create_client 메서드를 사용하여 서비스를 생성하며, 서비스 타입과 이름을 인자로 받는다.
  2. 비동기 호출: call_async 메서드를 통해 비동기적으로 서버에 요청을 보내고 응답을 기다린다.
  3. 응답 처리: spin_until_future_complete로 응답이 올 때까지 기다린 후 결과를 처리한다.

C++ 서비스 클라이언트 예제

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

class ServiceClient : public rclcpp::Node
{
public:
    ServiceClient()
    : Node("add_two_ints_client")
    {
        client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
        while (!client_->wait_for_service(std::chrono::seconds(1))) {
            RCLCPP_INFO(this->get_logger(), "Waiting for service to appear...");
        }
    }

    void send_request(int64_t a, int64_t b)
    {
        auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
        request->a = a;
        request->b = b;

        auto future = client_->async_send_request(request);
        future.wait();
        if (future.valid()) {
            RCLCPP_INFO(this->get_logger(), "Result: %ld", future.get()->sum);
        } else {
            RCLCPP_ERROR(this->get_logger(), "Service call failed");
        }
    }

private:
    rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared<ServiceClient>();
    node->send_request(2, 3);
    rclcpp::shutdown();
    return 0;
}
  1. 클라이언트 생성: C++에서 create_client 메서드를 사용하여 서비스를 생성하고, 서비스가 준비될 때까지 기다린다.
  2. 비동기 호출: async_send_request 메서드를 통해 서버에 비동기적으로 요청을 보낸다.
  3. 응답 처리: future.wait()로 응답을 기다린 후 결과를 출력한다.

클라이언트와 서버의 통신 흐름

sequenceDiagram participant Client participant Server Client->>Server: 요청 (Request) Server-->>Client: 응답 (Response)
  1. 클라이언트가 서버로 요청을 보낸다.
  2. 서버는 요청을 처리한 후 응답을 클라이언트로 반환한다.

서비스 호출 시 발생 가능한 문제

  1. 타임아웃: 서비스 서버가 비활성화되어 있거나 서비스가 오래 걸릴 경우, 클라이언트는 타임아웃이 발생할 수 있다. 이를 해결하기 위해 적절한 타임아웃 시간을 설정하거나 서비스의 성능을 최적화해야 한다.
  2. 비동기 호출 처리: 비동기 방식으로 요청을 보낼 때, 클라이언트는 서비스 호출이 완료될 때까지 대기해야 한다. 이를 효율적으로 처리하기 위해 future 객체를 활용하여 응답을 처리하는 방법이 많이 사용된다.

서비스의 비동기 호출과 처리

서비스 호출은 비동기적으로 이루어질 수 있으며, 이 경우 클라이언트는 서비스 응답이 올 때까지 기다리는 동안 다른 작업을 진행할 수 있다. 이를 통해 서비스 호출 중 발생하는 블로킹을 방지하고 시스템의 효율성을 높일 수 있다.

Python에서 비동기 처리

Python에서는 call_async() 메서드를 통해 비동기 호출을 할 수 있으며, Future 객체를 사용하여 응답을 처리할 수 있다. Future 객체는 요청을 보내고 응답이 올 때까지 기다리며, 응답이 완료되면 자동으로 결과를 반환한다.

def send_request(self, a, b):
    self.request.a = a
    self.request.b = b
    future = self.client.call_async(self.request)
    future.add_done_callback(self.callback_function)

위 코드에서 add_done_callback() 메서드를 사용하여 비동기 호출이 완료된 후 특정 함수(callback_function)를 호출하게 된다.

C++에서 비동기 처리

C++에서는 async_send_request() 메서드를 사용하여 비동기 호출을 처리하며, future 객체를 통해 결과를 받아올 수 있다.

auto future = client_->async_send_request(request);
future.wait(); // 응답이 완료될 때까지 대기
if (future.valid()) {
    RCLCPP_INFO(this->get_logger(), "Result: %ld", future.get()->sum);
}

future.wait() 메서드를 통해 응답이 완료될 때까지 대기하고, 응답이 완료되면 future.get()을 통해 결과를 받아온다.

서비스의 동기 호출과 처리

비동기 호출과 달리 동기 호출은 클라이언트가 요청을 보내고 응답을 받을 때까지 대기하는 방식이다. 동기 호출은 일반적으로 짧은 시간이 걸리는 서비스에 적합하며, 클라이언트가 요청과 응답 사이에 다른 작업을 수행할 필요가 없을 때 사용된다.

Python에서 동기 호출

Python에서는 rclpy.spin_until_future_complete() 메서드를 사용하여 동기적으로 호출할 수 있다.

response = node.send_request(2, 3)
rclpy.spin_until_future_complete(node, response)
if response.result() is not None:
    node.get_logger().info(f'Result: {response.result().sum}')

위 코드에서 spin_until_future_complete()는 클라이언트가 응답을 받을 때까지 대기하는 동기 호출 방식을 제공한다.

C++에서 동기 호출

C++에서는 future.wait() 메서드를 사용하여 응답을 기다리는 동기 호출을 구현할 수 있다.

auto future = client_->async_send_request(request);
future.wait(); // 응답이 올 때까지 대기
if (future.valid()) {
    RCLCPP_INFO(this->get_logger(), "Result: %ld", future.get()->sum);
}

wait() 메서드를 통해 클라이언트는 응답이 올 때까지 대기하며, future.get()을 통해 결과를 받아온다.

서비스 서버와 클라이언트의 네임스페이스 관리

ROS2에서 네임스페이스(namespace)는 노드와 서비스, 토픽 등을 그룹화하여 관리하는 데 사용된다. 이는 복잡한 시스템에서 각 노드를 효과적으로 구분하고 관리할 수 있는 방법이다. 서비스 서버와 클라이언트도 네임스페이스를 적용할 수 있다.

네임스페이스 적용 방법

서비스 서버와 클라이언트를 구현할 때 네임스페이스를 적용하려면 Node 객체의 생성 시 네임스페이스를 지정할 수 있다. 예를 들어, 서비스 서버를 특정 네임스페이스로 그룹화하려면 다음과 같이 작성할 수 있다.

# Python 예시
node = Node('add_two_ints_server', namespace='/my_namespace')
// C++ 예시
auto node = std::make_shared<ServiceServer>("/my_namespace");

위와 같이 네임스페이스를 적용하면, 클라이언트는 해당 네임스페이스 하위에서 서비스 서버를 찾을 수 있다.

서비스 호출의 QoS 설정

QoS(Quality of Service)는 서비스 요청과 응답의 신뢰성을 높이는 데 중요한 역할을 한다. ROS2는 다양한 QoS 정책을 제공하며, 서비스 클라이언트와 서버 간의 통신에서 QoS를 설정할 수 있다. QoS는 특히 네트워크 상태가 불안정한 환경에서 신뢰성 있는 통신을 보장하는 데 유용하다.

QoS 정책

ROS2에서 QoS는 Reliability, Durability, History, Deadline, Lifespan 등의 파라미터로 구성된다. 서비스 클라이언트와 서버 간 통신 시 QoS 설정을 적용할 수 있으며, 이를 통해 데이터 손실을 최소화할 수 있다.

QoS 적용 예시

Python에서 QoS 설정을 적용하려면 서비스 생성 시 QoS 설정을 함께 정의할 수 있다.

from rclpy.qos import QoSProfile, QoSReliabilityPolicy

qos_profile = QoSProfile(depth=10, reliability=QoSReliabilityPolicy.RMW_QOS_POLICY_RELIABILITY_RELIABLE)
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback, qos_profile=qos_profile)

위 코드에서 QoSProfileQoSReliabilityPolicy를 통해 서비스 서버에 QoS 설정을 적용한 것이다. 이와 유사하게 클라이언트에서도 QoS를 설정할 수 있다.

서비스의 콜백 함수 처리

서비스 서버에서 클라이언트의 요청을 처리하기 위해서는 요청이 들어올 때마다 콜백 함수가 실행된다. 콜백 함수는 요청 데이터를 입력받아 이를 처리하고 응답 데이터를 반환하는 역할을 한다.

콜백 함수의 구조

콜백 함수는 일반적으로 두 개의 인자를 받는다: - request: 클라이언트에서 전달된 요청 데이터. - response: 서버가 클라이언트에 응답할 데이터를 담는 객체.

콜백 함수는 요청 데이터를 바탕으로 서버에서 처리해야 할 작업을 수행한 후, 응답 객체에 결과를 담아 반환한다.

Python에서의 콜백 함수

Python에서의 콜백 함수는 아래와 같이 정의할 수 있다.

def add_two_ints_callback(self, request, response):
    response.sum = request.a + request.b
    self.get_logger().info(f'Incoming request: a={request.a}, b={request.b}')
    return response
  1. request.a, request.b: 클라이언트에서 전달된 두 정수 값.
  2. response.sum: 서버에서 계산한 결과를 응답 객체에 저장.
  3. return response: 계산이 완료된 후 응답 객체를 반환.

C++에서의 콜백 함수

C++에서의 콜백 함수는 아래와 같이 정의된다.

void add_two_ints_callback(
    const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
    std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
    response->sum = request->a + request->b;
    RCLCPP_INFO(this->get_logger(), "Incoming request: a=%ld, b=%ld", request->a, request->b);
}
  1. request->a, request->b: 클라이언트에서 전달된 정수 값.
  2. response->sum: 서버에서 계산한 결과를 응답 객체에 저장.
  3. RCLCPP_INFO: 요청 데이터를 로그로 출력하는 부분.

서비스 클라이언트에서의 요청 처리

클라이언트는 서버에 요청을 보내고 응답을 기다린다. 요청과 응답이 이루어지는 동안 클라이언트는 비동기적으로 요청 결과를 받아 처리할 수 있다. 요청 시 클라이언트가 보낸 데이터는 서버의 요청 객체에서 확인할 수 있으며, 서버가 응답하는 데이터는 클라이언트의 응답 객체에서 받아 처리한다.

Python에서의 클라이언트 요청 처리

def send_request(self, a, b):
    self.request.a = a
    self.request.b = b
    future = self.client.call_async(self.request)
    return future
  1. 요청 생성: self.request.aself.request.b에 클라이언트에서 서버로 전송할 데이터를 할당한다.
  2. 비동기 요청: call_async() 메서드를 통해 비동기적으로 서버에 요청을 보낸다.
  3. 응답 대기: spin_until_future_complete()를 통해 응답이 올 때까지 대기하거나 future 객체를 통해 응답을 비동기적으로 처리할 수 있다.

C++에서의 클라이언트 요청 처리

void send_request(int64_t a, int64_t b)
{
    auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
    request->a = a;
    request->b = b;

    auto future = client_->async_send_request(request);
    future.wait(); // 응답 대기
    if (future.valid()) {
        RCLCPP_INFO(this->get_logger(), "Result: %ld", future.get()->sum);
    }
}
  1. 요청 생성: request->arequest->b에 클라이언트에서 서버로 전송할 데이터를 할당한다.
  2. 비동기 요청: async_send_request() 메서드를 통해 비동기적으로 서버에 요청을 보낸다.
  3. 응답 대기: future.wait()로 응답이 올 때까지 대기하고, 응답이 유효한지 확인한 후 결과를 처리한다.

서비스 클라이언트와 서버의 데이터 흐름

서비스 서버와 클라이언트 간의 데이터 흐름은 다음과 같다:

graph TD; A[클라이언트 요청] -->|Request| B[서비스 서버]; B -->|Response| A; B -->|콜백 함수| C[데이터 처리];
  1. 클라이언트가 서버로 요청(Request)을 보낸다.
  2. 서버는 요청을 받아 콜백 함수를 호출하여 데이터를 처리한다.
  3. 처리된 결과를 응답(Response)으로 클라이언트에 반환한다.

서비스 호출의 오류 처리

서비스 호출 중 오류가 발생할 수 있으며, 이를 적절하게 처리하는 것이 중요하다. 예를 들어, 서버가 요청을 처리하는 동안 서버가 중단되거나 클라이언트와의 통신이 끊어질 수 있다.

Python에서의 오류 처리

if response.result() is not None:
    node.get_logger().info(f'Result: {response.result().sum}')
else:
    node.get_logger().error('Service call failed')
  1. 정상 처리: 응답이 정상적으로 반환되면 result() 메서드를 통해 결과를 확인하고 로그에 출력한다.
  2. 오류 처리: 응답이 실패한 경우 else 구문에서 에러 로그를 출력한다.

C++에서의 오류 처리

if (future.valid()) {
    RCLCPP_INFO(this->get_logger(), "Result: %ld", future.get()->sum);
} else {
    RCLCPP_ERROR(this->get_logger(), "Service call failed");
}
  1. 정상 처리: future.valid() 메서드를 통해 응답이 정상적으로 반환되었는지 확인한 후, 결과를 출력한다.
  2. 오류 처리: 응답이 실패한 경우 RCLCPP_ERROR로 에러 메시지를 출력한다.