이번 글에서는 ROS 2에서 Python으로 Service Client를 만드는 방법을 정리합니다.
ROS 2의 서비스는 기본적으로 다음 구조입니다.
Service Client → Request → Service Server
Service Client ← Response ← Service Server
즉, 클라이언트가 서버에 요청을 보내면 서버가 처리 결과를 응답하는 방식입니다.
이번 예제에서는 turtlesim에서 제공하는 /turtle1/teleport_absolute 서비스를 Python 코드로 호출하여 거북이의 위치를 이동시켜 보겠습니다.
1. 실행환경 준비
새로운 터미널을 실행하고 turtlesim_node를 실행합니다.
ros2 run turtlesim turtlesim_node

다른 터미널을 실행하여 노트북을 실행합니다.
jupyter notebook

Jupyter에서 새 Python3 노트북을 만들고, 예제 코드를 입력합니다.

2. 서비스 목록 확인
현재 실행 중인 서비스 목록과 타입을 확인합니다.
ros2 service list -t

출력 중에서 다음 항목을 찾을 수 있습니다.
/turtle1/teleport_absolute [turtlesim/srv/TeleportAbsolute]
여기서 중요한 정보는 두 가지입니다.
서비스 이름: /turtle1/teleport_absolute
서비스 타입: turtlesim/srv/TeleportAbsolute
이 서비스는 거북이를 특정 좌표와 방향으로 즉시 이동시키는 기능을 제공합니다.
3. 서비스 타입 import 하기
Python 코드에서 해당 서비스를 사용하려면 서비스 정의 클래스를 import 해야 합니다.
from turtlesim.srv import TeleportAbsolute
turtlesim.srv 패키지 안에 있는 TeleportAbsolute 서비스 정의를 가져오는 코드입니다.

4. rclpy 초기화하기
ROS 2 Python 기능을 사용하려면 먼저 rclpy를 초기화해야 합니다.
import rclpy as rp
rp.init()

여기서 rclpy는 ROS 2의 Python 클라이언트 라이브러리입니다.
여기서는 rclpy를 rp라는 별칭으로 import 하고 있습니다. 실무에서는 보통 아래처럼 그대로 쓰는 경우도 많습니다.
import rclpy
rclpy.init()
두 방식 모두 가능합니다.
5. 노드 생성하기
서비스 클라이언트도 ROS 2 노드 안에서 동작합니다. 따라서 먼저 노드를 생성합니다.
test_node = rp.create_node('client_test')
이 코드는 client_test라는 이름의 ROS 2 노드를 만듭니다.
create_node() 함수는 내부적으로 Node 객체를 반환합니다. 그래서 test_node 변수 뒤에 점을 찍으면 노드가 제공하는 여러 기능을 사용할 수 있습니다.
예를 들면 다음과 같은 함수들이 있습니다.
create_client()
create_publisher()
create_subscription()
create_service()
create_timer()
declare_parameter()
여기서는 이 중 create_client()를 사용합니다.

6. 서비스 이름 변수 만들기
서비스 이름이 길기 때문에 변수에 저장해 둡니다.
service_name = '/turtle1/teleport_absolute'
이렇게 하면 이후 코드에서 서비스 이름을 반복해서 직접 입력하지 않아도 됩니다.

7. Service Client 생성하기
이제 실제 서비스 클라이언트를 만듭니다.
cli = test_node.create_client(TeleportAbsolute, service_name)

이 코드는 다음과 같은 의미입니다.
test_node 노드에서
TeleportAbsolute 타입의
/turtle1/teleport_absolute 서비스에 연결할 수 있는
클라이언트 객체 cli를 만든다
cli는 Service Client 객체입니다. 이후 cli.call_async()를 사용해서 서비스 요청을 보낼 수 있습니다.
8. Request 객체 생성하기
TeleportAbsolute 서비스는 요청 데이터로 다음 값을 사용합니다.
float32 x
float32 y
float32 theta
즉, 거북이의 이동 위치와 방향을 지정해야 합니다.
Request 객체는 다음처럼 생성합니다.
req = TeleportAbsolute.Request()

이제 req 안에 x, y, theta 값을 넣을 수 있습니다.
req.x = 3.0
req.y = 5.0
req.theta = 0.0

각 값의 의미는 다음과 같습니다.
| x | 이동할 x 좌표 |
| y | 이동할 y 좌표 |
| theta | 거북이의 방향 각도 |
| 단위 | theta는 라디안 기준 |
9. 서비스 요청 보내기
서비스 요청은 call_async() 함수로 보냅니다.
cli.call_async(req)
rp.spin_once(test_node)

call_async()는 이름 그대로 비동기 방식입니다.
요청을 보낸 뒤 바로 결과가 올 수도 있고, 조금 기다려야 할 수도 있습니다.
rp.spin_once(test_node)는 노드를 한 번 실행하면서 응답 처리를 진행하게 합니다.
간단한 테스트에서는 위 코드만으로도 거북이가 이동하는 것을 볼 수 있습니다.

