Skip to main content
Summary

지난 장에서 토픽은 로봇 안의 데이터 도로라고 했다. /scan에는 LiDAR 데이터가 계속 흐르고, /odom에는 로봇 위치 추정값이 계속 흐르며, /cmd_vel에는 속도 명령이 흘러간다. 이런 흐름은 마치 라디오 방송과 비슷하다. 발행자는 계속 말하고, 구독자는 필요할 때 듣는다.

그런데 로봇 시스템의 모든 대화가 방송처럼 흘러가야 하는 것은 아니다. 때로는 이렇게 묻고 싶다.

"지금 모터를 켜도 되나요?"

"센서를 한 번 리셋해 주세요."

"현재 지도를 파일로 저장했나요?"

이런 질문에는 명확한 대답이 필요하다. 성공했는지, 실패했는지, 실패했다면 이유가 무엇인지 알고 싶다. 이때 사용하는 ROS 2 통신 방식이 서비스다. 서비스는 한 노드가 요청을 보내고, 다른 노드가 그 요청을 처리한 뒤 응답을 돌려주는 구조다.

토픽이 로봇 안의 계속 흐르는 데이터라면, 서비스는 로봇에게 건네는 짧고 분명한 질문이다. 버튼을 눌렀을 때 "삑, 처리되었습니다"라고 답을 받는 느낌에 가깝다.

서비스로 요청과 응답 다루기

서비스는 질문과 대답이다

서비스 통신에는 두 역할이 있다. 요청을 처리할 준비를 하고 기다리는 쪽을 서비스 서버라고 하고, 요청을 보내는 쪽을 서비스 클라이언트라고 한다.

여기서 서버라는 말이 거창하게 들릴 수 있지만, 반드시 웹 서버나 데이터베이스 서버 같은 것을 떠올릴 필요는 없다. ROS 2에서 서비스 서버는 "누가 이 이름으로 요청하면 내가 처리해 줄게"라고 등록한 노드다. 예를 들어 모터 제어 노드가 /motor_power라는 서비스를 제공한다면, 다른 노드는 이 서비스를 호출해서 모터를 켜거나 끌 수 있다.

서비스의 기본 흐름은 단순하다.

  1. 서비스 서버가 특정 서비스 이름과 타입으로 대기한다.

  2. 서비스 클라이언트가 요청 데이터를 보낸다.

  3. 서비스 서버가 요청을 처리한다.

  4. 서비스 서버가 응답 데이터를 돌려준다.

  5. 서비스 클라이언트가 응답을 확인한다.

이 흐름은 일반적인 함수 호출과 닮았다. turn_motor_on()이라는 함수를 부르면 결과가 돌아오는 것처럼 보인다. 하지만 중요한 차이가 있다. ROS 2 서비스는 같은 프로세스 안의 함수를 직접 부르는 것이 아니라, 서로 다른 노드 사이에서 이름과 타입을 기준으로 연결된다. 클라이언트 노드는 서버 노드의 내부 코드를 몰라도 된다. 서버 노드가 어떤 언어로 작성되었는지도 몰라도 된다. 서비스 이름과 서비스 타입만 맞으면 요청을 보낼 수 있다.

이 점은 토픽과 닮았다. 토픽에서도 발행자와 구독자는 서로의 내부를 몰랐다. 서비스도 마찬가지로 느슨한 결합을 유지한다. 다만 토픽이 계속 흐르는 데이터 중심이라면, 서비스는 한 번 요청하고 한 번 응답받는 작업 중심이라는 차이가 있다.

토픽과 서비스는 언제 다르게 써야 할까

초보자가 ROS 2를 배울 때 가장 자주 헷갈리는 질문이 있다. "이 기능은 토픽으로 만들까요, 서비스로 만들까요?" 답은 데이터의 성격을 보면 꽤 분명해진다.

