# 비동기 서비스 호출 이해

#### 비동기 서비스 호출 개요

ROS2에서 서비스는 요청-응답 방식으로 동작한다. 일반적인 서비스 호출은 요청을 보내고 서버로부터 응답을 받을 때까지 대기하는 방식이다. 이는 호출된 서비스가 완료되기까지의 시간이 길어질 경우 전체 시스템이 지연될 수 있다는 단점이 있다. 이를 해결하기 위해 비동기 서비스 호출 방식이 사용되며, 이 방식은 요청을 보내고 결과를 기다리지 않고, 응답이 올 때 콜백 함수를 통해 처리하는 방식으로 설계된다.

비동기 호출을 활용하면 서비스 호출이 완료될 때까지 대기하지 않고, 다른 작업을 병렬로 처리할 수 있어, 실시간 시스템에서 매우 중요한 성능 최적화 방법 중 하나이다.

#### 비동기 서비스 호출 흐름

비동기 서비스 호출의 일반적인 흐름은 다음과 같다:

1. 클라이언트가 서비스 요청을 서버에 전송.
2. 클라이언트는 응답을 기다리지 않고 다른 작업을 수행.
3. 서버가 요청을 처리한 후 응답을 준비.
4. 클라이언트가 응답을 수신하면 지정된 콜백 함수를 통해 응답을 처리.

Mermaid를 사용하여 비동기 서비스 호출의 흐름을 도식화하면 다음과 같다:

{% @mermaid/diagram content="sequenceDiagram
participant Client
participant Server

```
Client->>Server: 요청 전송
Client-->>Client: 응답 대기 없이 작업 계속 수행
Server->>Server: 요청 처리
Server-->>Client: 응답 전송
Client->>Client: 응답 수신 및 콜백 처리" %}
```

#### 비동기 서비스의 장점

비동기 호출 방식은 특히 다음과 같은 상황에서 유용하다:

* **긴 처리 시간**이 필요한 서비스 호출 시 시스템이 블로킹되지 않음.
* **병렬 작업 처리**가 가능하여 자원을 효율적으로 사용.
* **콜백 기반 응답 처리**를 통해 특정 작업 완료 후에 추가 작업을 연결.

#### Python을 사용한 비동기 서비스 구현 예제

Python에서는 `rclpy` 모듈을 사용하여 ROS2 비동기 서비스를 호출할 수 있다. 아래는 Python을 사용한 간단한 비동기 서비스 호출 예제이다:

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

class AsyncClient(Node):
    def __init__(self):
        super().__init__('async_service_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()
        self.request.a = 10
        self.request.b = 20
        self.future = self.client.call_async(self.request)
        self.future.add_done_callback(self.response_callback)

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

def main(args=None):
    rclpy.init(args=args)
    async_client = AsyncClient()
    rclpy.spin(async_client)
    async_client.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()
```

이 코드에서는 `call_async` 메서드를 사용하여 비동기적으로 서비스를 호출한다. 서비스 호출 후 `add_done_callback` 메서드를 통해 결과를 처리하는 콜백을 설정한다.

#### 비동기 호출을 위한 수학적 모델

비동기 호출의 성능을 수학적으로 모델링할 수 있다. 예를 들어, 호출 시간 $T\_h$와 서버 응답 시간 $T\_s$가 주어졌을 때, 비동기 호출에서 전체 지연 시간 $T\_d$는 다음과 같이 표현될 수 있다:

$$
T\_d = \max(T\_h, T\_s)
$$

여기서 호출 시간과 응답 시간의 상관 관계에 따라 비동기 호출의 효율성을 측정할 수 있다. 호출 시간이 짧아도 서버 응답 시간이 길다면, 비동기 호출은 클라이언트의 병목을 최소화하는 효과를 가져온다.

#### 비동기 서비스의 비동기 처리 특성

비동기 서비스 호출에서 중요한 부분은 **콜백 함수**의 적절한 사용이다. 콜백 함수는 서비스 응답을 받았을 때 호출되는 함수로, 응답이 처리되는 시점에 추가적인 작업을 수행하도록 설계된다. 콜백 함수가 필요한 이유는, ROS2 클라이언트가 서비스 요청 후 다른 작업을 계속 수행하기 때문이다.

ROS2의 비동기 서비스 호출에서 발생하는 콜백 흐름은 다음과 같은 구조로 나타낼 수 있다:

1. 클라이언트는 **요청 메시지**를 생성하여 서버에 보낸다.
2. 클라이언트는 \*\*미래 객체 (future object)\*\*를 통해 서비스 응답을 기다린다. 하지만 실제로 대기 상태에 있지는 않고, 그동안 다른 작업을 수행할 수 있다.
3. 서버가 응답을 완료하면, **콜백 함수**가 호출되어 응답을 처리하게 된다.

다음은 콜백 함수의 호출 시퀀스를 시각적으로 나타낸 것이다:

{% @mermaid/diagram content="graph TD;
요청생성 -->|요청 전송| 미래객체;
미래객체 -->|대기 없이 작업 진행| 다른작업;
응답도착 -->|콜백 호출| 응답처리;" %}

#### 비동기 호출에서의 예외 처리

비동기 호출에서는 예상치 못한 **네트워크 지연** 또는 **서버 오류**로 인해 응답이 지연되거나 실패할 수 있다. 이러한 상황을 처리하기 위해, **예외 처리** 메커니즘이 중요하다. 콜백 함수 안에서 응답이 성공적으로 도착했는지, 오류가 발생했는지 확인하는 과정이 필요하다.

Python의 비동기 호출에서는 `future.result()` 호출 시 발생하는 예외를 처리할 수 있으며, 만약 응답이 실패하거나 오류가 발생하면, 이를 처리할 수 있는 로직을 추가해야 한다. 다음은 예외 처리를 포함한 비동기 호출 예제이다:

```python
def response_callback(self, future):
    try:
        response = future.result()
        self.get_logger().info(f'Result: {response.sum}')
    except Exception as e:
        self.get_logger().error(f'Service call failed: {str(e)}')
```

이 코드에서는 `try-except` 블록을 통해 응답 결과를 처리하고, 오류 발생 시 로그로 오류 메시지를 기록한다.

#### QoS 설정과 비동기 서비스

ROS2에서는 **QoS(품질 서비스, Quality of Service)** 설정을 통해 네트워크 통신의 신뢰성을 제어할 수 있다. 비동기 서비스 호출에서 QoS 설정은 특히 중요하다. QoS 설정을 통해 다음과 같은 항목을 제어할 수 있다:

* **신뢰성 (Reliability)**: 메시지가 반드시 도착해야 하는지 여부를 결정한다.
* **지속성 (Durability)**: 서버가 과거의 응답을 클라이언트에게 유지하고 있는지 여부를 결정한다.

비동기 서비스에서 QoS를 설정하면 네트워크 상태나 서버 상태에 따라 서비스 응답의 신뢰성과 안정성을 강화할 수 있다. 예를 들어, 응답 메시지가 반드시 전달되어야 한다면, 신뢰성 QoS를 강화할 수 있다.

```python
qos_profile = QoSProfile(
    reliability=ReliabilityPolicy.RELIABLE,
    durability=DurabilityPolicy.TRANSIENT_LOCAL
)
client = self.create_client(AddTwoInts, 'add_two_ints', qos_profile)
```

위 예제에서는 `QoSProfile`을 사용하여 신뢰성 `RELIABLE`과 지속성 `TRANSIENT_LOCAL`을 설정하여, 서비스 응답이 꼭 전달되도록 보장한다.

#### 비동기 호출의 성능 평가

비동기 서비스 호출의 성능을 평가할 때, 주로 두 가지 요소를 고려한다:

1. **응답 시간 (Response Time)**: 요청을 보내고 서버에서 응답을 받을 때까지의 시간이다.
2. **작업 병렬성 (Concurrency)**: 클라이언트가 비동기적으로 얼마나 많은 작업을 동시에 처리할 수 있는지를 평가하는 지표이다.

응답 시간이 $T\_s$, 서비스 처리 시간이 $T\_p$, 그리고 요청 큐 대기 시간이 $T\_q$일 때, 비동기 서비스의 전체 지연 시간 $T\_d$는 다음과 같이 모델링할 수 있다:

$$
T\_d = T\_q + T\_p + T\_s
$$

위 식에서 각 변수는 네트워크 상태, 서버 부하, 클라이언트 작업의 병렬성에 따라 달라질 수 있다. 병목 현상을 줄이기 위해 요청 큐에서의 대기 시간 $T\_q$을 줄이는 방법도 중요한 최적화 방법 중 하나이다.

#### 비동기 서비스 호출의 최적화

비동기 서비스 호출의 성능을 최적화하는 방법에는 여러 가지가 있다. 그 중 대표적인 방법은 **서비스 호출 패턴을 최적화**하고, **비동기 작업의 병렬 처리를 개선**하는 것이다.

**1. 서비스 호출 패턴 최적화**

비동기 서비스 호출 패턴을 개선하기 위해서는 주로 다음과 같은 전략을 사용할 수 있다:

* **지연 최적화**: 서버에서 처리되는 시간을 줄이기 위해, 서비스 요청을 보다 효율적으로 처리할 수 있도록 서버 측에서 **스레드 처리**를 도입하거나, **서버 자원**을 최적화할 수 있다.
* **콜백 체인**: 서비스 호출이 연속적으로 이어지는 경우, 각각의 호출이 완료될 때까지 기다리지 않고, 비동기 방식으로 여러 서비스를 차례로 호출하는 **콜백 체인 (Callback Chain)** 패턴을 사용할 수 있다. 콜백 체인 패턴은 비동기 호출 간의 종속성을 줄이고, 여러 작업을 동시에 처리할 수 있는 장점이 있다.

콜백 체인을 시각화하면 다음과 같다:

{% @mermaid/diagram content="graph TD;
호출1 -->|비동기 처리| 호출2;
호출2 -->|비동기 처리| 호출3;
호출3 --> 응답처리;" %}

이 방식은 각 호출이 완료될 때마다 다음 호출을 이어서 실행하며, 대기 시간을 최소화한다.

**2. 병렬 처리 최적화**

병렬 처리를 개선하는 또 다른 방법은 클라이언트에서 \*\*스레드 풀 (Thread Pool)\*\*을 사용하여 여러 비동기 작업을 동시에 처리하는 것이다. 이 방식은 다중 서비스 호출을 병렬로 실행하여 응답 시간을 줄이는 데 유리한다.

Python에서 스레드 풀을 사용하여 비동기 서비스를 처리하는 예시는 다음과 같다:

```python
import concurrent.futures

def async_service_call(self):
    with concurrent.futures.ThreadPoolExecutor() as executor:
        future1 = executor.submit(self.client.call_async, self.request1)
        future2 = executor.submit(self.client.call_async, self.request2)
        result1 = future1.result()
        result2 = future2.result()
        self.get_logger().info(f'Result1: {result1.sum}, Result2: {result2.sum}')
```

위 예제에서는 `concurrent.futures.ThreadPoolExecutor`를 사용하여 두 개의 비동기 서비스 호출을 병렬로 처리한다. 이 방식은 클라이언트가 여러 서비스 요청을 동시에 처리해야 할 때 매우 유용하다.

**3. 데이터 구조 최적화**

비동기 서비스 호출에서 사용되는 데이터 구조도 중요한 최적화 대상이다. 비동기 요청과 응답을 효율적으로 관리하기 위해, 큐(queue)나 스택(stack)을 사용할 수 있으며, 이를 통해 요청 순서와 응답 처리를 보다 효율적으로 관리할 수 있다.

예를 들어, 여러 비동기 요청을 큐에 저장하고, 응답이 도착하는 순서에 맞춰 처리하는 방식은 다음과 같은 구조로 나타낼 수 있다:

{% @mermaid/diagram content="graph TD;
요청1 --> 큐;
요청2 --> 큐;
큐 -->|순서대로 처리| 응답1;
큐 --> 응답2;" %}

이 방식은 서비스 호출이 완료된 순서대로 응답을 처리할 수 있도록 도와주며, 특히 네트워크 지연이 변동하는 상황에서 유용하다.

#### 비동기 서비스 호출에서의 동적 관리

ROS2 비동기 서비스 호출에서는 **동적 리소스 관리**가 중요한 역할을 한다. 서비스 요청의 양이나 네트워크 상태에 따라 **동적으로 자원을 관리**하는 방식은 성능 최적화의 중요한 요소가 된다.

**1. 동적 요청 관리**

요청이 갑작스럽게 증가하는 상황에서, 클라이언트는 일정한 큐 크기를 설정하여 **동적 큐 관리**를 적용할 수 있다. 예를 들어, 큐가 가득 찼을 때 새로운 요청을 받지 않고, 기존 요청을 우선 처리하는 방식으로 동적 자원 관리가 이루어질 수 있다.

큐의 동적 관리를 도식화하면 다음과 같다:

{% @mermaid/diagram content="graph TD;
요청 -->|큐 가득 참| 큐;
큐 -- 요청 처리 --> 응답;
요청 -- 대기 --> 큐;" %}

**2. 서버 자원 관리**

서버 측에서는 서비스 호출을 효율적으로 처리하기 위해 동적 자원 관리가 필요하다. 서비스 요청이 갑작스럽게 몰릴 때, 서버는 동적으로 스레드를 할당하거나, 일정한 자원 제약 내에서 요청을 제한할 수 있다. 이를 통해 **자원 고갈**을 방지하고 서비스의 안정성을 유지할 수 있다.