10. 서비스 서버가 실행 중인지 기다리기
서비스 클라이언트는 서비스 서버가 실행 중이어야 동작합니다.
만약 /turtle1/teleport_absolute 서비스를 제공하는 turtlesim_node가 실행되지 않았다면 요청이 실패하거나 응답이 오지 않습니다.
그래서 보통은 다음처럼 wait_for_service()를 사용합니다.
while not cli.wait_for_service(timeout_sec=1.0):
print('waiting for service')
이 코드는 서비스 서버가 준비될 때까지 1초마다 확인합니다.
서비스가 준비되면 while문을 빠져나가고, 그다음 요청을 보냅니다.

11. Future 객체로 응답 확인하기
call_async()는 요청 결과를 바로 반환하지 않습니다. 대신 Future 객체를 반환합니다.
future = cli.call_async(req)

Future는 “아직 완료되지 않았지만 나중에 결과가 들어올 작업”을 의미합니다.
응답이 완료될 때까지 확인하려면 다음처럼 작성합니다.
while not future.done():
rp.spin_once(test_node)
print(future.done(), future.result())
서비스 응답이 완료되면 future.done()이 True가 되고, future.result()에서 응답 객체를 확인할 수 있습니다.

12. 전체 예제 코드
import rclpy as rp
from turtlesim.srv import TeleportAbsolute
rp.init()
test_node = rp.create_node('client_test')
service_name = '/turtle1/teleport_absolute'
cli = test_node.create_client(TeleportAbsolute, service_name)
while not cli.wait_for_service(timeout_sec=1.0):
print('waiting for service')
req = TeleportAbsolute.Request()
req.x = 3.0
req.y = 5.0
req.theta = 0.0
future = cli.call_async(req)
while not future.done():
rp.spin_once(test_node)
print(future.done(), future.result())
13. Service와 Topic의 차이
ROS 2에서 Topic과 Service는 자주 헷갈립니다.
| 통신 방식 | 계속 발행/구독 | 요청/응답 |
| 예시 | 센서 데이터, 카메라 영상, 위치 정보 | 좌표 이동, 모드 변경, 설정 변경 |
| 방향 | Publisher → Subscriber | Client ↔ Server |
| 응답 여부 | 보통 없음 | 있음 |
turtlesim의 /turtle1/teleport_absolute는 명령을 보내고 결과를 받는 구조이므로 Topic보다 Service에 적합합니다.
14. 왜 call_async를 사용할까?
ROS 2 Python에서는 서비스 요청을 비동기로 처리하는 방식이 일반적입니다.
만약 동기 방식으로 오래 기다리면 노드 전체가 멈춘 것처럼 보일 수 있습니다.
그래서 call_async()로 요청을 보내고, Future 객체를 통해 결과가 완료되었는지 확인합니다.
이 구조는 실제 로봇 제어에서 중요합니다.
예를 들어 드론이나 로봇팔에서 서비스 요청 중에도 센서 데이터, 상태 메시지, 안전 조건 등을 계속 처리해야 하기 때문입니다.
15. wait_for_service가 필요한 이유
서비스 클라이언트가 먼저 실행되고 서비스 서버가 아직 실행되지 않은 경우가 많습니다.
이때 바로 call_async()를 호출하면 요청 대상이 없기 때문에 정상 동작하지 않을 수 있습니다.
따라서 실무 코드는 보통 다음 구조를 넣습니다.
while not cli.wait_for_service(timeout_sec=1.0):
print('service not available, waiting...')
이 코드는 서비스 서버가 준비될 때까지 안전하게 기다리는 역할을 합니다.
16. 요약
핵심 코드는 다음 네 줄입니다.
cli = test_node.create_client(TeleportAbsolute, service_name)
req = TeleportAbsolute.Request()
future = cli.call_async(req)
rp.spin_once(test_node)
서비스 클라이언트 코드는 다음 순서로 이해하시면 됩니다.
1. 서비스 타입 import
2. rclpy 초기화
3. 노드 생성
4. 서비스 이름 지정
5. create_client()로 클라이언트 생성
6. wait_for_service()로 서버 준비 확인
7. Request 객체 생성
8. 요청 데이터 입력
9. call_async()로 요청 전송
10. spin_once()로 응답 처리
핵심은 create_client()와 call_async()입니다.
create_client()는 서비스와 연결할 클라이언트 객체를 만들고, call_async()는 실제 요청을 보냅니다.
서비스 클라이언트는 로봇 제어에서 매우 자주 사용됩니다.
예를 들어 다음 기능에 활용할 수 있습니다.
로봇팔 동작 시작 요청
드론 이륙/착륙 요청
카메라 캡처 요청
그리퍼 열기/닫기 요청
모터 제어 모드 변경
파라미터 재설정
즉, Service는 “한 번 명령하고 결과를 받는 기능”에 적합합니다.
'강좌 > ROS2' 카테고리의 다른 글
| ROS2 사용자 정의 메세지 만들기 #1 (0) | 2026.05.09 |
|---|---|
| ROS2 패키지 만들고 토픽 다루기 (0) | 2026.05.08 |
| Python에서 ROS2 토픽 발행 (0) | 2026.05.07 |
| Python에서 ROS2 토픽 구독 (0) | 2026.05.07 |
| ROS2 파라미터 (0) | 2026.05.06 |
