1261.30 서비스 클라이언트(Service Client)의 구조

1. 서비스 클라이언트의 정의와 역할

ROS2에서 서비스 클라이언트(Service Client)는 요청-응답 통신 패턴의 요청 측 엔드포인트로서, 서비스 서버(Service Server)에 요청(Request) 메시지를 전달하고 처리 결과인 응답(Response) 메시지를 수신하는 통신 개체이다. 서비스 클라이언트는 ROS2 노드(Node) 내부에 생성되며, 원격 프로시저 호출(RPC)의 호출자(Caller) 역할을 수행한다.

로봇 시스템에서 서비스 클라이언트는 다른 노드가 제공하는 기능을 요청하는 소비자(Consumer) 역할을 담당한다. 경로 계획 요청, 센서 보정 실행, 매개변수 조회 등의 작업을 수행할 때 서비스 클라이언트를 통해 해당 기능을 호스팅하는 노드에 요청을 전달한다.

2. rclcpp 기반 서비스 클라이언트 생성

2.1 기본 생성 구조

rclcpp에서 서비스 클라이언트는 rclcpp::Node 클래스의 create_client<>() 템플릿 메서드를 통해 생성된다.

auto client = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

이 메서드는 서비스 타입과 서비스 이름을 매개변수로 받으며, rclcpp::Client<ServiceT> 타입의 공유 포인터를 반환한다. 선택적으로 QoS 프로파일과 콜백 그룹을 지정할 수 있다.

2.2 서비스 가용성 확인

서비스 클라이언트가 요청을 전송하기 전에, 대상 서비스 서버가 네트워크에 존재하고 활성 상태인지 확인하는 것이 필수적이다. ROS2는 이를 위한 두 가지 메서드를 제공한다.

// 서비스 서버의 현재 가용성을 비동기적으로 확인
bool ready = client->service_is_ready();

// 서비스 서버가 활성화될 때까지 지정된 시간 동안 대기
bool found = client->wait_for_service(std::chrono::seconds(5));

wait_for_service() 메서드는 DDS 검색(Discovery) 프로토콜을 통해 해당 서비스 이름으로 등록된 서버가 발견될 때까지 호출 스레드를 차단(Block)한다. 지정된 대기 시간 내에 서버가 발견되지 않으면 false를 반환한다.

3. 비동기 서비스 호출

3.1 async_send_request 메서드

rclcpp에서 서비스 호출은 async_send_request() 메서드를 통해 비동기적으로 수행된다. 이 메서드는 요청 객체를 인자로 받아 DDS 요청 토픽을 통해 서버에 전달하고, std::shared_future 객체를 반환한다.

auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = 5;
request->b = 3;

auto future = client->async_send_request(request);

반환된 future 객체를 통해 응답의 도착을 비동기적으로 감지할 수 있다.

3.2 콜백 기반 응답 처리

async_send_request() 메서드는 선택적으로 응답 수신 시 호출될 콜백 함수를 두 번째 인자로 받을 수 있다.

auto future = client->async_send_request(request,
    [this](rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedFuture%20response_future) {
        auto response = response_future.get();
        RCLCPP_INFO(this->get_logger(), "Result: %ld", response->sum);
    }
);

이 패턴을 사용하면 응답이 도착하는 시점에 자동으로 콜백이 호출되며, 별도의 대기 로직을 구현할 필요가 없다. 콜백은 실행자(Executor)의 이벤트 루프 내에서 호출되므로, spin()이 활성 상태이어야 한다.

3.3 Future 기반 응답 대기

콜백을 사용하지 않는 경우, future 객체의 get() 메서드를 통해 응답을 직접 대기하거나, rclcpp::spin_until_future_complete() 함수를 사용하여 실행자 기반의 대기를 수행할 수 있다.

auto result = rclcpp::spin_until_future_complete(node, future, std::chrono::seconds(10));
if (result == rclcpp::FutureReturnCode::SUCCESS) {
    auto response = future.get();
    RCLCPP_INFO(node->get_logger(), "Sum: %ld", response->sum);
} else if (result == rclcpp::FutureReturnCode::TIMEOUT) {
    RCLCPP_ERROR(node->get_logger(), "Service call timed out");
}

spin_until_future_complete() 함수는 응답이 도착할 때까지 실행자를 구동하므로, 응답 대기 중에도 다른 콜백(토픽 구독자, 타이머 등)이 정상적으로 처리된다.

4. rclpy 기반 서비스 클라이언트

Python 클라이언트 라이브러리인 rclpy에서 서비스 클라이언트는 다음과 같이 생성하고 사용한다.

