[Robotics] 1. ROS2 개념 정리

김성윤(Jack)·7일 전

Robotics

목록 보기
1/6

0. 전체 내용 요약과 키워드

이 문서는 ROS2를 로봇 시스템을 구성하는 분산 미들웨어로 정리한다. 핵심은 로봇 기능을 하나의 큰 프로그램이 아니라 여러 node로 나누고, node 사이를 topic, service, action으로 연결하는 방식이다. Topic은 지속적으로 흐르는 데이터, service는 요청과 응답, action은 시간이 걸리는 목표 수행과 feedback/cancel을 담당한다.

실습 코드는 단순 문법보다 통신 파이프라인을 이해하는 데 초점이 있다. Publisher가 message를 어디로 publish하는지, subscriber가 어떤 callback으로 받는지, service client가 어떤 request를 보내고 server가 어떤 response를 채우는지, action client/server가 goal, feedback, result를 어떻게 주고받는지 확인한다. spin, spin_once, spin_until_future_complete, MultiThreadedExecutor는 생성된 통신 endpoint가 실제 callback으로 실행되게 하는 핵심 실행 구조다.

키워드: ROS2, node, topic, publisher, subscriber, service, .srv, request, response, action, goal, feedback, result, cancel, callback, spin, executor, launch, parameter, workspace, package, interface, QoS, ROS graph

1. ROS2

1.1. ROS2 기본 구조

1.1.1. 학습 목적

ROS2는 로봇 시스템을 하나의 거대한 프로그램으로 만들지 않고, 여러 개의 독립된 실행 단위가 서로 통신하며 협력하도록 만드는 로봇 미들웨어다. 로봇은 센서 입력, 인식, 판단, 경로 계획, 제어, 시각화, 시뮬레이션이 동시에 움직이는 시스템이다. 이 모든 기능을 하나의 코드 안에 넣으면 구조가 복잡해지고, 어느 부분에서 문제가 생겼는지 찾기 어렵다.

ROS2의 핵심은 로봇 기능을 node로 나누고, node 사이를 topic, service, action 같은 통신 구조로 연결하는 것이다. 그래서 ROS2를 배운다는 것은 단순히 명령어를 외우는 것이 아니라, 로봇 시스템을 어떤 기능 단위로 나누고, 그 기능들을 어떤 통신 방식으로 다시 묶을지 이해하는 것이다.

수업 흐름에서도 ROS2는 뒤에 나오는 URDF, Gazebo, RViz, MoveIt, Isaac Sim을 이해하기 위한 기반이 된다. URDF는 로봇 모델을 정의하고, Gazebo는 그 모델을 물리 시뮬레이션 안에서 움직이며, RViz는 상태와 좌표계를 시각화하고, MoveIt은 motion planning을 수행한다. 이 모든 흐름은 ROS2 node와 message 통신 위에서 연결된다.

1.1.2. Node

Node는 ROS2 시스템의 기본 실행 단위다. 하나의 node는 하나의 역할을 갖는 것이 좋다. 예를 들어 카메라 이미지를 발행하는 node, 이미지를 받아 물체를 검출하는 node, 로봇 팔의 경로를 계획하는 node, controller에 명령을 보내는 node를 분리할 수 있다.

Node를 분리하면 각 기능을 독립적으로 실행하고 테스트할 수 있다. 또한 다른 프로젝트에서도 재사용하기 쉽다. 반대로 여러 기능을 한 node에 몰아넣으면 topic 흐름이 보이지 않고, 특정 기능만 교체하거나 디버깅하기 어렵다.

ROS2 실습에서 ros2 node list, ros2 node info 같은 명령을 사용하는 이유는 현재 시스템에 어떤 node가 있고, 각 node가 어떤 topic/service/action과 연결되는지 확인하기 위해서다. 단순히 node 이름을 보는 것이 아니라, 시스템의 기능 분해 구조를 읽는 과정이다.

1.2. Topic, Service, Action 통신

1.2.1. Topic: 지속 데이터 통신

Topic은 지속적으로 흐르는 데이터에 적합한 통신 방식이다. Publisher는 topic에 message를 발행하고, subscriber는 topic을 구독해 message를 받는다. 센서 데이터, 카메라 이미지, 로봇 상태, joint state, object detection 결과처럼 계속 갱신되는 데이터는 topic으로 다루는 것이 자연스럽다.

Topic의 핵심은 비동기성과 느슨한 연결이다. Publisher는 subscriber가 누구인지 직접 알 필요가 없다. Subscriber도 publisher 내부 구현을 알 필요가 없다. 둘은 topic 이름과 message type만 맞으면 통신할 수 있다. 이 구조 덕분에 로봇 시스템은 센서 node, perception node, visualization node를 독립적으로 바꾸면서도 같은 topic을 기준으로 연결할 수 있다.

Topic 실습에서 중요한 것은 “메시지가 오고 간다”는 결과보다 어떤 데이터가 지속 흐름으로 설계되는지 판단하는 것이다. 예를 들어 Turtlesim에서 velocity command는 계속 publish될 수 있고, turtle pose도 계속 갱신된다. 로봇에서는 camera image, lidar scan, joint state, detection result가 topic으로 흐른다.

1.2.2. Service: 요청과 응답 통신

Service는 명확한 요청과 응답이 필요한 작업에 적합하다. Client가 request를 보내면 server가 response를 돌려준다. 예를 들어 특정 물품의 재고를 확인하거나, 결제를 승인하거나, 현재 설정값을 조회하거나, 특정 계산을 요청하는 작업은 service 구조가 잘 맞는다.

Service는 함수 호출처럼 보일 수 있지만 실제로는 node 사이의 통신이다. 따라서 server가 실행 중인지 확인해야 하고, 응답 실패나 지연 가능성도 고려해야 한다. 서비스가 준비되지 않았는데 client가 요청을 보내면 전체 흐름이 실패할 수 있다.

수업 과제에서 확인한 smart shop service 실습은 ROS2 service 구조를 잘 보여 준다. order_manager node는 place_order service server로 주문 요청을 받고, 내부적으로 check_stock, authorize_payment, discount_server 같은 다른 service에 의존한다. 즉 하나의 node가 server이면서 동시에 client가 될 수 있다. 실제 로봇 시스템에서도 하나의 작업 node가 다른 node의 기능을 요청하는 구조가 자주 등장한다.

위 실행 화면에서는 여러 터미널에서 order_manager, stock_server, payment_server, discount_server, order_client, log_monitor가 동시에 실행된다. 주문이 들어오면 재고 확인, 할인 적용, 결제 승인, 로그 기록이 node별로 나뉘어 처리된다. 성공 주문은 success=True와 결제 승인 코드가 출력되고, 재고가 부족한 주문은 rejected 상태와 남은 재고가 출력된다. 이 이미지는 ROS2 service가 단순 예제 호출이 아니라 여러 node가 역할을 나누어 하나의 업무 흐름을 구성할 수 있음을 보여 준다.

1.2.3. Action: 장기 작업 통신