LiDAR 데이터처럼 계속 들어오는 값은 토픽이 어울린다. 로봇 위치, 카메라 영상, IMU 센서값, 속도 명령처럼 시간에 따라 계속 변하는 데이터도 토픽이 자연스럽다. 구독자는 최신 데이터를 계속 받아 보면서 자신의 판단을 업데이트한다.

반대로 한 번 요청하고 결과를 확인하면 되는 작업은 서비스가 어울린다. 센서 리셋, 모터 활성화, 로봇 상태 확인, 저장 명령, 설정 변경처럼 "해 주세요"와 "했습니다"가 분명한 기능은 서비스로 표현하기 좋다.

구분토픽서비스
통신 형태지속적인 발행과 구독일회성 요청과 응답
데이터 성격계속 변하는 상태나 센서 데이터명령, 조회, 설정 변경
응답 여부기본적으로 응답 없음요청마다 응답 있음
대표 예/scan, /odom, /cmd_vel/reset, /clear, /motor_power
어울리는 상황계속 관찰해야 할 때처리 성공 여부를 알아야 할 때

예를 들어 /cmd_vel은 속도 명령이므로 서비스처럼 보일 수도 있다. "앞으로 가라"는 명령이기 때문이다. 하지만 실제 이동 로봇에서는 속도 명령을 한 번 보내고 끝내는 방식보다, 짧은 주기로 계속 보내는 방식이 안전하고 제어하기 쉽다. 명령이 끊기면 로봇을 멈추도록 설계할 수도 있다. 그래서 /cmd_vel은 보통 토픽으로 만든다.

반면 "모터 전원을 활성화하라"는 명령은 계속 보낼 필요가 없다. 한 번 요청하고, 성공 여부를 확인하면 된다. 이런 기능은 서비스로 만들면 깔끔하다.

서비스 타입은 요청서와 응답서의 형식이다

토픽에는 메시지 타입이 있었다. /cmd_vel에는 geometry_msgs/msg/Twist가 흐르고, /scan에는 sensor_msgs/msg/LaserScan이 흐른다. 서비스에도 타입이 있다. 다만 서비스 타입은 하나의 데이터 구조가 아니라 요청 구조와 응답 구조를 함께 가진다.

서비스 타입 파일은 보통 .srv 확장자를 가진다. 구조는 가운데의 ---를 기준으로 위쪽이 요청, 아래쪽이 응답이다.

요청 필드
---
응답 필드

예를 들어 어떤 서비스가 정수 두 개를 받아 합계를 돌려준다면 다음처럼 생각할 수 있다.

int64 a
int64 b
---
int64 sum

클라이언트는 a와 b를 담아 요청을 보내고, 서버는 sum을 담아 응답한다. 이 구조를 알면 서비스가 갑자기 어렵게 느껴지지 않는다. 서비스는 결국 "어떤 질문지를 보내고, 어떤 답안지를 받을 것인가"를 정하는 일이다.

ROS 2에는 자주 쓰는 간단한 서비스 타입이 이미 준비되어 있다. 그중 std_srvs 패키지의 Trigger, SetBool, Empty는 작은 명령 인터페이스를 만들 때 매우 유용하다.

Trigger는 요청 필드가 없다. 그냥 "실행해 주세요"라고 신호를 보내는 타입이다. 응답에는 성공 여부와 메시지가 들어 있다.

ros2 interface show std_srvs/srv/Trigger

출력은 다음과 비슷하다.

---
bool success
string message

이 타입은 "자가 진단을 실행해 주세요", "현재 상태를 확인해 주세요", "저장 가능한지 검사해 주세요" 같은 기능에 잘 어울린다. 요청에 따로 보낼 값은 없지만, 결과는 알고 싶을 때 사용한다.

SetBool은 요청으로 참 또는 거짓 값을 보낸다. 응답에는 역시 성공 여부와 메시지가 들어 있다.

ros2 interface show std_srvs/srv/SetBool
bool data
---
bool success
string message

