ROS 2 토픽·서비스·액션 비교와 QoS 선택 기준
연속 센서 스트리밍용 토픽, 단발 질의응답용 서비스, 장기 목표 제어용 액션의 선택 기준을 정리하고, 토픽 이름이 같아도 데이터가 오가지 않는 QoS 불일치 원인과 해결법을 다룹니다.
무료 공개 · 최근 수정
응답 필요 여부와 소요 시간으로 고르는 세 가지 통신 방식
ROS 2 입문자가 가장 자주 겪는 당황스러운 순간이 있습니다. 카메라 드라이버 노드가 /camera/image_raw라는 이름으로 영상을 발행하고 있고, 내가 작성한 파이썬 구독 노드도 오타 없이 똑같은 /camera/image_raw를 구독했는데 콜백 함수가 단 한 번도 실행되지 않는 현상입니다. 에러 메시지도 뜨지 않고 화면만 멈춰 있으니 코드 로직 문제로 오해하기 쉽습니다.
이 문제의 원인은 대부분 통신 통로의 이름이나 데이터 형식이 아니라 전송 품질 약속이 어긋났기 때문입니다. 노드들이 서로를 찾은 뒤(ROS 2 계층 구조와 DDS 무마스터 자동 디스커버리 참조) 데이터를 주고받을 때는 작업 성격에 맞는 통신 패턴과 전송 품질을 함께 맞춰야 합니다.
로봇 내부에서 오가는 데이터는 크게 세 부류로 나뉩니다. 쉴 새 없이 쏟아지는 센서 측정값, 즉시 확인받아야 하는 설정 변경 요청, 그리고 완료까지 몇 초에서 몇 분이 걸리는 물리적 이동 명령입니다. ROS 2는 이 세 가지 상황에 맞춰 각기 다른 통신 인터페이스를 제공하며, 각각의 역할과 전송 품질 보증 방식을 비교하면 아래 다이어그램과 같습니다.
그림: 작업 성격에 따른 토픽·서비스·액션 선택 기준과 발행자·구독자 사이의 QoS 호환성 매칭 구조
어떤 통신 방식을 써야 할지 고민될 때는 두 가지 질문만 던져 보면 됩니다. 첫째, "보낸 쪽이 상대방의 처리 결과 응답을 꼭 받아야 하는가?" 둘째, "그 작업이 끝날 때까지 시간이 오래 걸려 중간 경과 보고나 취소 기능이 필요한가?"입니다.
| 구분 | 토픽 (Topic) | 서비스 (Service) | 액션 (Action) |
|---|---|---|---|
| 통신 구조 | 단방향 연속 발행·구독 (1:N, N:M) | 1회 요청 · 1회 응답 (Client-Server) | 목표 전송 · 주기적 피드백 · 최종 결과 |
| 인터페이스 확장자 | .msg (단일 구조체) | .srv (---로 요청·응답 구분) | .action (--- 두 개로 목표·결과·피드백 구분) |
| 중도 취소 가능 여부 | 해당 없음 (연속 스트림) | 불가능 (응답까지 대기) | 가능 (실행 중 취소 요청 지원) |
| 대표 활용 사례 | 카메라 프레임, 라이다 스캔, 관절 각도 | 센서 영점 초기화, 지도 저장, 모드 전환 | 자율주행 목표 지점 이동, 로봇 팔 궤적 제어 |
연속 센서 스트림을 다루는 토픽과 단발 요청을 처리하는 서비스
주기적으로 계속 발생하는 데이터를 흘려보낼 때는 토픽(Topic)을 씁니다. 발행자(Publisher)는 구독자가 한 개든 열 개든 상관하지 않고 정해진 주기로 메시지를 내보냅니다. 결과를 기다리지 않고 던지기만 하는 구조(Fire-and-Forget)이므로, 카메라 영상 하나를 비전 AI 노드와 화면 시각화 도구(RViz2), 데이터 기록기(rosbag2)가 동시에 받아 가도 카메라 노드의 부담이 늘어나지 않습니다(로봇 코드를 노드로 나누는 이유와 ROS 2 분산 구조 참조).
반면 "로봇의 비상 브레이크를 해제해 달라"거나 "현재 배터리 잔량을 알려 달라"는 명령을 토픽으로 보내면 곤란합니다. 보낸 쪽에서는 상대 노드가 그 메시지를 제대로 받았는지, 명령이 성공했는지 확인할 길이 없기 때문입니다.
이렇게 요청에 대한 성공 여부나 결과값을 반드시 확인해야 하는 단발성 작업에는 서비스(Service)를 사용합니다. 서비스 정의 파일(SetGripperForce.srv)은 구분선(---) 하나를 사이에 두고 위쪽에는 클라이언트가 보낼 요청(Request) 필드를, 아래쪽에는 서버가 돌려줄 응답(Response) 필드를 적습니다.
float32 target_force_newton
---
bool success
string status_message서비스를 쓸 때 주의할 점은 처리 시간이 짧은 작업에만 써야 한다는 것입니다. 만약 완료하는 데 10초가 걸리는 로봇 팔 이동을 서비스로 구현하면, 클라이언트는 10초 동안 로봇 팔이 어디까지 움직였는지 전혀 알 수 없고 앞에 장애물이 나타나도 중간에 명령을 취소할 수 없습니다.
장기 주행·조작 작업을 제어하는 ROS 2 액션의 3요소
몇 초 이상 걸리는 물리적 동작을 지시할 때는 ROS 2 액션(Action)을 선택합니다. 액션은 목표(Goal)를 보낸 뒤 작업이 끝날 때까지 현재 진행률인 피드백(Feedback)을 주기적으로 받아 보고, 완료되면 최종 결과(Result)를 수신하며, 필요할 때 실행 중인 작업을 즉시 취소(Cancel)할 수 있는 양방향 인터페이스입니다.
[참고] 피지컬 AI의 행동(Action)과 ROS 2 액션의 구분 강화학습이나 시각-언어-행동(VLA) 모델에서 말하는 액션(Action)은 모델이 매 주기 출력하는 관절 각도 변화량(
Δq)이나 속도 명령 값 자체를 뜻합니다. 반면 ROS 2 아키텍처에서 말하는 액션은 목표·피드백·결과 3요소로 장기 작업을 관리하는 통신 인터페이스를 가리킵니다.
흥미롭게도 액션은 완전히 새로운 네트워크 프로토콜이 아니라, 내부적으로 서비스 3개(목표 전송, 취소 요청, 최종 결과 요청)와 토픽 2개(주기적 피드백 발행, 상태 알림)를 묶어 만든 복합 상태 기계입니다. 액션 정의 파일(NavigateToWaypoint.action)은 구분선 두 개를 사용해 목표·최종 결과·중간 피드백 세 영역을 차례대로 정의합니다.
float64 target_x
float64 target_y
---
bool arrived
float32 total_elapsed_sec
---
float32 remaining_distance_meter토픽 이름이 같아도 통신이 안 되는 이유: QoS 정책과 호환성 표
세 가지 통신 방식을 골랐다면 마지막 관문은 서비스 품질(Quality of Service; QoS) 설정입니다. ROS 1은 TCP처럼 모든 패킷을 무조건 재전송하거나 UDP처럼 마구 보내는 수준만 선택할 수 있었지만, ROS 2는 DDS 미들웨어 위에서 데이터 성격에 따라 정교한 전송 정책을 지정합니다.
초보자가 가장 먼저 알아야 할 핵심 QoS 정책은 네 가지입니다.
- 신뢰성(Reliability): 패킷이 유실되었을 때 끝까지 재전송해 도착을 보장할지(
RELIABLE), 아니면 재전송하느라 시간을 끌지 않고 유실을 감수하며 최신 데이터를 가장 빨리 받을지(BEST_EFFORT) 결정합니다. - 내구성(Durability): 구독자가 뒤늦게 켜졌을 때 연결 이후의 새 메시지만 받을지(
VOLATILE), 아니면 발행자가 이전에 보냈던 최근 메시지를 보관해 두었다가 늦게 들어온 구독자에게도 챙겨 줄지(TRANSIENT_LOCAL) 정합니다. 로봇의 2D 격자 지도(/map)나 외형 모델(/robot_description)처럼 한 번만 발행되는 데이터에TRANSIENT_LOCAL을 씁니다. - 이력(History)과 깊이(Depth): 처리 속도가 느려 메시지가 밀릴 때 최근
N개만 큐에 남길지(KEEP_LAST와depth=N), 모든 메시지를 다 쌓아 둘지(KEEP_ALL) 정합니다.
문제는 발행자와 구독자의 QoS 설정이 호환되지 않으면 DDS가 아예 연결을 맺지 않는다는 점입니다. 글 첫머리에서 본 "카메라 토픽 이름이 같은데도 콜백이 안 불리는 현상"이 바로 여기서 발생합니다.
| 발행자 (Publisher) 신뢰성 | 구독자 (Subscriber) 신뢰성 | 통신 성립 여부 및 이유 |
|---|---|---|
RELIABLE (무손실 보장) | RELIABLE (무손실 요구) | 성립 (양측 모두 유실 없는 재전송에 합의) |
RELIABLE (무손실 보장) | BEST_EFFORT (유실 허용) | 성립 (발행자가 구독자의 요구 수준 이상을 제공) |
BEST_EFFORT (유실 허용) | RELIABLE (무손실 요구) | 불성립 (수신 0건) — 유실을 허용하는 발행자가 무손실 요구를 만족시킬 수 없음 |
BEST_EFFORT (유실 허용) | BEST_EFFORT (유실 허용) | 성립 (양측 모두 최신 속도 우선에 합의) |
대부분의 카메라·라이다 센서 드라이버는 33ms마다 새 프레임이 나오므로 지나간 프레임을 재전송받지 않으려고 BEST_EFFORT로 토픽을 발행합니다. 그런데 rclpy나 rclcpp에서 구독자를 만들 때 숫자(10)만 적으면 기본값인 RELIABLE이 적용됩니다. 결과적으로 BEST_EFFORT(발행자)와 RELIABLE(구독자)이 만나 통신이 거부되는 것입니다.
CLI 도구로 통신 상태와 QoS 불일치 진단하기
토픽 데이터가 들어오지 않을 때는 코드를 추측하기보다 터미널에서 ros2 topic info에 -v(상세 보기) 옵션을 붙여 양쪽의 QoS 프로필을 대조해 보는 것이 가장 빠릅니다.
ros2 topic info /scan -v실행 결과에서 발행자와 구독자의 Reliability 항목을 나란히 비교하면 불일치 지점이 바로 드러납니다.
Publisher count: 1
Node name: lidar_driver
QoS Profile:
Reliability: BEST_EFFORT
Subscription count: 1
Node name: my_obstacle_checker
QoS Profile:
Reliability: RELIABLE원인을 찾았다면 구독자 코드에서 센서 전용 내장 프로필(qos_profile_sensor_data)을 지정하거나 신뢰성을 BEST_EFFORT로 맞춰 주면 즉시 데이터가 들어옵니다.
import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
class ObstacleChecker(Node):
def __init__(self):
super().__init__('my_obstacle_checker')
# 기본 깊이(10) 대신 센서 데이터용 QoS(BEST_EFFORT, KEEP_LAST 5)를 지정
self.sub = self.create_subscription(
LaserScan,
'/scan',
self.scan_callback,
qos_profile_sensor_data
)
def scan_callback(self, msg: LaserScan):
self.get_logger().info(f'정면 거리 측정값: {msg.ranges[0]:.2f}m')[참고] 토픽의 실제 발행 주기(Hz) 측정하기 통신이 연결된 뒤 데이터가 제 속도로 들어오는지 확인하려면
ros2 topic hz /scan명령을 실행합니다. 초당 평균 발행 횟수와 최소·최대 간격 편차(Standard Deviation)를 실시간 측정값으로 보여 주어 센서 지연 여부를 바로 판단할 수 있습니다.
노드 바깥의 통신 통로와 품질 규칙까지 맞췄다면, 이제 메시지가 도착했을 때 노드 내부에서 함수가 어떤 순서와 스레드로 실행되는지 알아볼 차례입니다. 시리즈의 마지막 글인 노드와 프로세스의 차이와 콜백 실행 원리: ROS 2 Executor와 콜백 그룹에서 노드 내부의 스케줄링 구조를 살펴봅니다.
참고 문서
설명이 어렵거나 잘못된 부분을 발견하셨나요?
문서 수정 의견 보내기