Action은 시간이 오래 걸리는 목표 수행에 적합하다. Service는 요청을 보내고 응답을 기다리는 구조이지만, 로봇의 이동이나 작업 수행은 즉시 끝나지 않는다. 로봇 팔을 목표 위치로 이동시키거나, 모바일 로봇이 목적지까지 주행하거나, 물체를 집고 옮기는 작업은 진행 중 상태를 확인하고, 필요하면 취소할 수 있어야 한다.

Action은 goal, feedback, result 구조를 가진다. Client는 goal을 보내고, server는 수행 중 feedback을 보내며, 작업이 끝나면 result를 반환한다. 이 구조는 로봇 motion planning이나 navigation처럼 시간이 걸리는 작업에 적합하다.

ROS2의 Action 개념은 MoveIt과도 연결된다. MoveIt에서 trajectory execution은 단순한 함수 호출이 아니라 목표 trajectory를 실행하고 상태를 추적하는 장기 작업으로 볼 수 있다. 따라서 Action은 로봇의 “명령을 보내고 끝”이 아니라 “목표를 주고 수행 과정을 관리”하는 방식으로 이해해야 한다.

1.2.4. Topic, Service, Action: 통신 방식 비교와 핵심 코드

  • 기본 개념

ROS2 통신 구조에서 가장 중요한 단위는 node, topic, service, action이다. 이들은 모두 노드 사이의 연결을 만든다는 공통점이 있지만, 다루는 데이터의 성격이 다르다. 이 차이를 정확히 이해해야 로봇 시스템을 설계할 수 있다.

Node는 ROS2 시스템의 기본 실행 단위다. 하나의 node는 보통 하나의 책임을 가진다. 센서 값을 읽는 node, 모터 명령을 보내는 node, 카메라 이미지를 처리하는 node, 물체 검출 결과를 publish하는 node처럼 역할을 분리한다. Node를 작게 나누면 재사용과 디버깅이 쉬워진다.

  • Topic: 계속 흐르는 상태와 이벤트

Topic은 지속적으로 흐르는 데이터에 적합한 통신 방식이다. Publisher는 topic에 message를 발행하고, subscriber는 해당 topic을 구독해 message를 받는다. 센서 데이터, 카메라 이미지, 로봇 상태, joint state, object detection 결과처럼 계속 갱신되는 데이터는 topic으로 다루는 것이 자연스럽다.

Topic의 핵심은 비동기성이다. Publisher는 subscriber가 누구인지 직접 알 필요가 없고, subscriber도 publisher의 내부 구현을 알 필요가 없다. 둘은 topic name과 message type을 통해 느슨하게 연결된다.

  • Service: 명확한 요청과 응답

Service는 명확한 요청과 응답이 필요한 작업에 적합하다. Client가 request를 보내면 server가 response를 돌려준다. 재고 확인, 설정 값 조회, 특정 계산 요청, 간단한 상태 확인처럼 “질문을 보내고 답을 받는” 구조라면 service가 어울린다.

Service는 함수 호출과 비슷하게 느껴질 수 있지만 실제로는 노드 사이의 네트워크 통신이다. 따라서 service server가 준비되어 있는지 확인해야 하고, 응답 지연이나 실패 가능성도 고려해야 한다.

Service를 구성할 때는 먼저 .srv interface를 설계해야 한다. .srv 파일은 ---를 기준으로 위쪽 request와 아래쪽 response를 나눈다. Request에는 client가 server에게 처리해 달라고 보낼 입력 값을 넣고, response에는 server가 처리 후 돌려줄 결과 값을 넣는다.

예를 들어 주문 처리 흐름에서는 PlaceOrder service가 주문 요청을 받는 외부 interface가 된다. Request에는 order_id, item_id, quantity, amount, currency처럼 주문을 처리하는 데 필요한 값이 들어간다. Response에는 success, status, detail, remaining_stock, payment_auth_code처럼 주문 결과를 해석하는 데 필요한 값이 들어간다.

이 interface를 기준으로 server와 client가 분리된다. order_clientPlaceOrder.Request 객체를 만들어 값을 채우고 service를 호출한다. order_managerPlaceOrder service server를 열어 request를 받은 뒤, 내부에서 CheckStock, AuthorizePayment, DiscountApply 같은 다른 service client를 호출해 결과를 조합한다. 마지막으로 PlaceOrder.Response에 성공 여부와 상세 정보를 채워 client에게 돌려준다.

따라서 service 실습의 핵심은 server/client 코드를 각각 실행하는 것이 아니라, request/response interface가 여러 node의 책임을 어떻게 연결하는지 이해하는 것이다. 재고 확인은 stock server가 담당하고, 결제 승인은 payment server가 담당하며, 주문 manager는 이 결과를 조합해 하나의 주문 처리 결과로 만든다.

  • Action: 진행률과 취소가 필요한 목표 수행

Action은 시간이 오래 걸리는 목표 수행에 적합하다. 로봇 팔을 특정 위치로 이동시키거나, 모바일 로봇을 목표 지점까지 이동시키는 작업은 즉시 끝나지 않는다. 이런 작업은 목표를 보내고, 수행 중 feedback을 받고, 최종 result를 받으며, 필요하면 cancel할 수 있어야 한다.

Action은 service처럼 목표를 요청하지만, topic처럼 중간 상태를 계속 받을 수 있다는 점에서 장기 작업에 적합하다.

통신 방식 실습 정리

  • 실습에서 확인할 점

서비스 서버와 서비스 클라이언트가 함께 등장하는 구조는 ROS2의 요청/응답 흐름을 이해하는 데 좋다. 주문 관리 node가 주문 요청을 받으면 재고 확인 service와 결제 승인 service를 차례로 호출할 수 있다. 이 구조에서 하나의 node는 service server이면서 동시에 다른 service의 client가 된다.

이 실습을 통해 ROS2 통신 방식이 단순 문법 문제가 아니라 시스템 책임 분리의 문제라는 점을 이해할 수 있다. 어떤 데이터가 계속 흘러야 하는지, 어떤 기능은 요청/응답으로 처리해야 하는지, 어떤 목표는 action으로 관리해야 하는지 판단해야 한다.

Topic 코드: publisher와 subscriber가 같은 topic 계약으로 만나는 코드

my_pubsub/my_pub.py의 핵심은 publisher 생성, timer 등록, 메시지 발행이다.

self.publisher_ = self.create_publisher(String, 'topic', 10)
self.timer = self.create_timer(timer_period, self.timer_callback)

msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)

여기서 String은 message type, 'topic'은 통신 이름, 10은 QoS queue depth다. Publisher가 상대 node를 직접 호출하지 않는다는 점이 중요하다. 이 node는 “누가 받을지”가 아니라 “어떤 topic에 어떤 type의 메시지를 흘릴지”만 정한다. 그래서 publisher와 subscriber는 서로의 파일명, class명, 내부 구현을 몰라도 topic 이름과 message type만 같으면 연결된다.

my_pubsub/my_sub.py는 같은 topic과 같은 message type을 기준으로 callback을 연결한다.