이 타입은 모터 활성화, LED 켜기와 끄기, 안전 모드 진입과 해제처럼 두 가지 상태 중 하나를 설정할 때 편리하다. data: true는 켜기, data: false는 끄기처럼 해석할 수 있다.

Empty는 요청도 비어 있고 응답도 비어 있는 타입이다.

ros2 interface show std_srvs/srv/Empty
---

성공 여부나 메시지도 필요 없고, 그저 신호만 보내면 되는 경우에 사용할 수 있다. 다만 실제 로봇에서는 실패 가능성이 있는 작업이 많다. 센서 리셋에 실패할 수도 있고, 모터 활성화 조건이 맞지 않을 수도 있다. 그래서 실전에서는 Empty보다 Trigger나 SetBool처럼 응답에 성공 여부가 들어 있는 타입이 더 안전한 경우가 많다.

turtlesim에서 서비스 호출해 보기

서비스를 코드로 만들기 전에, 먼저 이미 실행 중인 서비스가 어떻게 보이는지 살펴보자. turtlesim은 서비스 실습에 아주 좋은 작은 놀이터다.

첫 번째 터미널에서 거북이 시뮬레이터를 실행한다.

ros2 run turtlesim turtlesim_node

다른 터미널에서 현재 서비스 목록을 확인한다.

ros2 service list

다음과 비슷한 이름들이 보일 것이다.

/clear
/kill
/reset
/spawn
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative

이 목록은 현재 ROS 2 그래프 안에서 호출할 수 있는 서비스들이다. /clear는 화면의 선을 지우고, /reset은 시뮬레이터를 초기화한다. /spawn은 새 거북이를 만든다.

서비스 타입까지 함께 보고 싶다면 -t 옵션을 붙인다.

ros2 service list -t

특정 서비스의 타입만 확인할 수도 있다.

ros2 service type /clear

예상 출력은 다음과 같다.

std_srvs/srv/Empty

/clear 서비스는 요청에 담을 값도, 응답으로 받을 값도 없다. 그냥 "화면을 지워 주세요"라는 신호만 보내면 된다.

ros2 service call /clear std_srvs/srv/Empty "{}"

이번에는 새 거북이를 만들어 보자. 먼저 /spawn의 타입을 확인한다.

ros2 service type /spawn
turtlesim/srv/Spawn

서비스 구조를 보려면 ros2 interface show를 사용한다.

ros2 interface show turtlesim/srv/Spawn
float32 x
float32 y
float32 theta
string name
---
string name

요청에는 새 거북이를 만들 위치 x, y, 방향 theta, 이름 name이 들어간다. 응답에는 실제로 만들어진 거북이 이름이 돌아온다. 이제 호출해 보자.

ros2 service call /spawn turtlesim/srv/Spawn "{x: 2.0, y: 2.0, theta: 0.0, name: 'friend'}"

화면에 새 거북이가 나타나면 서비스 호출이 성공한 것이다. 이 장면은 작지만 중요한 감각을 준다. 토픽으로 속도를 계속 흘려보내 거북이를 움직이는 것과, 서비스로 한 번 요청해서 새 거북이를 만드는 것은 완전히 다른 통신 방식이다.

로봇 기능을 서비스로 설계하기

이제 실제 이동 로봇을 떠올려 보자. 서비스는 로봇의 버튼, 점검 창, 관리 명령에 잘 어울린다.

예를 들어 모터 활성화 기능을 생각해 보자. 로봇이 켜졌다고 해서 곧바로 모터에 힘을 넣는 것은 위험할 수 있다. 안전 스위치가 눌려 있는지, 배터리 전압이 충분한지, 비상 정지 상태가 아닌지 확인한 뒤 모터를 활성화해야 한다. 이때 /motor_power 같은 서비스를 만들고 SetBool 타입을 사용할 수 있다. 클라이언트가 {data: true}를 보내면 서버는 조건을 확인한 뒤 success: true와 message: "motor enabled"를 응답한다. 조건이 맞지 않으면 success: false와 실패 이유를 돌려준다.

