1261.31 서비스 통신의 동기적 호출과 비동기적 호출

1. 동기적 호출과 비동기적 호출의 개념적 구분

서비스 통신에서 동기적 호출(Synchronous Call)과 비동기적 호출(Asynchronous Call)은 클라이언트가 서버의 응답을 대기하는 방식에 근본적인 차이를 보인다. 동기적 호출에서는 클라이언트가 요청을 전송한 후 응답이 반환될 때까지 호출 스레드의 실행을 중단(Block)하며, 비동기적 호출에서는 요청 전송 직후 제어가 호출자에게 즉시 반환되어 응답 도착과 무관하게 후속 작업을 계속 수행할 수 있다.

ROS2에서는 서비스 통신의 내부 구현이 DDS(Data Distribution Service) 기반의 비동기 메시지 교환으로 이루어져 있으나, 클라이언트 라이브러리(rclcpp, rclpy)에서 동기적 대기와 비동기적 대기를 모두 지원하는 인터페이스를 제공한다.

2. ROS2에서의 비동기적 서비스 호출

2.1 기본 비동기 호출 메커니즘

ROS2의 서비스 클라이언트 API는 비동기적 호출을 기본 인터페이스로 제공한다. rclcpp에서의 async_send_request() 메서드와 rclpy에서의 call_async() 메서드가 이에 해당한다.

// rclcpp 비동기 호출
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = 5;
request->b = 3;
auto future = client->async_send_request(request);
# rclpy 비동기 호출
request = AddTwoInts.Request()
request.a = 5
request.b = 3
future = self.client.call_async(request)

비동기 호출은 future 객체를 반환하며, 이 객체는 응답이 도착하면 완료 상태(Fulfilled)로 전이한다.

2.2 콜백 기반 응답 처리

비동기 호출의 가장 일반적인 활용 패턴은 응답 수신 시 호출되는 콜백 함수를 등록하는 것이다.

// rclcpp 콜백 기반 비동기 호출
auto future = client->async_send_request(request,
    [this](rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedFuture%20future) {
        auto response = future.get();
        RCLCPP_INFO(this->get_logger(), "Result: %ld", response->sum);
    }
);
# rclpy 콜백 기반 비동기 호출
future = self.client.call_async(request)
future.add_done_callback(self.response_callback)

def response_callback(self, future):
    response = future.result()
    self.get_logger().info(f'Result: {response.sum}')

콜백 함수는 실행자(Executor)의 이벤트 루프 내에서 호출되므로, 다른 콜백(토픽 구독자, 타이머 등)과 함께 자연스럽게 스케줄링된다.

2.3 비동기 호출의 장점

비동기적 서비스 호출은 다음과 같은 이점을 제공한다.

  • 실행자 이벤트 루프의 비차단: 호출 스레드가 차단되지 않으므로, 서비스 응답 대기 중에도 토픽 콜백, 타이머 콜백 등이 정상적으로 처리된다.
  • 교착 상태 방지: 단일 스레드 실행자 환경에서도 교착 상태(Deadlock)의 위험이 없다.
  • 병렬 요청 처리: 복수의 서비스 호출을 동시에 발행하고, 각 응답을 독립적으로 처리할 수 있다.

3. ROS2에서의 동기적 서비스 호출

3.1 spin_until_future_complete를 통한 동기적 대기

rclcpp에서는 rclcpp::spin_until_future_complete() 함수를 사용하여 비동기 호출을 동기적으로 대기할 수 있다.

auto future = client->async_send_request(request);
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");
} else {
    RCLCPP_ERROR(node->get_logger(), "Service call interrupted");
}

이 함수는 future가 완료될 때까지 실행자를 구동하며 호출 스레드를 차단한다. 타임아웃 매개변수를 통해 최대 대기 시간을 설정할 수 있다. 대기 중에도 실행자가 구동되므로, 다른 콜백은 정상적으로 처리된다.

반환되는 FutureReturnCode는 다음의 세 가지 값을 가질 수 있다.

반환 코드의미
SUCCESS응답이 정상적으로 수신되었다
TIMEOUT지정된 대기 시간이 초과되었다
INTERRUPTED실행이 외부적으로 중단되었다

3.2 rclpy의 동기적 호출

rclpy에서는 서비스의 동기적 호출이 더욱 간결하게 지원된다. rclpy Humble 이후 버전에서는 call() 메서드를 통해 직접 동기적 호출을 수행할 수 있다.