self.subscription = self.create_subscription(
    String,
    'topic',
    self.listener_callback,
    10
)

def listener_callback(self, msg):
    self.get_logger().info('I heard: "%s"' % msg.data)

Subscriber의 본질은 값을 “가져오는” 것이 아니라 도착한 message를 callback으로 처리하는 것이다. 그래서 topic은 sensor stream, robot state, log, detection 결과처럼 지속적으로 흘러가고 여러 node가 동시에 관찰해도 되는 데이터에 맞다.

smart_shop_nodes/log_monitor.py는 topic이 사건 기록에도 쓰인다는 점을 보여 준다.

self.subscription = self.create_subscription(
    OrderLog,
    'order_log',
    self.listener_callback,
    10
)

if msg.success:
    self.success_count += 1
else:
    self.failed_count += 1

주문 처리는 service로 요청/응답을 끝내지만, 처리 결과는 OrderLog message로 order_log topic에 남는다. log_monitor는 주문을 요청한 client가 아니어도 같은 topic을 구독해 성공/실패 통계를 만들 수 있다. 따라서 topic은 “명령을 처리해 달라”보다 “상태 변화나 사건을 여러 node가 관찰하게 하라”에 가깝다.

Service 코드: .srv가 request와 response 계약을 만드는 코드

Service 실습에서 .srv 파일은 단순 보조 파일이 아니라 server와 client가 공유하는 함수 시그니처에 해당한다. 예를 들어 PlaceOrder.srv는 위쪽에 request, --- 아래쪽에 response를 둔다.

string order_id
string item_id
int32 quantity
float64 amount
string currency
---
bool success
string status
string detail
int32 remaining_stock
string payment_auth_code

--- 위는 client가 server에게 넘겨야 하는 입력이다. order_id, item_id, quantity, amount, currency가 없으면 주문 처리에 필요한 판단을 할 수 없다. --- 아래는 server가 처리 후 돌려줘야 하는 결과다. success만 있으면 왜 실패했는지 알 수 없으므로 status, detail, remaining_stock, payment_auth_code처럼 다음 판단에 필요한 정보를 함께 둔다.

order_manager.py는 이 interface를 server로 열면서 동시에 다른 service들의 client가 된다.

self.srv = self.create_service(
    PlaceOrder,
    'place_order',
    self.cb_place_order,
    callback_group=self.cb_group
)

self.stock_cli = self.create_client(CheckStock, 'check_stock', callback_group=self.cb_group)
self.pay_cli = self.create_client(AuthorizePayment, 'authorize_payment', callback_group=self.cb_group)
self.disc_cli = self.create_client(DiscountApply, 'discount_apply', callback_group=self.cb_group)

이 구조에서 order_manager는 외부에는 place_order 기능을 제공하는 server지만, 내부 처리에는 재고, 할인, 결제 service를 호출하는 client다. 그래서 service는 “한 node가 한 역할만 한다”가 아니라, 각 node가 자신의 책임을 service로 공개하고 필요한 기능을 다른 service에 요청하는 구조다.

처리 흐름은 request를 받아 내부 service request로 바꾸고, 각 response를 근거로 최종 response를 채우는 방식이다.

stock_res = self.stock_cli.call(stock_req)
disc_res = self.disc_cli.call(disc_req)
pay_res = self.pay_cli.call(pay_req)

response.success = True
response.status = 'success'
response.remaining_stock = stock_res.remaining
response.payment_auth_code = pay_res.auth_code

따라서 workspace, package, interface의 역할도 함께 정리된다. Workspace는 여러 package를 같은 빌드 환경에서 묶는 단위다. Interface package는 .srv.msg를 두고 빌드 과정에서 Python/C++에서 import 가능한 타입을 생성한다. Node package는 생성된 타입을 사용해 실제 server, client, publisher, subscriber 동작을 구현한다. Interface와 구현을 분리해야 service 계약은 유지하면서 node 내부 로직을 바꿀 수 있다.

Action 코드: goal, feedback, result가 나뉘는 코드

my_action_interfaces/action/PatrolTest.action은 action이 세 구역으로 나뉜다는 점을 코드 자체로 보여 준다.

geometry_msgs/Pose2D[] waypoints
float64 tolerance
---
bool success
string message
---
int32 current_index
float64 distance_remaining

첫 번째 구역은 goal이다. Patrol에서는 여러 waypoint와 허용 오차가 목표다. 두 번째 구역은 result다. 모든 waypoint 수행이 끝난 뒤 성공 여부와 메시지를 돌려준다. 세 번째 구역은 feedback이다. 실행 도중 현재 waypoint index와 남은 거리를 계속 알려 준다.

patrol_action_server.py의 핵심은 goal을 받은 뒤 즉시 끝내지 않고 실행 상태를 유지한다는 점이다.

feedback_msg.current_index = self.current_idx
feedback_msg.distance_remaining = dist
goal_handle.publish_feedback(feedback_msg)

if self.current_idx >= len(self.waypoints):
    goal_handle.succeed()
    result.success = True
    return result

Service라면 request를 받고 response를 한 번 돌려주면 끝난다. 하지만 patrol, navigation, robot arm trajectory, pick-and-place sequence는 시간이 걸리는 목표 수행이다. 실행 중 feedback을 보고, 필요하면 cancel하며, 끝났을 때 result를 받아야 하므로 action이 필요하다.

Spin과 executor 코드: callback을 실제로 실행하는 부분

ROS2 node에서 publisher, subscriber, service, action을 생성하는 것만으로 callback이 저절로 실행되지는 않는다. Callback을 처리하려면 node를 executor에 올리고 spin해야 한다. order_manager.py는 service callback 안에서 다른 service를 동기 호출하므로 ReentrantCallbackGroupMultiThreadedExecutor를 함께 사용한다.

self.cb_group = ReentrantCallbackGroup()

self.srv = self.create_service(
    PlaceOrder,
    'place_order',
    self.cb_place_order,
    callback_group=self.cb_group
)

self.stock_cli = self.create_client(
    CheckStock,
    'check_stock',
    callback_group=self.cb_group
)
executor = MultiThreadedExecutor(num_threads=4)
executor.add_node(node)

try:
    executor.spin()
except KeyboardInterrupt:
    pass
finally:
    node.destroy_node()
    rclpy.shutdown()

여기서 create_servicecreate_client는 통신 endpoint를 만드는 부분이고, executor.spin()은 실제 callback을 계속 받아 실행하는 event loop다. Service callback 안에서 다시 service를 호출하는 구조는 한 callback이 기다리는 동안 다른 callback도 처리될 수 있어야 하므로 multi-threaded executor가 필요하다. 즉 spin은 보조 코드가 아니라 ROS2 통신이 실제로 살아 움직이게 하는 실행 조건이다.

Service 대기와 fallback 코드: dependency가 없을 때 안전하게 응답하는 부분