센서 리셋도 좋은 예다. LiDAR나 IMU가 일시적으로 이상한 값을 내보낼 때, 드라이버 노드에 /reset_sensor 서비스를 제공하도록 만들 수 있다. 클라이언트는 리셋을 요청하고, 서버는 리셋 성공 여부를 응답한다. 요청과 응답이 분명하므로 토픽보다 서비스가 자연스럽다.

지도 저장도 서비스와 잘 어울린다. SLAM으로 지도를 만들고 나면 사용자는 "지금 지도를 파일로 저장해 주세요"라고 요청한다. 지도 저장 노드는 파일 경로, 지도 이름, 저장 형식 같은 정보를 받아 처리한 뒤 성공 여부를 응답할 수 있다. 저장된 지도는 이후 위치 추정과 자율주행에서 다시 사용된다. 이처럼 서비스는 로봇 시스템의 중요한 순간에 "작업을 확정하고 결과를 확인하는 통로"가 된다.

반대로 계속 변하는 로봇 상태를 서비스로만 확인하려 하면 불편해진다. 예를 들어 배터리 잔량을 10Hz로 계속 보고 싶다면 매번 서비스를 호출하는 것보다 /battery_state 토픽을 구독하는 편이 낫다. 서비스는 필요할 때 한 번 묻는 데 강하고, 토픽은 계속 지켜보는 데 강하다.

서비스 서버 만들기: 모터 전원 스위치

이번에는 직접 서비스 서버와 클라이언트를 만들어 보자. 예제는 간단한 모터 전원 스위치다. 실제 모터를 바로 제어하지는 않고, true 요청이 오면 모터가 켜졌다고 기록하고, false 요청이 오면 모터가 꺼졌다고 기록한다. 구조는 단순하지만 실제 로봇의 모터 활성화 서비스로 확장하기 좋다.

먼저 워크스페이스의 src 폴더에서 패키지를 만든다.

cd ~/ros2_ws/src
ros2 pkg create my_robot_services --build-type ament_python --dependencies rclpy std_srvs

패키지 안에 서버 파일을 만든다.

cd ~/ros2_ws/src/my_robot_services/my_robot_services
nano motor_power_server.py

다음 코드를 입력한다.

import rclpy
from rclpy.node import Node
from std_srvs.srv import SetBool


class MotorPowerServer(Node):
    def __init__(self):
        super().__init__('motor_power_server')
        self.motor_enabled = False
        self.service = self.create_service(
            SetBool,
            'motor_power',
            self.handle_motor_power
        )
        self.get_logger().info('Motor power service is ready.')

    def handle_motor_power(self, request, response):
        self.motor_enabled = request.data

        if self.motor_enabled:
            response.success = True
            response.message = 'Motor power enabled.'
            self.get_logger().info('Motor power enabled by service request.')
        else:
            response.success = True
            response.message = 'Motor power disabled.'
            self.get_logger().info('Motor power disabled by service request.')

        return response


def main(args=None):
    rclpy.init(args=args)
    node = MotorPowerServer()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

핵심은 create_service()다. 첫 번째 인자는 서비스 타입인 SetBool, 두 번째 인자는 서비스 이름인 motor_power, 세 번째 인자는 요청이 들어왔을 때 실행할 콜백 함수다.

서비스 이름을 코드에서는 motor_power라고 썼지만, 노드가 루트 네임스페이스에서 실행되면 명령줄에서는 보통 /motor_power로 보인다. 앞으로 네임스페이스를 사용하면 /robot1/motor_power처럼 앞에 로봇 이름이 붙을 수도 있다.

handle_motor_power() 함수는 요청과 응답을 받는다. 요청의 request.data에는 클라이언트가 보낸 참 또는 거짓 값이 들어 있다. 서버는 이 값을 보고 내부 상태를 바꾼 뒤, response.success와 response.message를 채워 반환한다.

