[ROS2 Mastery 7편] ROS2 Service 프로그래밍 – 거북이 펜 색상 자동 변경 서비스 가이드
안녕하세요! 로봇 공학의 꿈을 안고 자율주행과 시스템 제어를 깊이 있게 연구하고 계신 대학생 및 연구원 여러분. 쿼드(QUAD) 드론연구소의 자료를 바탕으로 진행되는 ‘ROS2 속성 마스터 과정’의 마지막 연재, 대망의 7편에 오신 것을 진심으로 환영합니다.
지난 6편에서는 토픽(Topic)의 발행(Publish)과 구독(Subscribe)을 하나의 노드에 결합하여, 로봇 스스로 벽을 인지하고 회피하는 ‘자율주행 거북이’를 구현해 보았습니다. 토픽 통신이 로봇의 센서 데이터 스트리밍이나 지속적인 속도 명령처럼 ‘연속적이고 비동기적인 단방향 통신’에 적합하다면, 오늘 우리가 배울 서비스(Service)는 이와는 완전히 다른 특성을 가집니다.
이번 7편에서는 클라이언트(Client)가 요청(Request)할 때만 서버(Server)가 응답(Response)하는 ‘동기식 양방향 통신’인 ROS2 Service에 대해 깊이 있게 알아보고, 이전 시간에 만든 자율주행 컨트롤러에 거북이가 화면의 위치에 따라 이동 경로의 펜 색상을 자동으로 변경(왼쪽은 빨강, 오른쪽은 초록)하는 서비스 클라이언트 기능을 추가해 보도록 하겠습니다.