Service client는 server가 떠 있다는 가정만으로 호출하면 안 된다. order_manager.py는 내부 dependency인 check_stock, authorize_payment service가 준비되지 않았을 때 기다려 보고, 없으면 실패 response를 명시적으로 채운다.

def wait_service_or_fail(self, client, name, timeout_sec=3.0):
    if not client.wait_for_service(timeout_sec=timeout_sec):
        self.get_logger().error(f"Service not available: {name}")
        return False
    return True
if not self.wait_service_or_fail(self.stock_cli, 'check_stock'):
    response.success = False
    response.status = "dependency_unavailable"
    response.detail = "check_stock not available"
    response.remaining_stock = 0
    response.payment_auth_code = ""
    return response

이 fallback은 예외가 나면 프로그램을 멈추는 방식이 아니라, service 계약에 맞는 실패 응답을 돌려주는 방식이다. Client 입장에서는 응답이 끊기지 않고 success=False, status="dependency_unavailable"를 통해 실패 원인을 해석할 수 있다. 그래서 service interface에는 성공 여부뿐 아니라 상태 문자열과 상세 이유를 넣는 것이 중요하다.

test_svc/my_client.py는 async service client에서 service 준비 대기와 future spin을 보여 준다.

self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
    self.get_logger().info('service not available, waiting again...')
future = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
rclpy.spin_until_future_complete(minimal_client, future)
response = future.result()

call_async()는 요청을 보내고 즉시 future를 돌려준다. 결과가 올 때까지 node가 callback 처리를 해야 하므로 spin_until_future_complete()가 필요하다. 이 코드는 service가 단순 함수 호출처럼 보이더라도 실제로는 ROS graph 안에서 request와 response가 오가고, executor가 그 응답을 처리해야 한다는 점을 보여 준다.

Action server 코드: pub/sub/timer/action이 한 node 안에서 결합되는 부분

patrol_action_server.py는 action이 단독 통신이 아니라 topic, timer, callback과 함께 동작한다는 점을 잘 보여 준다.

self.cmd_pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
self.create_subscription(Pose, '/turtle1/pose', self.pose_cb, 10)
self.server = ActionServer(
    self,
    PatrolTest,
    'patrol',
    execute_callback=self.execute_callback,
    goal_callback=self.goal_callback,
    handle_accepted_callback=self.handle_accepted_callback,
    cancel_callback=self.cancel_callback,
    callback_group=self.cb
)

self.timer = self.create_timer(0.05, self.timer_cb, callback_group=self.cb)

Action server는 patrol goal을 받고, pose는 /turtle1/pose topic subscription으로 계속 갱신하며, 실제 속도 명령은 /turtle1/cmd_vel topic으로 publish한다. timer_cb()는 주기적으로 현재 pose와 goal waypoint를 비교해 feedback과 제어 명령을 만든다.

if self.goal_handle.is_cancel_requested:
    self.stop()
    self.goal_handle.canceled()
    self.reset()
    return

fb = PatrolTest.Feedback()
fb.current_index = self.idx
fb.remaining_distance = dist
self.goal_handle.publish_feedback(fb)

self.cmd_pub.publish(cmd)

Cancel 요청이 오면 로봇을 멈추고 action 상태를 canceled로 바꾼다. 실행 중에는 feedback을 publish해 client가 진행 상황을 볼 수 있게 하고, 동시에 Twist 명령을 publish해 turtlesim을 움직인다. 따라서 action은 “긴 service”가 아니라 goal 관리, feedback, cancel, 내부 topic 제어가 결합된 실행 구조다.

executor = MultiThreadedExecutor()
executor.add_node(node)
executor.spin()

이 action server도 executor가 spin해야 goal callback, cancel callback, timer callback, subscription callback이 처리된다. 특히 timer 기반 action은 spin이 멈추면 feedback도, 제어 명령 publish도 멈춘다.

통신 파이프라인 코드: 어디서 보내고 어디서 받는가

ROS2 실습 코드를 읽을 때는 publish()callback() 한 줄만 보지 말고, 데이터가 어떤 방향으로 흐르는지 따라가야 한다. 가장 단순한 topic 실습의 파이프라인은 my_pub.py -> topic -> my_sub.py다.

self.publisher_ = self.create_publisher(String, 'topic', 10)
self.timer = self.create_timer(timer_period, self.timer_callback)

msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.subscription = self.create_subscription(
    String,
    'topic',
    self.listener_callback,
    10
)

def listener_callback(self, msg):
    self.get_logger().info('I heard: "%s"' % msg.data)

여기서 my_pub.py는 timer callback에서 String message를 만들어 topic으로 날린다. my_sub.py는 같은 topic 이름과 같은 message type을 기준으로 message를 받고 listener_callback()에서 처리한다. Cmd에서는 보통 publisher node와 subscriber node를 각각 실행한 뒤, 필요하면 ros2 topic list, ros2 topic echo /topic으로 중간에 흐르는 message를 확인한다. 즉 topic은 sender와 receiver가 서로를 직접 부르지 않고, ROS graph의 topic 이름을 통해 만나는 구조다.

Service 실습의 파이프라인은 client -> place_order service -> order_manager -> 내부 service들 -> 최종 response다. order_manager.py는 외부에서 place_order 요청을 받는 server이면서, 내부적으로는 재고와 결제 service를 호출하는 client다.

self.srv = self.create_service(
    PlaceOrder,
    'place_order',
    self.cb_place_order,
    callback_group=self.cb_group
)

self.stock_cli = self.create_client(CheckStock, 'check_stock', callback_group=self.cb_group)
self.pay_cli = self.create_client(AuthorizePayment, 'authorize_payment', callback_group=self.cb_group)
stock_res = self.stock_cli.call(stock_req)
pay_res = self.pay_cli.call(pay_req)

response.success = True
response.status = "success"
response.detail = "order accepted"
response.remaining_stock = stock_res.remaining
response.payment_auth_code = pay_res.auth_code

이 흐름에서 place_order로 들어온 request는 cb_place_order() 안에서 CheckStock.Request, AuthorizePayment.Request로 다시 바뀌어 다른 service로 날아간다. 각 service의 response가 돌아오면 order_manager가 최종 PlaceOrder.Response를 채운다. Cmd에서는 server node들이 먼저 떠 있어야 하고, client가 service call을 보낸 뒤 response를 확인한다. 중간 service가 없으면 앞에서 본 wait_for_service()와 fallback response가 작동한다.

Action 실습의 파이프라인은 action client -> patrol goal -> action server -> /turtle1/cmd_vel -> turtlesim, 그리고 반대 방향으로 /turtle1/pose -> action server -> feedback -> action client가 동시에 흐른다.

goal_msg = PatrolTest.Goal()
goal_msg.waypoints = self.generate_random_waypoints(count=100)
goal_msg.tolerance = 0.3

self._client.wait_for_server()
send_future = self._client.send_goal_async(
    goal_msg,
    feedback_callback=self.feedback_callback
)
def feedback_callback(self, feedback_msg):
    fb = feedback_msg.feedback
    self.get_logger().info(
        f'Feedback: waypoint={fb.current_index}, '
        f'remaining={fb.remaining_distance:.2f}'
    )