이 예제에서는 항상 성공한다고 처리했다. 실제 로봇이라면 조금 더 엄격해야 한다. 비상 정지 버튼이 눌려 있거나 배터리 전압이 너무 낮거나 모터 드라이버와 통신이 끊겨 있다면 success를 False로 만들고, message에 이유를 담아야 한다. 서비스 응답은 단순한 장식이 아니라 문제를 좁혀 가는 중요한 단서다.

명령줄에서 서비스 서버 호출하기

Python 클라이언트를 만들기 전에, 먼저 ros2 service call로 서버를 호출해 보자. 패키지 실행을 위해 setup.py의 entry_points에 서버를 등록한다.

entry_points={
    'console_scripts': [
        'motor_power_server = my_robot_services.motor_power_server:main',
    ],
},

그다음 빌드하고 환경을 적용한다.

cd ~/ros2_ws
colcon build --packages-select my_robot_services
source install/setup.bash

첫 번째 터미널에서 서비스 서버를 실행한다.

ros2 run my_robot_services motor_power_server

다른 터미널에서 서비스 목록을 확인한다.

ros2 service list

/motor_power가 보이면 서버가 정상적으로 등록된 것이다. 타입도 확인해 보자.

ros2 service type /motor_power
std_srvs/srv/SetBool

이제 모터를 켜는 요청을 보낸다.

ros2 service call /motor_power std_srvs/srv/SetBool "{data: true}"

응답은 다음과 비슷하다.

response:
std_srvs.srv.SetBool_Response(success=True, message='Motor power enabled.')

이번에는 끄는 요청을 보낸다.

ros2 service call /motor_power std_srvs/srv/SetBool "{data: false}"

서비스 서버 터미널에도 로그가 출력될 것이다. 여기서 중요한 점은 클라이언트가 서버의 내부 변수를 직접 건드리지 않았다는 것이다. 클라이언트는 오직 정해진 서비스 이름으로 정해진 타입의 요청을 보냈고, 서버는 그 요청을 자신의 방식으로 처리했다.

이 구조가 로봇 소프트웨어를 단단하게 만든다. 나중에 가짜 모터 서버를 실제 모터 드라이버 서버로 바꾸더라도, 서비스 이름과 타입을 유지하면 클라이언트 코드를 크게 바꾸지 않아도 된다.

서비스 클라이언트 만들기: 비동기 호출 이해하기

명령줄 호출은 테스트에는 좋지만, 실제 로봇 시스템에서는 다른 노드가 서비스를 호출해야 한다. 예를 들어 상태 관리 노드가 부팅 절차를 마친 뒤 모터 전원 서비스를 호출할 수 있다. 이번에는 Python으로 서비스 클라이언트 노드를 만들어 보자.

같은 패키지 안에 클라이언트 파일을 만든다.

cd ~/ros2_ws/src/my_robot_services/my_robot_services
nano motor_power_client.py

다음 코드를 입력한다.

import sys

import rclpy
from rclpy.node import Node
from std_srvs.srv import SetBool


class MotorPowerClient(Node):
    def __init__(self):
        super().__init__('motor_power_client')
        self.client = self.create_client(SetBool, 'motor_power')
        self.done = False

        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('Waiting for /motor_power service...')

    def send_request(self, enable):
        request = SetBool.Request()
        request.data = enable

        future = self.client.call_async(request)
        future.add_done_callback(self.handle_response)

    def handle_response(self, future):
        try:
            response = future.result()
        except Exception as error:
            self.get_logger().error(f'Service call failed: {error}')
        else:
            self.get_logger().info(
                f'success={response.success}, message="{response.message}"'
            )
        finally:
            self.done = True