self.client = self.create_client(AddTwoInts, 'add_two_ints')

# 서비스 서버 대기
while not self.client.wait_for_service(timeout_sec=1.0):
    self.get_logger().info('Waiting for service...')

# 비동기 요청 전송
request = AddTwoInts.Request()
request.a = 5
request.b = 3
future = self.client.call_async(request)
future.add_done_callback(self.response_callback)

rclpy에서는 call_async() 메서드를 통해 비동기 호출을 수행하며, add_done_callback() 메서드로 응답 처리 콜백을 등록한다.

5. DDS 계층에서의 클라이언트 매핑

5.1 내부 DDS 엔드포인트 구조

서비스 클라이언트는 DDS 미들웨어 계층에서 두 개의 DDS 엔드포인트로 매핑된다.

  1. 요청 발행용 DataWriter: <서비스_이름>/Request 토픽을 통해 요청 메시지를 전송한다.
  2. 응답 수신용 DataReader: <서비스_이름>/Response 토픽에서 서버의 응답 메시지를 수신한다.

5.2 요청-응답 대응 메커니즘

각 요청 메시지에는 클라이언트의 GUID(Globally Unique Identifier)와 시퀀스 번호(Sequence Number)가 포함된다. 응답 메시지에도 동일한 GUID와 시퀀스 번호가 첨부되어 반환되며, 클라이언트는 이를 통해 자신이 전송한 요청에 대한 정확한 응답을 식별한다. 동일 서비스에 복수의 클라이언트가 동시에 요청을 전송하더라도, 각 클라이언트는 자신의 요청에 대한 응답만을 정확히 수신할 수 있다.

6. 서비스 클라이언트의 생명주기

6.1 클라이언트 생성과 소멸

서비스 클라이언트 객체는 노드의 생명주기 동안 유지되며, 복수의 서비스 호출에 재사용된다. 클라이언트 객체가 소멸하면 DDS 계층의 엔드포인트가 해제되며, 진행 중인 미수신 응답에 대한 처리가 중단된다.

6.2 복수 클라이언트의 동시 운용

하나의 노드 내에 동일 서비스에 대한 복수의 클라이언트를 생성할 수 있으며, 각 클라이언트는 독립적인 시퀀스 번호를 관리한다. 또한, 상이한 서비스에 대한 복수의 클라이언트를 하나의 노드에서 동시에 운용하는 것이 일반적인 패턴이다.

7. 서비스 클라이언트 구현 시 고려사항

7.1 타임아웃 처리

서비스 서버가 비활성 상태이거나 요청 처리에 비정상적으로 긴 시간이 소요되는 경우, 클라이언트 측에서 적절한 타임아웃을 설정하여 무한 대기를 방지하여야 한다. spin_until_future_complete()의 타임아웃 매개변수를 활용하거나, 타이머 콜백을 통해 대기 시간을 모니터링하는 방식이 사용된다.

7.2 콜백 내 서비스 호출의 제약

단일 스레드 실행자 환경에서 콜백 함수 내부에서 spin_until_future_complete()를 호출하면 교착 상태(Deadlock)가 발생할 수 있다. 이는 응답 수신을 위해 실행자가 필요하나, 현재 콜백이 실행자 스레드를 점유하고 있어 응답 처리 콜백이 스케줄링되지 못하기 때문이다. 이 문제를 해결하기 위해서는 멀티 스레드 실행자를 사용하거나, 콜백 기반의 비동기 응답 처리 패턴을 적용하여야 한다.

7.3 서비스 호출의 재시도 전략

네트워크 불안정이나 서버의 일시적 장애로 인해 서비스 호출이 실패할 수 있다. 이 경우, 지수 백오프(Exponential Backoff) 전략을 적용한 재시도 로직을 구현하여 시스템의 견고성(Robustness)을 향상시킬 수 있다. 다만, ROS2는 서비스 계층에서 자동 재시도 메커니즘을 내장하고 있지 않으므로, 이는 응용 계층에서 명시적으로 구현하여야 한다.

8. 참고 문헌

  • Open Robotics, “ROS 2 Documentation: Humble Hawksbill,” https://docs.ros.org/en/humble/, 2022.
  • Open Robotics, “rclcpp API Reference,” https://docs.ros2.org/humble/api/rclcpp/, 2022.
  • OMG, “Data Distribution Service (DDS) Specification, Version 1.4,” Object Management Group, 2015.
  • Maruyama, Y., Kato, S., Azumi, T., “Exploring the Performance of ROS2,” Proceedings of the 13th International Conference on Embedded Software (EMSOFT), 2016.