Client는 waypoint 목록을 goal로 보내고, server는 pose topic으로 현재 위치를 받아 다음 속도 명령을 계산한다. Server가 /turtle1/cmd_velTwist를 publish하면 turtlesim이 움직이고, turtlesim은 다시 /turtle1/pose를 publish한다. Server는 pose를 보고 남은 거리를 feedback으로 client에게 보내며, Enter 입력 같은 조건이 들어오면 client가 cancel request를 보낸다. 이 구조 때문에 action은 단순한 “요청 후 응답”이 아니라 목표, 상태 관찰, 제어 명령, feedback, cancel이 동시에 묶인 파이프라인이다.

Cmd에서 파이프라인을 확인하는 법

실습을 실행할 때 cmd에서는 “node를 실행한다”와 “ROS graph 안에서 흐름을 관찰한다”를 나누어 봐야 한다. setup.py의 console script를 보면 my_pubsub package는 talker, listener 실행 파일을 만든다.

entry_points={
    'console_scripts': [
        'talker = my_pubsub.my_pub:main',
        'listener = my_pubsub.my_sub:main',
    ],
}

따라서 build와 source가 끝난 뒤에는 다음처럼 두 node를 각각 실행하고, 다른 터미널에서 topic을 확인할 수 있다.

ros2 run my_pubsub talker
ros2 run my_pubsub listener
ros2 topic list
ros2 topic echo /topic

Service 실습의 setup.pyservice, client 실행 파일을 만든다.

entry_points={
    'console_scripts': [
        'service = my_svc.my_service:main',
        'client = my_svc.my_client:main',
    ],
}

이때 실행 순서는 server를 먼저 띄우고 client를 실행하는 방식이다. ros2 service listadd_two_ints가 보이는지 확인하고, client를 실행하면 call_async()로 request가 날아가고 spin_until_future_complete() 이후 response가 출력된다.

ros2 run my_svc service
ros2 service list
ros2 run my_svc client 2 3

Action 실습의 my_patrol_server package는 action server와 client를 따로 실행하게 되어 있다.

entry_points={
    'console_scripts': [
        'patrol_action_server = my_patrol_server.patrol_action_server:main',
        'patrol_action_client = my_patrol_server.patrol_action_client:main',
    ],
}

Cmd에서는 turtlesim, patrol server, patrol client를 함께 띄운 뒤 action과 topic을 동시에 확인한다. /turtle1/cmd_vel은 server가 turtlesim으로 날리는 제어 명령이고, /turtle1/pose는 turtlesim이 server에게 돌려주는 현재 상태다.

ros2 run turtlesim turtlesim_node
ros2 run my_patrol_server patrol_action_server
ros2 run my_patrol_server patrol_action_client
ros2 topic echo /turtle1/cmd_vel
ros2 topic echo /turtle1/pose
ros2 action list

이렇게 실행하면 코드 안의 create_publisher, create_subscription, ActionClient, ActionServer, spin()이 cmd에서 보이는 ROS graph와 대응된다. 즉 cmd는 단순 실행 창이 아니라, 코드 파이프라인이 실제 ROS 통신으로 연결되었는지 검증하는 관찰 도구다.

  • 같은 시스템 안에서 세 통신이 함께 쓰이는 이유

하나의 로봇 시스템은 보통 topic, service, action 중 하나만 쓰지 않는다. 센서와 상태는 topic으로 계속 흐르고, 설정 조회나 단발 판단은 service로 처리되며, 시간이 걸리는 목표 수행은 action으로 관리된다. Smart shop 예제에서는 주문 요청은 service이고 주문 로그는 topic이다. Patrol 예제에서는 목표 waypoint 수행이 action이고, 내부 제어 명령은 topic이다. 통신 방식을 고르는 기준은 “데이터가 계속 흐르는가, 응답이 필요한가, 진행 중 상태와 취소가 필요한가”이다.

  • 한 줄 정리

Topic은 지속 데이터, Service는 요청과 응답, Action은 시간이 걸리는 목표 수행을 위한 통신 방식이다.

1.3. Launch와 Parameter

1.3.1. Launch: 실행 구성

Launch는 여러 node와 설정을 한 번에 실행하기 위한 구성 파일이다. ROS2 시스템이 커지면 매번 터미널에서 node를 하나씩 실행하기 어렵다. 어떤 node를 실행할지, 어떤 parameter를 넣을지, topic 이름을 remap할지, namespace를 어떻게 둘지, RViz나 Gazebo를 함께 실행할지 등을 launch 파일에 묶는다.

Launch는 단순히 명령어를 줄이는 도구가 아니다. 로봇 시스템의 실행 구조를 기록하는 문서이기도 하다. Launch 파일을 보면 어떤 node들이 함께 동작하고, 어떤 설정으로 연결되는지 알 수 있다.

위 이미지는 launch 파일로 여러 Turtlesim node를 실행하고, 배경색 parameter를 바꾸는 실습 화면이다. 오른쪽 코드에는 generate_launch_description()TimerAction, event handler가 보이고, 터미널에는 여러 node가 process로 시작되는 로그가 보인다. 이 이미지는 launch가 단순 실행 단축이 아니라 여러 node와 이벤트, parameter 변경을 하나의 실행 시나리오로 구성한다는 점을 보여 준다.

1.3.2. Parameter: 실행 조건

Parameter는 node의 동작을 코드 수정 없이 바꾸기 위한 설정 값이다. 로봇 속도 제한, 센서 주기, controller gain, threshold, 목표 위치, 색상 값, planning 옵션 같은 값은 코드 안에 박아두기보다 parameter로 분리하는 것이 좋다.

Parameter를 사용하면 같은 node를 여러 조건에서 반복 실험할 수 있다. 예를 들어 Turtlesim 배경색을 바꾸거나, 센서 publish 주기를 바꾸거나, controller 설정값을 바꾸는 일을 코드 수정 없이 할 수 있다. 이렇게 하면 알고리즘 코드는 그대로 두고 실행 조건만 바꿀 수 있어, 실험 결과를 비교할 때 어떤 조건이 달라졌는지 추적하기 쉽다.

1.3.3. Launch와 Parameter: 실습 코드와 실행 시나리오

  • 기본 개념

ROS2 시스템이 커지면 여러 node를 매번 터미널에서 하나씩 실행하는 방식은 한계에 부딪힌다. 실행해야 할 node가 많아지고, 각 node에 넘겨야 할 parameter와 remapping이 늘어나며, namespace와 실행 순서도 관리해야 한다. Launch는 이런 실행 조건을 하나의 파일로 묶어 재현 가능한 실행 구성을 만드는 도구다.

Parameter는 node의 동작을 코드 수정 없이 바꾸기 위한 설정 값이다. 로봇 속도 제한, 센서 publish 주기, controller gain, 목표 위치, file path, threshold 값, planning 옵션 등은 parameter로 분리할 수 있다.

  • Launch의 역할