def main(args=None):
    rclpy.init(args=args)
    node = MotorPowerClient()

    enable = True
    if len(sys.argv) >= 2:
        enable = sys.argv[1].lower() in ['true', '1', 'on', 'yes']

    node.send_request(enable)

    while rclpy.ok() and not node.done:
        rclpy.spin_once(node)

    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

여기서 눈여겨볼 부분은 call_async()다. 이름 그대로 비동기 호출이다. 클라이언트가 요청을 보낸 뒤 응답이 올 때까지 프로그램 전체가 딱 멈춰 있는 방식이 아니다. 대신 future라는 객체를 돌려받는다. future는 "아직 결과가 오지 않았지만, 나중에 결과가 담길 약속 상자"라고 생각하면 된다.

future.add_done_callback(self.handle_response)는 응답이 도착했을 때 실행할 함수를 등록한다. 응답이 오면 handle_response()가 호출되고, 그 안에서 future.result()로 실제 응답을 꺼낸다.

이 방식은 로봇 시스템에서 중요하다. 로봇 노드는 서비스 응답을 기다리는 동안에도 다른 콜백을 처리해야 할 수 있다. 예를 들어 상태 관리 노드가 모터 활성화 요청을 보내는 동안 배터리 상태, 비상 정지 상태, 사용자 입력을 계속 확인해야 한다면 비동기 호출이 더 안전하다.

클라이언트도 setup.py에 등록한다.

entry_points={
    'console_scripts': [
        'motor_power_server = my_robot_services.motor_power_server:main',
        'motor_power_client = my_robot_services.motor_power_client:main',
    ],
},

다시 빌드하고 환경을 적용한다.

cd ~/ros2_ws
colcon build --packages-select my_robot_services
source install/setup.bash

먼저 서버를 실행한다.

ros2 run my_robot_services motor_power_server

다른 터미널에서 클라이언트를 실행한다.

ros2 run my_robot_services motor_power_client true

끄는 요청도 보낼 수 있다.

ros2 run my_robot_services motor_power_client false

이제 우리는 서비스 서버와 서비스 클라이언트를 모두 만들었다. 서버는 요청을 기다리고, 클라이언트는 요청을 보내며, 응답은 비동기적으로 처리된다. 작은 예제지만 로봇 시스템의 관리 명령 대부분이 이 패턴 위에 올라간다.

서비스가 보이지 않을 때 확인할 것들

서비스는 토픽보다 조용하다. 토픽은 echo로 계속 흐르는 데이터를 볼 수 있지만, 서비스는 호출하지 않으면 아무 일도 일어나지 않는다. 그래서 디버깅할 때 확인 순서를 갖고 있는 것이 좋다.

먼저 서비스 서버가 실행 중인지 확인한다.

ros2 service list

목록에 서비스 이름이 없다면 서버 노드가 실행되지 않았거나, 네임스페이스가 예상과 다르거나, 노드가 시작 중 오류로 종료되었을 가능성이 크다.

서비스 이름이 보인다면 타입을 확인한다.

ros2 service type /motor_power

클라이언트가 기대하는 타입과 서버가 제공하는 타입이 다르면 호출할 수 없다. 서비스도 토픽처럼 이름과 타입이 모두 맞아야 한다.

서비스를 제공하는 노드 정보를 보고 싶다면 다음 명령을 사용할 수 있다.

ros2 service info /motor_power

서비스 타입으로 검색할 수도 있다.

ros2 service find std_srvs/srv/SetBool

마지막으로 직접 호출해 본다.

ros2 service call /motor_power std_srvs/srv/SetBool "{data: true}"

명령줄 호출이 성공하는데 코드에서만 실패한다면 클라이언트 코드의 서비스 이름, 타입, 요청 필드, 실행 환경 source 여부를 확인해야 한다. 특히 새로 빌드한 뒤 source install/setup.bash를 잊으면 이전 코드가 실행되거나 실행 파일을 찾지 못할 수 있다.

서비스 콜백은 짧고 분명해야 한다