1. ROS2 Service의 개념과 구조 이해하기
로봇 시스템을 연구하다 보면, 지속적인 데이터 스트리밍이 아니라 “지금 카메라로 사진을 한 장 찍어서 보내줘”, “지금 위치에서 그리퍼(로봇팔)를 열어줘”와 같이 일회성 명령과 결과 확인이 필요한 경우가 빈번하게 발생합니다. 이때 사용되는 통신 방식이 바로 Service(서비스)입니다.
1.1 서비스 통신의 특징 (Client – Server 구조)
- 서비스 서버(Server): 외부의 요청을 대기하고 있다가, 요청이 들어오면 정의된 작업을 수행한 후 그 결과(응답)를 반환합니다.
- 서비스 클라이언트(Client): 필요할 때 서버에 작업을 요청(Request)하고, 처리가 완료되어 응답(Response)이 올 때까지 기다리거나 비동기적으로 결과를 처리합니다.
1.2 서비스 메시지 명세 파일 (.srv) 분석
서비스가 어떻게 구성되어 있는지 확인하기 위해, 쿼드 드론연구소 교재에 나온 예시인 add_two_ints 서비스를 잠시 살펴보겠습니다. 이 서비스는 두 수를 입력하면 더해서 결과값을 반환합니다. 서비스 통신을 위해서는 서버와 클라이언트가 서로 주고받는 데이터의 명세서인 .srv 파일이 필요합니다.
터미널에서 ros2 interface show example_interfaces/srv/AddTwoInts 명령을 실행하면 다음과 같은 구조를 볼 수 있습니다.
int64 a
int64 b
---
int64 sum
여기서 중간에 있는 — 구분자가 매우 중요합니다. 구분자를 기준으로 위쪽은 클라이언트가 서버로 보내는 ‘요청(Request)’ 메시지이고, 아래쪽은 서버가 클라이언트로 돌려주는 ‘응답(Response)’ 메시지를 의미합니다.
2. 목표 서비스 분석: /turtle1/set_pen
오늘 우리가 사용할 서비스는 Turtlesim이 기본적으로 제공하는 펜 설정 서비스입니다. 코딩에 앞서 항상 하던 대로 CLI 도구를 이용해 제어 대상을 분석해 보겠습니다.
터미널을 열고 turtlesim_node를 실행한 상태에서 다음 명령어를 입력해 보세요.
ros2 service list
출력되는 서비스 목록 중에 거북이의 펜과 관련된 /turtle1/set_pen 이라는 서비스를 찾을 수 있습니다. 이 서비스의 데이터 타입을 알아봅시다.
ros2 service type /turtle1/set_pen
타입이 turtlesim/srv/SetPen으로 출력됩니다. 이제 대망의 메시지 구조를 들여다볼 차례입니다.
ros2 interface show turtlesim/srv/SetPen
uint8 r
uint8 g
uint8 b
uint8 width
uint8 off
---
분석 결과가 흥미롭습니다. --- 위쪽의 요청(Request) 영역에는 RGB 색상 값(r, g, b)과 펜의 굵기(width), 그리고 펜을 끄고 켤 수 있는 플래그(off)가 있습니다. 반면 --- 아래쪽의 응답(Response) 영역은 비어 있습니다. 이는 클라이언트가 펜 색상 변경을 요청하면, 서버(Turtlesim)가 색상을 변경한 뒤 별도의 결과 데이터를 반환하지 않고 처리 완료만 알린다는 의미입니다.
목표가 명확해졌습니다! 우리는 자율주행 노드 안에 SetPen 타입의 서비스 클라이언트를 만들고, 거북이의 x 좌표 위치에 따라 RGB 값을 담은 Request 객체를 서버로 전송하면 됩니다.
3. 거북이 펜 색상 자동 변경 로직 설계
우리는 6편에서 작성했던 turtle_controller.py 자율주행 코드를 업그레이드할 것입니다.
[자동 변경 알고리즘 로직]
- 위치 파악:
pose_callback함수에서 거북이의 실시간 x 좌표를 지속적으로 확인합니다. - 경계선 설정: Turtlesim 화면의 가로 길이는 약 11.0입니다. 따라서 절반인
x = 5.5를 경계선으로 설정합니다. - 색상 판단: 거북이가 화면 왼쪽(
x < 5.5)에 있을 때는 빨간색(r:255, g:0, b:0), 오른쪽(x >= 5.5)에 있을 때는 초록색(r:0, g:255, b:0)으로 변경합니다. - [연구원 핵심 팁] 상태 추적 방어 코드: 60Hz로 수신되는
pose토픽마다 서비스 호출을 날리면 1초에 60번씩 무의미한 통신 부하가 발생합니다. 따라서 이전 상태(왼쪽/오른쪽)를 기억하는 변수를 만들어, 경계를 넘어가는 ‘순간’에만 1회성으로 서비스 호출이 이루어지도록 설계해야 합니다.
4. 자율주행 노드 코드 수정: turtle_controller.py
기존에 작업하던 ~/ros2_ws/src/my_robot_controller/my_robot_controller/turtle_controller.py 파일을 에디터로 열어 다음과 같이 코드를 전면 수정(추가)합니다.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from turtlesim.msg import Pose
from turtlesim.srv import SetPen # 1. 새롭게 사용할 서비스 타입을 import 합니다. [2]
class TurtleControllerNode(Node):
def __init__(self):
super().__init__('turtle_controller')
# [기존 로직] 제어 명령 Publisher와 위치 정보 Subscriber
self.cmd_vel_publisher_ = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
self.pose_subscriber_ = self.create_subscription(Pose, '/turtle1/pose', self.pose_callback, 10)
# 2. 펜 색상을 변경할 '서비스 클라이언트' 생성
self.set_pen_client_ = self.create_client(SetPen, '/turtle1/set_pen')
# 3. 무한 서비스 호출을 방지하기 위한 상태 추적 플래그 초기화
self.is_on_left_ = True
self.get_logger().info("거북이 자율주행 및 펜 색상 제어 노드가 시작되었습니다!")
# 4. 서비스 호출을 위한 헬퍼(Helper) 함수
def call_set_pen_service(self, r, g, b, width, off):
# 서버가 통신 준비가 될 때까지 1초 단위로 대기
while not self.set_pen_client_.wait_for_service(1.0):
self.get_logger().warn("서비스 서버를 기다리는 중입니다...")
# Request 객체 생성 및 인자 할당
request = SetPen.Request()
request.r = r
request.g = g
request.b = b
request.width = width
request.off = off
# 비동기식(Async)으로 서비스 호출 후 콜백 등록
future = self.set_pen_client_.call_async(request)
future.add_done_callback(self.future_callback)
# 서비스 응답이 도착했을 때 처리할 콜백 함수
def future_callback(self, future):
try:
response = future.result()
self.get_logger().info("펜 색상 변경이 서버에 성공적으로 적용되었습니다.")
except Exception as e:
self.get_logger().error(f"서비스 호출 중 오류 발생: {e}")
# 위치 수신 시 반복 실행되는 메인 로직 콜백
def pose_callback(self, pose: Pose):
cmd = Twist()
# [기존 로직] 자율주행 (벽 회피)
if pose.x > 9.0 or pose.x < 2.0 or pose.y > 9.0 or pose.y < 2.0:
cmd.linear.x = 1.0
cmd.angular.z = 1.5
else:
cmd.linear.x = 5.0
cmd.angular.z = 0.0
self.cmd_vel_publisher_.publish(cmd)
# 5. [새로운 로직] 펜 색상 자동 변경 조건문 [2]
# 거북이가 화면의 중앙(x=5.5)을 기준으로 왼쪽에 진입했을 때 (최초 1회만 실행)
if pose.x < 5.5 and not self.is_on_left_:
self.is_on_left_ = True
self.get_logger().info("왼쪽 영역 진입: 펜 색상을 빨간색으로 변경합니다.")
self.call_set_pen_service(255, 0, 0, 3, 0) # 빨간색 요청
# 거북이가 화면 중앙을 기준으로 오른쪽에 진입했을 때 (최초 1회만 실행)
elif pose.x >= 5.5 and self.is_on_left_:
self.is_on_left_ = False
self.get_logger().info("오른쪽 영역 진입: 펜 색상을 초록색으로 변경합니다.")
self.call_set_pen_service(0, 255, 0, 3, 0) # 초록색 요청
def main(args=None):
rclpy.init(args=args)
node = TurtleControllerNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
[코드 심층 분석: 연구원을 위한 아키텍처 가이드]
연구원 여러분께서 주의 깊게 보셔야 할 부분은 서비스 클라이언트의 비동기 호출(Asynchronous Call) 구조입니다.
- create_client(): 토픽 발행자(Publisher)를 만들 때와 매우 유사하게, 사용할 타입(
SetPen)과 서비스명(/turtle1/set_pen)을 입력하여 클라이언트를 생성합니다. - wait_for_service(): 분산 시스템인 ROS2에서는 서버 노드가 클라이언트보다 늦게 켜질 수도 있습니다. 이 함수를 사용하면 서버가 네트워크상에 발견될 때까지 안전하게 대기할 수 있습니다.
- call_async(request): 파이썬에서 서비스 요청을 보낼 때 가장 권장되는 비동기 방식입니다. 자율주행 노드는 초당 수십 번씩 위치 데이터를 받아 벽을 피해야 합니다(
pose_callback). 만약 여기서 동기식(Sync)으로 서비스 응답이 올 때까지 프로그램 실행을 멈추고 기다린다면(Block), 그 찰나의 순간 동안 거북이는 제어를 잃고 벽에 부딪혀 버릴 수 있습니다. 따라서call_async를 통해 “요청을 던져놓고 나는 내 할 일(자율주행)을 계속한다”라는 비동기 이벤트를 발생시키고,add_done_callback()을 통해 응답이 오면 별도로 처리하도록 설계한 것입니다.
5. 빌드 및 대망의 최종 실행
로직 설계와 코드 작성이 모두 완료되었습니다. 노드 이름이나 진입점은 6편과 동일하므로 setup.py는 수정할 필요가 없습니다. 이제 작업 공간을 빌드하고 실행하여 우리가 만든 시스템이 완벽하게 동작하는지 확인해 보겠습니다.
# 작업 공간 루트로 이동하여 패키지 빌드
cd ~/ros2_ws
colcon build --symlink-install
[실행 단계]
- 첫 번째 터미널 (Turtlesim 서버 실행):
- 두 번째 터미널 (자율주행 클라이언트 노드 실행): 새 터미널을 열고 오버레이를 소싱한 뒤 노드를 실행합니다.
실행 즉시 거북이가 벽을 향해 자율주행을 시작합니다. 그리고 화면 정중앙의 보이지 않는 선(x = 5.5)을 넘나들 때마다 터미널에는 변경 완료 로그가 뜨며, 거북이 꽁무니에서 나오는 펜의 색상이 왼쪽에서는 강렬한 빨간색, 오른쪽에서는 선명한 초록색으로 실시간으로 변환되는 것을 감상하실 수 있습니다.
단순히 모터를 제어하는 토픽 통신과 상태를 설정하는 서비스 통신이 하나의 프로그램 안에서 완벽한 조화를 이루어 작동하는 훌륭한 시스템이 완성되었습니다!
마무리하며
이것으로 쿼드 드론연구소의 ROS2 속성 마스터 과정 (Intermediate) 강좌의 모든 튜토리얼을 성공적으로 마치셨습니다. 수고 많으셨습니다!
이번 연재를 통해 여러분은 작업 공간(Workspace) 구성이라는 기초부터 시작해, 노드(Node) 프로그래밍, 토픽(Topic)을 통한 센서 및 모터 제어, 그리고 서비스(Service)를 통한 시스템 상태 조작까지 ROS2의 가장 핵심적인 뼈대를 모두 마스터하셨습니다. 나아가 이를 시각화하여 디버깅하는 RQT의 활용법까지 익히셨으니, 이제 어떤 오픈소스 로봇 패키지를 접하더라도 그 내부 통신 구조를 꿰뚫어 보실 수 있는 탄탄한 기본기를 갖추게 되었습니다.
여기서 멈추지 마시고 실제 라이다(LiDAR) 센서나 카메라 영상 데이터를 구독(Subscribe)하여 더 복잡한 알고리즘을 판단하고 제어하는 심화 연구로 나아가시길 바랍니다. 대학생 및 연구원 여러분의 앞날에 눈부신 성취가 함께하기를 진심으로 응원합니다. 그동안 연재를 구독해 주셔서 감사합니다!
YouTube Tutorial

작성자: 에이든(Aiden), 쿼드(QUAD) 드론연구소 마케팅팀
기고일: 2026.05.30