Launch 파일은 단순히 긴 명령어를 줄여 주는 편의 기능이 아니다. 로봇 시스템에서 launch는 실험 조건을 정의하는 문서이기도 하다. 어떤 node를 실행할지, 어떤 parameter 파일을 적용할지, topic 이름을 어떻게 remap할지, RViz나 Gazebo를 함께 실행할지, namespace를 어떻게 나눌지가 모두 launch에 들어갈 수 있다.

따라서 launch 파일을 보면 로봇 시스템의 실행 구조를 읽을 수 있다. 이는 단순히 실행 자동화가 아니라, 여러 node가 하나의 시스템으로 묶이는 방식을 기록하는 일이다.

  • Launch를 어떻게 구성하는가

Launch 파일은 보통 generate_launch_description() 함수에서 실행할 action들을 반환하는 구조로 작성한다. 여기에는 Node 실행, 다른 launch 파일 포함, parameter 파일 전달, remapping, namespace 지정, 시간 지연 실행, event handler 등록 등이 들어갈 수 있다.

예를 들어 Turtlesim을 여러 개 실행하는 실습에서는 각각의 turtlesim node를 다른 이름이나 namespace로 실행해야 충돌이 나지 않는다. 같은 node executable을 실행하더라도 node name과 namespace를 다르게 두면 서로 다른 프로세스처럼 관리할 수 있다. 여기에 parameter를 함께 넘기면 각 node의 배경색이나 동작 조건도 다르게 설정할 수 있다.

Launch 구성을 읽을 때는 “무엇이 실행되는가”와 “어떤 이름으로 연결되는가”를 봐야 한다. ROS2 시스템에서는 node 이름, namespace, topic 이름이 통신 구조를 결정하기 때문이다. Launch에서 remapping을 사용하면 코드 안의 topic 이름은 그대로 두고 실행 시점에 연결 대상을 바꿀 수 있다.

  • Parameter의 역할

Parameter를 코드 안에 직접 넣으면 조건을 바꿀 때마다 코드를 수정해야 하고, 실험 재현성이 떨어진다. Parameter로 분리하면 같은 node를 여러 조건에서 반복 실행할 수 있다.

로봇 시스템에서는 같은 알고리즘이라도 parameter에 따라 완전히 다른 동작을 보일 수 있다. Controller gain이 달라지면 로봇 움직임이 달라지고, sensor frequency가 달라지면 perception pipeline의 지연이 달라질 수 있다.

  • Parameter를 어떻게 설계하는가

Parameter에는 node의 본질적 알고리즘보다 실행 조건에 가까운 값을 넣는다. 예를 들어 threshold, publish rate, frame name, file path, robot description path, controller gain, planning option, sensor resolution 같은 값이 parameter가 되기 좋다. 반대로 알고리즘의 흐름 자체를 바꾸는 복잡한 로직은 parameter로 숨기기보다 코드 구조로 명확히 드러내야 한다.

Parameter를 YAML 파일로 분리하면 실험 조건을 파일 단위로 관리할 수 있다. 같은 node를 sim.yaml, real_robot.yaml, debug.yaml처럼 다른 설정으로 실행할 수 있고, 어떤 실험에서 어떤 설정을 썼는지 나중에 추적할 수 있다.

Launch와 Parameter 실습 정리

  • 실습에서 확인할 점

Launch와 Parameter를 함께 다루면서 코드와 실험 설정을 분리하는 감각을 얻는다. 실습에서 봐야 하는 것은 실행 결과만이 아니다. 어떤 node들이 동시에 실행되는지, parameter가 어디서 들어오는지, topic 이름이 바뀌는지, node namespace가 나뉘는지 확인해야 한다.

실습에서 Turtlesim 배경색이나 실행 node 수가 launch와 parameter에 의해 바뀌었다면, 그 결과는 단순 화면 변화가 아니다. 같은 executable이라도 실행 구성에 따라 서로 다른 시스템이 만들어진다는 뜻이다. 이 원리를 Gazebo, RViz, MoveIt 실행으로 확장하면 복잡한 로봇 시스템도 하나의 launch entry point로 재현할 수 있다.

  • Launch는 실행 순서를 문서화한다

launch_ws/src/substitutions_py/launch/event_handlers_launch.py는 launch가 여러 node를 동시에 켜는 정도에서 끝나지 않는다는 점을 보여 준다. DeclareLaunchArgumentturtlesim_ns, use_provided_red, new_background_r 같은 실행 인자를 선언하고, LaunchConfiguration으로 실행 시점의 값을 읽는다. 이 값들은 node namespace나 parameter 변경 명령에 들어간다.

이 파일에서 OnProcessStart는 turtlesim node가 시작된 뒤 TimerAction으로 잠시 기다렸다가 /spawn service call을 실행한다. OnExecutionComplete는 spawn 명령이 끝난 뒤 배경색 parameter를 바꾸는 명령을 실행한다. OnProcessExit는 turtlesim 창이 닫히면 전체 launch를 shutdown한다. 따라서 launch는 “명령어 자동 실행”이 아니라 node 생명주기와 후속 동작을 연결하는 실행 시나리오다.

  • Substitution은 실행 시점 값을 끼워 넣는 장치다

main_launch.pyIncludeLaunchDescription으로 다른 launch 파일을 포함하고, PathJoinSubstitution, FindPackageShare, TextSubstitution을 사용해 package share 경로와 인자 값을 조립한다. 이 구조는 launch 파일 내부에 절대 경로를 박아 두지 않고, package가 설치된 위치를 ROS2가 찾게 만든다.

Substitution이 필요한 이유는 launch 파일이 작성 시점과 실행 시점의 정보를 모두 다루기 때문이다. package share 경로, 환경 변수, 실행 인자, node namespace는 실행 환경에 따라 달라질 수 있다. 이를 문자열로 고정하면 다른 PC나 다른 workspace에서 깨질 가능성이 커진다.

  • Multi Turtle 예제에서 namespace가 필요한 이유

multi_turtle_ws/src/multi_turtle_launch/launch/multi_turtle_launch.py는 같은 turtlesim_node executable을 두 번 실행하면서 namespace를 turtle_a, turtle_b로 나눈다. 같은 executable이라도 namespace가 다르면 /turtle_a/sim, /turtle_b/sim처럼 서로 다른 node와 topic/service 공간을 만들 수 있다.

이 실습의 핵심은 “turtlesim 두 개가 뜬다”가 아니다. 같은 코드가 실행 구성에 따라 독립된 시스템 구성 요소가 될 수 있다는 점이다. 실제 로봇에서는 같은 카메라 driver를 front camera와 wrist camera에 각각 띄우거나, 같은 controller node를 다른 robot namespace 아래에서 실행할 때 이 원리가 필요하다.

  • Parameter는 코드가 아니라 실험 조건을 바꾼다

multi_turtle_launch.pyspeed_launch.py에서는 launch argument와 parameter 변경 명령이 함께 등장한다. 예를 들어 background color를 parameter로 바꾸면 turtlesim node 코드를 수정하지 않고 실행 조건만 바꾼다. speed_controller.py처럼 별도 node를 함께 실행하면 사용자가 보는 화면 변화와 topic command 흐름이 launch 구성으로 묶인다.