# rclpy 동기적 호출 (별도 스레드에서 실행 권장)
response = self.client.call(request)
self.get_logger().info(f'Result: {response.sum}')

다만, call() 메서드는 내부적으로 별도의 실행자 컨텍스트를 생성하여 응답을 대기하므로, 이미 실행자가 구동 중인 콜백 내에서 호출하면 교착 상태를 유발할 수 있다.

4. 동기적 호출과 비동기적 호출의 비교

비교 항목동기적 호출비동기적 호출
호출 스레드 차단응답 도착 시까지 차단즉시 반환
코드 가독성순차적 흐름이 명시적으로 표현됨콜백 또는 Future 처리 로직이 필요
교착 상태 위험단일 스레드 실행자 + 콜백 내 호출 시 위험교착 상태 위험 없음
동시 요청 처리하나의 요청 완료 후 다음 요청 가능복수의 요청 동시 발행 가능
시스템 응답성대기 중 다른 작업 차단 가능대기 중 다른 작업 정상 수행
적합한 사용 환경초기화 루틴, 단순 스크립트실시간 로봇 제어 루프, 이벤트 기반 시스템

5. 단일 스레드 실행자에서의 교착 상태 문제

5.1 교착 발생 메커니즘

단일 스레드 실행자(SingleThreadedExecutor)를 사용하는 환경에서 콜백 함수 내부에서 동기적 서비스 호출을 수행하면 교착 상태가 발생한다. 그 메커니즘은 다음과 같다.

  1. 실행자의 단일 스레드가 현재 콜백(예: 타이머 콜백)을 실행 중이다.
  2. 콜백 내부에서 spin_until_future_complete()를 호출하여 서비스 응답을 대기한다.
  3. 서비스 응답은 DDS를 통해 도착하나, 응답을 처리하기 위한 콜백이 실행자에 의해 스케줄링되어야 한다.
  4. 실행자의 스레드가 현재 콜백의 완료를 대기하고 있어 응답 처리 콜백을 실행할 수 없다.
  5. 결과적으로 현재 콜백은 응답을 무한히 대기하고, 응답 처리는 현재 콜백의 완료를 무한히 대기하여 교착 상태에 진입한다.

5.2 해결 전략

이 교착 상태를 해결하기 위한 전략은 다음과 같다.

  • 비동기 호출 패턴 사용: 콜백 내부에서는 반드시 async_send_request()와 콜백 기반 응답 처리를 사용한다.
  • 멀티 스레드 실행자 적용: MultiThreadedExecutor를 사용하여 복수의 스레드가 콜백을 병렬로 처리하도록 한다.
  • 콜백 그룹 분리: 서비스 클라이언트를 재진입 가능 콜백 그룹(Reentrant Callback Group)에 할당하여 응답 수신 콜백이 독립적으로 실행되도록 한다.

6. 로봇 시스템에서의 적용 지침

6.1 초기화 단계에서의 동기적 호출

노드의 초기화 과정에서 설정 매개변수를 조회하거나, 초기 상태를 서비스 호출로 획득하는 경우에는 동기적 호출이 적합하다. 이 시점에서는 실행자의 이벤트 루프가 아직 구동되지 않거나, 다른 콜백의 처리가 요구되지 않기 때문이다.

6.2 실행 루프 내에서의 비동기적 호출

로봇의 주행 제어, 임무 실행 등 실행자가 활성 상태인 동안에는 비동기적 호출을 사용하여야 한다. 이를 통해 서비스 응답 대기 중에도 센서 데이터 수신, 제어 명령 발행 등 다른 콜백의 정상적인 처리가 보장된다.

6.3 복합 서비스 호출 체인

하나의 서비스 응답을 기반으로 후속 서비스를 호출하는 체인(Chain) 패턴에서는 콜백 기반 비동기 호출을 계단식으로 연결하거나, C++20의 코루틴(Coroutine) 또는 Python의 asyncio를 활용하여 비동기 호출의 순차적 흐름을 표현할 수 있다.

7. 참고 문헌

  • 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.
  • Tanenbaum, A. S., Van Steen, M., Distributed Systems: Principles and Paradigms, 2nd ed., Pearson, 2007.
  • Maruyama, Y., Kato, S., Azumi, T., “Exploring the Performance of ROS2,” Proceedings of the 13th International Conference on Embedded Software (EMSOFT), 2016.