서비스는 요청과 응답 구조라서 편리하지만, 아무 작업이나 서비스로 만들면 안 된다. 서비스 서버의 콜백이 너무 오래 걸리면 문제가 생길 수 있다. 요청한 클라이언트는 응답을 오래 기다려야 하고, 서버 노드는 그동안 다른 일을 처리하지 못할 수도 있다. 실행기 구조와 콜백 그룹 설정에 따라 차이는 있지만, 초보 단계에서는 "서비스 콜백은 짧게 끝내자"는 원칙을 지키는 것이 안전하다.

예를 들어 센서 리셋처럼 0.1초 안에 끝나는 작업은 서비스에 적합하다. 모터 활성화처럼 내부 조건을 확인하고 상태를 바꾼 뒤 바로 응답할 수 있는 작업도 좋다. 지도 저장도 파일 크기가 작고 처리 시간이 예측 가능하다면 서비스로 충분할 수 있다.

하지만 목표 지점까지 이동하는 작업은 다르다. 로봇이 복도를 지나 문 앞까지 가는 데 20초가 걸린다고 해 보자. 이것을 서비스로 만들면 클라이언트는 20초 동안 "끝났나?" 하고 기다려야 한다. 중간에 로봇이 어디쯤 갔는지 알기 어렵고, 사용자가 취소하고 싶어도 구조가 어색해진다. 장애물을 만나 우회 중인지, 길을 잃었는지, 목표에 거의 도착했는지도 표현하기 힘들다.

이런 작업에는 다음 장에서 배울 액션이 더 어울린다. 액션은 목표를 보내고, 진행 중 피드백을 받고, 필요하면 취소하며, 마지막 결과를 받는 구조다. 서비스가 짧은 질문과 대답이라면, 액션은 긴 임무를 맡기고 중간 보고를 받는 방식이다.

서비스에서 액션으로 넘어가야 하는 기준은 비교적 명확하다.

  • 작업 시간이 길고 완료 시점을 예측하기 어렵다.

  • 진행률이나 중간 상태를 계속 알고 싶다.

  • 실행 중 취소가 필요하다.

  • 실패했을 때 재시도나 복구 행동이 중요하다.

  • 목표 지점 이동, 로봇팔 작업, 도킹처럼 하나의 임무로 관리해야 한다.

이 기준에 해당한다면 서비스로 억지로 만들지 않는 편이 좋다. 로봇에게 "지금 당장 대답해 줘"라고 물을 일은 서비스로, "이 목표를 수행하고 중간중간 알려 줘"라고 맡길 일은 액션으로 설계하자.

서비스는 로봇의 제어판이다

토픽을 배우고 나면 로봇이 살아 움직이는 것처럼 보인다. 센서 데이터가 흐르고, 위치가 갱신되고, 속도 명령이 전달된다. 서비스까지 이해하면 로봇에 제어판이 생긴다. 버튼을 누르면 모터가 켜지고, 상태 확인을 요청하면 결과가 돌아오며, 센서 리셋이나 지도 저장처럼 분명한 작업을 안정적으로 수행할 수 있다.

좋은 ROS 2 시스템은 토픽과 서비스를 적절히 섞는다. 센서와 상태처럼 계속 흐르는 정보는 토픽으로 보낸다. 설정 변경, 상태 점검, 리셋, 저장처럼 명확한 응답이 필요한 기능은 서비스로 만든다. 그리고 오래 걸리는 임무는 액션으로 넘긴다.

이제 로봇 안의 대화 방식이 한 단계 더 또렷해졌다. 토픽은 계속 흐르는 방송, 서비스는 짧은 질문과 대답이다. 다음 장에서는 여기에 세 번째 방식이 더해진다. 로봇에게 목표를 맡기고, 중간 보고를 받으며, 필요하면 취소할 수 있는 통신 구조. 바로 액션이다.

책 목록

ROS2로 완성하는 자율주행 로봇: 노드·TF·SLAM·Nav2 실전 설계

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기