Parameter를 이렇게 분리하는 이유는 같은 알고리즘을 여러 조건에서 재사용하기 위해서다. 로봇에서는 속도 제한, publish 주기, controller gain, planning time, sensor threshold, frame name 같은 값을 코드에 직접 박아 두면 실험마다 코드를 고쳐야 한다. Parameter로 빼면 launch나 YAML만 바꿔 같은 node를 다른 환경에서 실행할 수 있다.

  • 실습 코드에서 확인해야 할 순서

Launch 파일을 읽을 때는 먼저 어떤 Node가 실행되는지 보고, 다음으로 namespace와 name이 어떻게 붙는지 본다. 그다음 parameter가 어디서 들어오는지, ExecuteProcess로 외부 명령을 호출하는지, RegisterEventHandler가 어떤 event에 어떤 후속 action을 연결하는지 확인한다. 이렇게 읽어야 launch가 시스템 실행 구조를 설명하는 문서처럼 보인다.

  • 한 줄 정리

Launch는 여러 node와 실행 조건을 묶고, Parameter는 같은 코드의 동작을 설정으로 바꾸게 해 준다.

1.4. Workspace, Package, Interface

ROS2 workspace는 여러 package를 한 번에 빌드하고 실행 환경으로 묶는 최상위 작업 공간이다. 실습에서 보인 svc_ws 같은 구조는 보통 src 폴더 아래에 package들이 들어가고, colcon build를 실행하면 build, install, log 폴더가 생성된다. 이후 source install/local_setup.bash를 실행해야 현재 터미널이 방금 빌드한 package와 interface를 인식한다.

Package는 역할 단위로 나누어 구성한다. Service 실습에서는 interface를 정의하는 package와 실제 node를 구현하는 package를 분리하는 구조가 확인된다. 예를 들어 smart_shop_interfaces.srv 파일을 담는 interface package이고, smart_shop_nodesstock_server, payment_server, discount_server, order_manager, order_client, log_monitor 같은 실행 node를 담는 Python package다.

이렇게 package를 분리하는 이유는 interface와 구현을 독립시키기 위해서다. .srv 파일은 server와 client가 공유하는 약속이다. Node 구현이 바뀌어도 request와 response 구조가 유지되면 다른 node는 같은 방식으로 통신할 수 있다. 반대로 .srv 구조가 바뀌면 server와 client 모두 새 interface에 맞게 다시 빌드하고 수정해야 한다.

1.4.1. .srv 파일 구성

.srv 파일은 service 통신의 request와 response 형식을 정의한다. 파일 안에서는 --- 위쪽이 request, 아래쪽이 response다. 예를 들어 재고 확인 service라면 request에는 item_id, quantity 같은 필드가 들어가고, response에는 available, remaining, reason 같은 필드가 들어갈 수 있다.

주문 처리 service라면 request에는 주문을 식별하고 처리하는 데 필요한 값이 들어가야 한다. 예를 들어 order_id, item_id, quantity, amount, currency가 request에 들어갈 수 있다. Response에는 주문 처리 결과를 client가 해석할 수 있도록 success, status, detail, remaining_stock, payment_auth_code 같은 필드가 필요하다.

이 구조는 단순 데이터 나열이 아니라 node 사이의 계약이다. order_client는 request 필드에 값을 채워 place_order service를 호출하고, order_manager는 같은 .srv 정의를 기준으로 request를 읽어 처리한 뒤 response를 채운다. 이때 response의 success는 전체 처리 성공 여부, statussuccessrejected 같은 상태, detail은 실패 이유나 할인 적용 결과, remaining_stock은 재고 처리 결과, payment_auth_code는 결제 승인 결과를 전달한다.

1.4.2. Service package를 구성하는 방식

Service interface package는 보통 srv 폴더에 .srv 파일을 둔다. ROS2는 빌드 과정에서 이 .srv 파일을 언어별 message/service class로 생성한다. Python node에서는 생성된 interface를 import해서 PlaceOrder.Request, PlaceOrder.Response처럼 사용한다.

Node package는 service server와 client를 구현한다. stock_serverCheckStock service server로 재고 가능 여부를 판단하고, payment_serverAuthorizePayment service server로 결제 승인 여부를 만든다. discount_server는 할인율을 계산하는 service가 될 수 있다. order_managerPlaceOrder service server로 외부 주문을 받으면서 내부적으로 재고, 할인, 결제 service client를 호출한다. order_client는 사용자가 보낸 주문 요청을 place_order service로 보내고 response를 출력한다.

이 구조에서 order_manager는 단순 server가 아니다. 외부에서는 주문을 처리하는 service server로 보이지만, 내부에서는 다른 service들을 호출하는 client다. 이 패턴은 실제 로봇 시스템에서도 자주 쓰인다. 예를 들어 manipulation task server가 perception service, planning service, gripper service를 순서대로 호출해 하나의 작업을 완성할 수 있다.

1.4.3. Build와 실행 흐름

Workspace를 만든 뒤에는 src 아래 package를 배치하고 colcon build로 빌드한다. Interface package가 포함되어 있다면 .srv 파일로부터 생성된 코드가 만들어진다. 이후 source install/local_setup.bash를 실행해야 ros2 run으로 node를 실행하거나 Python에서 interface를 import할 수 있다.

실행할 때는 service server node들이 먼저 준비되어야 한다. 예를 들어 stock_server, payment_server, discount_server가 실행되고 나서 order_manager가 이 service client들을 사용할 수 있다. order_client가 주문을 보내면 order_manager는 request를 받고, 재고 확인과 결제 승인 결과를 종합해 최종 response를 반환한다. log_monitor는 처리 결과를 별도로 관찰하거나 기록하는 node로 볼 수 있다.

이 실습에서 배울 점은 .srv가 node 사이에서 어떤 데이터를 주고받을지 정하는 계약이라는 점이다. Workspace는 여러 package를 함께 빌드하는 작업 공간이고, interface package는 통신 형식을 생성하며, node package는 그 형식을 사용해 server/client 동작을 구현한다. 이 세 구조가 분리되어야 service 정의를 재사용하고, 구현 node를 바꾸더라도 같은 interface를 기준으로 시스템을 유지할 수 있다.

1.4.4. Workspace, Package, Interface 실습 정리

  • 실습에서 이해할 점

ROS2 실습의 핵심은 node를 실행했다는 사실이 아니라, 여러 node가 서로 다른 책임을 가지고 통신한다는 구조를 이해하는 것이다. Topic은 지속 데이터 흐름, Service는 요청/응답, Action은 시간이 걸리는 목표 수행에 맞다. Launch는 여러 node와 설정을 하나의 실행 시나리오로 묶고, Parameter는 코드와 실험 조건을 분리한다.

이 개념들이 합쳐지면 로봇 시스템을 설계할 수 있다. 카메라 node는 image topic을 publish하고, perception node는 image를 subscribe해 detection result를 publish한다. Planning node는 detection result를 받아 목표 pose를 만들고, MoveIt action이나 controller에 trajectory를 보낸다. RViz는 TF와 topic을 시각화한다. 이런 전체 흐름이 ROS2 위에서 구성된다.

  • Workspace가 담당하는 것

새로 확인한 ROS/ROS_실습코드/ROS_실습코드ROS/backup_ros_code/backup_ros_data-001 안에는 svc_ws, my_pubsub, my_action, launch_ws, param_ws, gazebo_ws, multi_turtle_ws, move_urdf, depth_ws, ws_moveit 같은 workspace가 들어 있다. 여기서 workspace는 “코드를 모아 둔 폴더”가 아니라 여러 package를 같은 빌드 환경에서 해석하고, 생성된 interface와 실행 파일을 하나의 ROS2 환경으로 묶는 단위다.

src 아래에는 사람이 작성한 package가 들어가고, colcon build 이후에는 build, install, log가 생긴다. 개념 정리에서 중요한 것은 build/install/log 산출물이 아니라 src 아래의 package 구조다. 예를 들어 svc_ws/src/smart_shop_interfaces는 통신 형식을 정의하고, svc_ws/src/smart_shop_nodes는 그 형식을 실제 node 동작으로 사용한다. 이 분리가 되어야 interface를 여러 node가 공유할 수 있고, server 구현을 바꾸더라도 client는 같은 .srv 계약을 기준으로 동작할 수 있다.

  • Interface package가 담당하는 것

smart_shop_interfaces에는 srvmsg가 함께 있다. CheckStock.srvitem_id, quantity를 요청으로 받고 available, remaining, reason을 응답한다. 즉 재고 확인이라는 기능을 “어떤 입력으로 묻고 어떤 출력으로 판단할지” 정한다. AuthorizePayment.srvorder_id, amount, currency를 받아 approved, auth_code, reason을 돌려준다. 결제 서버 내부 구현이 실제 카드 결제든 더미 승인 로직이든 client는 이 응답 필드만 보고 다음 흐름을 결정할 수 있다.

PlaceOrder.srv는 주문 전체를 대표하는 service interface다. 요청에는 order_id, item_id, quantity, amount, currency가 들어가고, 응답에는 success, status, detail, remaining_stock, payment_auth_code가 들어간다. 여기서 응답 필드는 단순 성공/실패만 말하지 않는다. 왜 실패했는지, 재고가 얼마나 남았는지, 결제 승인 코드가 무엇인지까지 다음 node나 사용자에게 전달한다.

DiscountApply.srv는 할인 계산을 별도 service로 분리한다. item_id, original_amount를 요청하고 discounted_amount, discount_rate, reason을 응답하게 되어 있다. 이 구조는 주문 manager가 할인 정책을 직접 품지 않고, 할인 판단을 독립 node로 위임할 수 있게 한다.

OrderLog.msg는 service 응답과 다르게 지속 관찰을 위한 topic 메시지다. order_id, item_id, quantity, amount, success, status, detail, timestamp를 담아 주문 처리 결과를 order_log topic으로 흘린다. service가 “이번 주문을 처리해 달라”는 요청/응답이라면, topic log는 “처리 결과가 발생했다”는 사건 기록이다.

  • Node package가 담당하는 것

smart_shop_nodes는 interface를 실제 동작으로 바꾸는 package다. stock_server.pyCheckStock service server로 재고 가능 여부를 판단하고, payment_server.pyAuthorizePayment server로 결제 승인 여부를 판단한다. discount_server.py는 할인율과 할인 금액을 계산한다.

핵심은 order_manager.py다. 이 node는 place_order service server이면서 동시에 check_stock, authorize_payment, discount_apply service client다. 주문 요청이 오면 먼저 재고 service를 호출하고, 재고가 부족하면 rejected 상태를 응답한다. 재고가 있으면 할인 service로 최종 금액을 계산하고, 그 금액으로 결제 service를 호출한다. 결제가 거절되면 결제 실패 이유를 응답하고, 모두 성공하면 success 상태와 결제 승인 코드를 응답한다.

이 구조가 중요한 이유는 하나의 node가 “모든 일을 직접 처리하는 거대한 함수”가 아니라, 여러 service를 조합하는 orchestration node가 될 수 있음을 보여 주기 때문이다. 실제 로봇에서도 task manager가 perception service, motion planning service, gripper service, safety check service를 순서대로 호출해 하나의 작업을 완성하는 구조가 자주 쓰인다.

  • Topic과 Service가 함께 쓰이는 이유

order_manager.py는 최종 응답을 client에게 돌려주는 것과 별도로 OrderLog 메시지를 order_log topic으로 publish한다. log_monitor.py는 이 topic을 subscribe해서 주문 성공/실패 누적 개수를 세고 로그를 출력한다.

여기서 service와 topic의 역할 차이가 분명해진다. order_client는 주문 처리 결과를 한 번 받아야 하므로 service response가 필요하다. 반면 log_monitor는 주문 요청의 당사자가 아니어도 전체 주문 처리 흐름을 계속 관찰해야 하므로 topic subscription이 적합하다. 하나의 사건이 “요청자에게는 response”이고 “감시자에게는 event stream”이 되는 것이다.

  • Action 실습이 보여 주는 장기 작업 구조

my_action/src/my_action_interfaces/action/PatrolTest.action은 waypoint 목록과 tolerance를 goal로 받고, 현재 waypoint index와 남은 거리를 feedback으로 보내며, 최종적으로 성공 여부와 메시지를 result로 돌려주는 구조다. Turtlesim patrol server는 /turtle1/pose를 subscribe하고 /turtle1/cmd_vel을 publish하면서 goal로 받은 waypoint를 차례로 따라간다.

이 실습에서 action이 필요한 이유는 patrol이 즉시 끝나는 계산이 아니기 때문이다. Client는 목표 waypoint 묶음을 보내고, server는 이동 중에 계속 remaining distance를 feedback으로 보낸다. 사용자는 중간에 cancel할 수 있고, server는 cancel 요청을 받으면 속도를 0으로 publish하고 작업을 정리한다. 이것이 service와 action의 가장 실질적인 차이다.

  • 실습 코드를 읽을 때의 기준

ROS2 코드를 볼 때는 파일 이름보다 책임을 먼저 봐야 한다. Interface package는 데이터 계약을 만든다. Node package는 그 계약을 사용해 publisher, subscriber, service server, service client, action server, action client를 구현한다. Launch package는 여러 node와 parameter를 한 번에 실행한다. 이 네 층을 구분하면 ROS2가 단순 명령어 모음이 아니라 분산 로봇 시스템을 구성하는 구조라는 점이 보인다.

  • 한 줄 정리

ROS2는 로봇 시스템을 node로 나누고, topic/service/action/launch/parameter를 통해 기능들이 협력하도록 만드는 분산 로봇 미들웨어다.

profile
AI 공부합니다

0개의 댓글