AEGLOSLAB ROS 2 Jazzy 대전

제1부 계산 그래프의 코어

1.5 QoS 심층 I — 정책과 호환성 매트릭스

1.5.1 이 장에서 배우는 것 / 선수 지식

QoS(Quality of Service)는 ROS 2가 ROS 1에서 가장 크게 달라진 지점이자, 실무에서 가장 많은 시간을 소모하는 지점이다. ROS 1의 통신은 사실상 하나의 모드(TCP 기반, 순서 보장, 연결 지향)였다. ROS 2는 그것을 여러 축의 정책으로 분해해 사용자에게 넘겼다. 얻은 것은 무선·손실 링크·실시간 상황에 맞출 수 있는 유연성이고, 잃은 것은 "별도 설정 없이 동작하는" 기본값이다.

이 장은 정책 하나하나를 정확히 정의하고, 연결이 성립하는 조건(호환성) 을 규칙으로 세운다. 프로파일 설계와 함정은 다음 장 1.6에서 다룬다.

배우는 것:

  • ROS 2가 노출하는 정책 전수 — 각 정책의 정의·값·기본값·비용.
  • RxO(Requested vs Offered) 원칙 — DDS의 호환성 판정 규칙과, "엄격한 쪽이 요청하면 실패"라는 방향성.
  • 호환성 매트릭스 — 어떤 조합이 연결되고 어떤 조합이 연결되지 않는가.
  • 내장 프로파일 — SensorDataQoS·SystemDefaultsQoS 등이 실제로 무엇으로 설정되는가.
  • QoS와 DDS의 관계 — ROS 2가 노출하지 않는 DDS 정책들, 그리고 XML로 내려가는 통로.
  • 정책이 경로의 어디서 작용하는가 — 1.4의 경로 그림 위에 정책을 겹쳐 본다.
  • 내용 필터(content filtered topic) — "무엇을 받을지"를 미들웨어에 맡기는 구독 옵션과 그 벤더 의존성(1.5.10).

선수 지식: 1.4(발행·구독 경로와 큐 구조). 0.7.5(CLI에서 QoS 불일치를 만난 경험)가 있으면 이해가 빠르다.

정직: 정책 정의는 OMG DDS 명세와 ROS 2 QoS 설계 문서에 근거한다. 기본값은 rmw 계층에서 정의되며 벤더·버전에 따라 세부가 다를 수 있다(확인 필요). 실제 적용된 값을 확인하는 정본은 ros2 topic info -v이며, 이 환경에서 실행 검증하지 않았다(목차 3).

검증 상태(추가): 이 장의 서술 가운데 6곳은 이후 회차에서 Jazzy 실물과 대조해 확인했다 — 본문의 (실행 확인) 표기가 그것이다. 표기가 없는 서술은 위 고지가 그대로 적용된다(목차 3의 검증 범위 표).

1.5.2 왜 QoS인가 — ROS 1과의 대비

ROS 1 TCP 중심 단일 모드 순서·신뢰성 보장 연결 지향 단순하지만 무선·실시간에 부적합 ROS 2 정책 조합 신뢰성·내구성·시한 각각 선택 맞추면 강력 틀리면 침묵

ROS 1의 TCP는 무선 링크에서 재전송이 누적되며 오래된 데이터가 최신 데이터를 막는 문제를 낳았다. 로봇 제어에서는 "1초 전 레이저 스캔을 확실히 받는 것"보다 "최신 스캔을 지금 받는 것"이 중요하다. QoS는 이 선택을 명시적으로 만든다.

대가는 침묵하는 실패다. QoS가 안 맞으면 연결이 성립하지 않고, 예외도 로그도 없다(1.4.6). 이 장의 실무적 목표는 그 침묵을 예측 가능하게 만드는 것이다.

1.5.3 정책 전수

1.5.3.1 History — 무엇을 얼마나 보관하나

값의미
KEEP_LAST(depth)최근 depth개만 보관. 넘치면 오래된 것부터 버림
KEEP_ALL자원 한계까지 전부 보관

KEEP_LAST가 기본이며 depth가 함께 지정된다(흔히 10). History는 발행·구독 양쪽에 각각 존재한다 — 1.4.4의 ①과 ③.

KEEP_ALL의 "자원 한계"는 DDS의 RESOURCE_LIMITS 정책이 정하며, ROS 2 API로는 직접 노출되지 않는다 — 벤더 XML로 내려가야 한다(3.7). 그래서 KEEP_ALL은 "무한 보관"이 아니라 "벤더 기본 한계까지 보관"이다.

함정: KEEP_ALL + RELIABLE은 히스토리가 차면 publish()를 블로킹시킨다(1.4.2). 실시간 루프에서 금물.

1.5.3.2 Reliability — 유실을 복구하나

값의미비용
RELIABLEACK·재전송으로 도달 보장(큐 안에서)재전송 트래픽, 지연 변동, 메모리
BEST_EFFORT보내고 잊음최소

"신뢰성"의 범위를 정확히 알아야 한다. RELIABLE은 네트워크 유실을 복구하지만, 히스토리에서 축출된 것은 복구하지 않는다. KEEP_LAST(5) + RELIABLE은 "최근 5개는 확실히 전달하려 노력한다"이지 "모든 메시지가 도착한다"가 아니다.

무선 환경에서 RELIABLE의 위험: 유실률이 높으면 재전송이 누적되어 대역을 잠식하고 지연이 폭증한다. 링크가 나쁠수록 더 많이 보내는 구조라 악순환이 된다(1.6·3.9).

1.5.3.3 Durability — 늦게 온 구독자에게도 주나

값의미
VOLATILE구독 시점 이후 데이터만
TRANSIENT_LOCAL발행자가 보관한 최근 데이터를 나중에 참여한 구독자에게도 전달

ROS 1의 latch=True에 해당하는 것이 TRANSIENT_LOCAL이다. 보관 개수는 History의 depth가 정한다.

DDS 명세에는 그 위로 TRANSIENT·PERSISTENT(외부 저장소 기반)가 있으나 ROS 2 API는 노출하지 않는다.

전형적 용도: /map, /robot_description, /tf_static, 정적 설정 토픽.

1.5.3.4 Deadline — 이 주기를 지키기로 약속한다

발행자는 "이 간격 안에 다음 메시지를 낸다"고 약속(offer) 하고, 구독자는 "이 간격 안에 받아야 한다"고 요구(request) 한다. 어기면 이벤트가 발생한다(1.4.6).

  • 발행 측: offered_deadline_missed
  • 구독 측: requested_deadline_missed

데드라인은 강제가 아니라 계약과 통보다. 어겼다고 무언가를 멈추지 않는다 — 알려줄 뿐이며, 대응은 애플리케이션이 한다. 센서 정지·프리즈 감지에 유용하다(6.9).

1.5.3.5 Lifespan — 오래된 데이터를 폐기한다

발행 후 이 기간이 지난 샘플은 전달되지 않고 버려진다. 오래된 명령이 뒤늦게 도착해 실행되는 사고를 막는다.

전형적 용도: 명령·목표 토픽(3초 지난 속도 명령은 위험하다), 시각 데이터.

함정: TRANSIENT_LOCAL + 짧은 lifespan은 서로 충돌한다 — 보관해 두라고 하면서 폐기하라고 하는 셈이다. 의도적 조합이 아니면 피한다.

1.5.3.6 Liveliness — 상대가 실행 중인가

값의미
AUTOMATIC미들웨어가 참가자 수준에서 자동 관리
MANUAL_BY_TOPIC발행자가 명시적으로 생존을 알림(assert_liveliness())

lease_duration 안에 생존 신호가 없으면 liveliness_changed 이벤트가 발생한다.

AUTOMATIC과 MANUAL_BY_TOPIC의 차이가 실무적으로 중요하다. AUTOMATIC은 프로세스가 실행 중이면 생존으로 본다 — 콜백이 데드락에 빠진 좀비 노드도 "실행 중"으로 보고된다. 실제 동작 여부를 감시하려면 MANUAL_BY_TOPIC으로 두고, 발행자가 정상 동작 시에만 생존을 선언해야 한다.

DDS 명세의 MANUAL_BY_PARTICIPANT는 ROS 2에서 지원이 축소·제거된 이력이 있다(확인 필요).

1.5.3.7 ROS 2가 노출하지 않는 DDS 정책들

DDS 정책용도ROS 2 노출
OWNERSHIP / OWNERSHIP_STRENGTH다중 발행자 중 하나만 채택(이중화)없음 — XML로
PARTITION논리적 격리없음 — XML로
RESOURCE_LIMITS큐·인스턴스 상한없음 — XML로
TIME_BASED_FILTER수신 측 다운샘플링없음 — XML로
DESTINATION_ORDER순서 기준(발행 시각 vs 수신 시각)없음
TRANSPORT_PRIORITY전송 우선순위없음
LATENCY_BUDGET배치 허용 지연없음

이것들이 필요하면 벤더 XML 프로파일로 내려간다(3.7·3.8). 다만 그 순간 RMW 이식성을 잃는다 — 설정이 벤더 전용이 된다. 이것이 ROS 2 QoS 설계의 근본 트레이드오프다: 공통 분모만 노출해 이식성을 지키되, 나머지는 벤더 통로로 미룬다.

OWNERSHIP이 없다는 사실은 이중화 설계에 직접 영향을 준다. 센서 두 대를 같은 토픽에 발행시켜 자동 페일오버하는 DDS 관용구를 ROS 2 API만으로는 못 쓴다 — 애플리케이션 레벨에서 선택 로직을 만들거나 XML로 내려가야 한다(14.9).

1.5.3.8 BEST_AVAILABLE — 상대에 맞춰 자동으로 낮춘다

지금까지 본 정책값 외에 값이 하나 더 있다. BEST_AVAILABLE은 Reliability·Durability·Liveliness·Deadline에 지정할 수 있으며, "이미 연결된 상대와 호환되는 가장 높은 수준을 골라 달라" 는 뜻이다(실행 확인: rmw/types.h, rclpy ReliabilityPolicy·DurabilityPolicy·LivelinessPolicy 열거형).

정책BEST_AVAILABLE이 하는 일
Reliability상대가 BEST_EFFORT뿐이면 BEST_EFFORT, 아니면 RELIABLE
Durability상대가 VOLATILE뿐이면 VOLATILE, 아니면 TRANSIENT_LOCAL
Liveliness상대에 맞춰 AUTOMATIC / MANUAL_BY_TOPIC
Deadline · Liveliness lease상대의 값에 맞춰 결정(센티넬 값 {9223372036, 854775806})

C++은 rclcpp::BestAvailableQoS, Python은 rclpy.qos.qos_profile_best_available, rmw는 rmw_qos_profile_best_available로 노출된다. 프로파일 자체는 KEEP_LAST(10) + 네 정책 전부 BEST_AVAILABLE이다(실행 확인: rmw/qos_profiles.h).

이것이 앞의 "연결되지 않는 조합" 문제를 도구 레벨에서 상당 부분 지웠다 — ros2 topic echo가 대표적이다(1.5.4의 마지막 문단).

함정 — 비용이 든다. rmw 헤더가 직접 경고한다: 결정은 디스커버리 시점에 이미 보이는 엔드포인트를 기준으로 내려지므로, 나중에 참여하는 엔드포인트와는 레이스가 난다. 먼저 참여한 발행자가 BEST_EFFORT뿐이어서 BEST_EFFORT로 정착했는데 뒤늦게 RELIABLE 발행자가 연결되면, 구독자는 이미 낮춰진 상태다. 결과가 실행 순서에 의존한다. 그래서 진단 도구에는 좋고, 운영 노드의 QoS를 이것으로 정하는 것은 권하지 않는다 — 노드 간 계약은 명시적으로 고정하는 편이 낫다(1.6).

1.5.4 호환성 — RxO 원칙

규칙 하나로 요약된다: 구독자가 요구(Requested)하는 수준이 발행자가 제공(Offered)하는 수준보다 강하면 연결되지 않는다.

예 아니오 발행자: Offered Requested ≤ Offered? 구독자: Requested 연결 성립 연결 없음 + incompatible_qos 이벤트

정책마다 "강함"의 순서가 정의된다.

Reliability: RELIABLE > BEST_EFFORT

발행자(Offered)구독자(Requested)결과
RELIABLERELIABLE✅ 연결 (신뢰)
RELIABLEBEST_EFFORT✅ 연결 (best effort로 동작)
BEST_EFFORTRELIABLE❌ 연결 안 됨
BEST_EFFORTBEST_EFFORT✅ 연결

Durability: TRANSIENT_LOCAL > VOLATILE

발행자구독자결과
TRANSIENT_LOCALTRANSIENT_LOCAL✅ 과거 데이터도 받음
TRANSIENT_LOCALVOLATILE✅ 이후 데이터만
VOLATILETRANSIENT_LOCAL❌ 연결 안 됨
VOLATILEVOLATILE✅

Deadline: 구독자의 기간이 더 짧으면(더 엄격하면) 실패.

발행자 offered구독자 requested결과
100 ms200 ms✅ (여유롭게 요구)
100 ms100 ms✅
100 ms50 ms❌ (더 엄격)
무한(기본)유한❌

Liveliness: 종류는 MANUAL_BY_TOPIC > AUTOMATIC, lease_duration은 Deadline과 같은 방향(구독자가 더 짧으면 실패).

History·Lifespan은 호환성 판정에 들어가지 않는다. 양쪽이 각자 자기 설정대로 동작한다. 그래서 depth가 달라도 연결은 된다 — 연결은 되는데 데이터가 빠지는 상황이 여기서 나온다.

실무 요약: "연결되지 않는" 조합은 사실상 셋뿐이다 — ① 발행 BEST_EFFORT + 구독 RELIABLE, ② 발행 VOLATILE + 구독 TRANSIENT_LOCAL, ③ 구독이 더 엄격한 deadline·liveliness. 현장에서 마주치는 QoS 불일치의 압도적 다수가 ①과 ②다.

기본값이 이 함정을 만든다. ROS 2 기본 프로파일은 RELIABLE+VOLATILE인데, 센서 드라이버들은 흔히 SensorDataQoS(BEST_EFFORT)로 발행한다. 직접 작성한 노드가 기본값으로 구독하면 ①에 정확히 걸린다.

Jazzy에서 달라진 것 — ros2 topic echo는 더 이상 여기 걸리지 않는다(실행 확인). 예전 배포판에서 "ros2 topic echo가 침묵한다"는 것이 이 함정의 대표 증상이었으나, Jazzy의 ros2 topic echo는 Reliability·Durability·Liveliness 기본값이 best_available 이라 이미 연결된 발행자에 맞춰 자동으로 낮춰 매칭된다(1.5.3.8). 실측했다 — BEST_EFFORT 발행자에 대해 플래그 없는 ros2 topic echo는 정상 수신하고, 오히려 --qos-reliability reliable을 강제하면 양쪽에 incompatible QoS ... Last incompatible policy: RELIABILITY 경고가 출력되며 끊긴다. 즉 Jazzy에서는 "안 보이면 best_effort를 붙여라"가 아니라, "옵션을 붙이지 마라"가 맞다. 반대로 직접 작성한 구독 노드는 이 자동 보정을 받지 못한다 — 명시하지 않으면 기본이 RELIABLE이므로 ①에 그대로 걸린다. CLI로는 보이는데 내 노드로는 안 들어오는 혼동을 일으키는 상황이 여기서 나온다(0.7.5).

1.5.5 내장 프로파일

rclcpp가 제공하는 이름 붙은 프로파일이다. 수동으로 정책을 조립하기 전에 이것부터 본다.

프로파일ReliabilityDurabilityHistory용도
rclcpp::QoS(10) (기본)RELIABLEVOLATILEKEEP_LAST(10)범용
SensorDataQoS()BEST_EFFORTVOLATILEKEEP_LAST(5)센서 스트림
ServicesQoS()RELIABLEVOLATILEKEEP_LAST(10)서비스
ParametersQoS()RELIABLEVOLATILEKEEP_LAST(1000)파라미터
ParameterEventsQoS()RELIABLEVOLATILEKEEP_LAST(1000)/parameter_events
RosoutQoS()RELIABLETRANSIENT_LOCALKEEP_LAST(1000)/rosout
ClockQoS()BEST_EFFORTVOLATILEKEEP_LAST(1)/clock(1.6.11)
SystemDefaultsQoS()미들웨어 기본미들웨어 기본미들웨어 기본XML로 위임
BestAvailableQoS()BEST_AVAILABLEBEST_AVAILABLEKEEP_LAST(10)진단·범용 도구(1.5.3.8)

위 표는 Jazzy 실물과 대조해 확인했다(실행 확인) — rmw/qos_profiles.h의 rmw_qos_profile_default·_sensor_data·_services_default·_parameters·_parameter_events·_system_default·_best_available, 그리고 rcl/logging_rosout.h의 rcl_qos_profile_rosout_default가 근거다.

표에 안 보이는 것 하나 — RosoutQoS에는 lifespan = 10초가 걸려 있다(실행 확인). 다른 내장 프로파일은 전부 lifespan이 무한(DEFAULT)인데 /rosout만 다르다. TRANSIENT_LOCAL이라 늦게 참여한 구독자도 과거 로그를 받지만 10초보다 오래된 것은 이미 폐기됐다. ros2 topic echo /rosout으로 한참 전 로그를 조회하려다 실패하는 이유가 이것이다 — 그 용도로는 로그 파일(ROS_LOG_DIR)을 봐야 한다(1.11.3).

SystemDefaultsQoS()의 의미가 중요하다. 이것은 "정책을 지정하지 않겠다"는 선언이며, 실제 값은 DDS 벤더의 기본값 또는 XML 프로파일이 정한다. 즉 코드를 고치지 않고 XML로 QoS를 바꿀 수 있는 통로다(3.7). 운영에서 튜닝 여지를 남기고 싶을 때 쓴다. 대가는 예측 가능성 상실 — 코드만 읽어서는 실제 QoS를 알 수 없다.

조립 문법:

auto qos = rclcpp::QoS(rclcpp::KeepLast(5))
             .best_effort()
             .durability_volatile()
             .deadline(std::chrono::milliseconds(100))
             .lifespan(std::chrono::seconds(1))
             .liveliness(RMW_QOS_POLICY_LIVELINESS_MANUAL_BY_TOPIC)
             .liveliness_lease_duration(std::chrono::seconds(2));
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy
qos = QoSProfile(
    history=HistoryPolicy.KEEP_LAST, depth=5,
    reliability=ReliabilityPolicy.BEST_EFFORT,
    durability=DurabilityPolicy.VOLATILE)

1.5.6 정책이 경로의 어디서 작용하나

1.4의 경로 그림 위에 정책을 겹쳐 보면 각 정책의 실체가 분명해진다.

publish() DataWriter 히스토리 History·Durability 전송·재전송 Reliability 폐기 판정 Lifespan DataReader 히스토리 History 감시·통보 Deadline·Liveliness executor → 콜백
정책작용 지점주 효과
History양쪽 큐보관량·유실
Reliability전송 계층재전송·블로킹
Durability발행 큐 + 신규 연결늦은 구독자
Lifespan전달 직전오래된 것 폐기
Deadline감시자이벤트 통보
Liveliness감시자이벤트 통보

중요한 관찰: 어떤 정책도 executor 이후에는 작용하지 않는다. 콜백이 느려 생기는 문제는 QoS로 못 고친다(1.4.3). QoS는 "무엇을 언제까지 보관하고 전달할지"를 정할 뿐, "얼마나 빨리 처리할지"는 실행 모델의 영역이다(2.3·2.4). QoS를 조정하고 있는데 문제가 안 풀린다면, 문제가 QoS 층에 없을 가능성을 먼저 의심하라.

1.5.7 QoS 확인·진단

ros2 topic info -v /scan

이 명령이 양쪽 엔드포인트의 실제 QoS를 보여준다. QoS 문제 진단의 시작이자 대개 끝이다. 다만 전부는 아니다 — History (Depth)는 디스커버리로 교환되지 않아 상대 엔드포인트에 대해 UNKNOWN으로 표시된다(3.3.2). 큐 깊이는 RxO 판정에 쓰이지 않는 로컬 자원 정책이기 때문이다.

# Jazzy 기본값(best_available)이 자동으로 맞춘다 — 대개 옵션이 필요 없다
ros2 topic echo /scan

# 협상 결과를 일부러 고정하고 싶을 때만 명시
ros2 topic echo /scan --qos-reliability best_effort
ros2 topic echo /map --qos-durability transient_local

코드에서 상대의 QoS 확인:

for (const auto & info : node->get_publishers_info_by_topic("/scan")) {
  const auto & q = info.qos_profile();
  // q.reliability(), q.durability(), q.depth() …
}

이것으로 적응형 구독을 만들 수 있다 — 발행자가 BEST_EFFORT면 따라가고, RELIABLE이면 RELIABLE로. 범용 도구·시각화 노드에서 유용하다. 다만 경합이 있다(구독 생성 시점에 발행자가 아직 없을 수 있다).

직접 작성하기 전에, 바로 이 동작을 미들웨어가 이미 제공한다는 점을 보라 — rclcpp::BestAvailableQoS(1.5.3.8)가 정확히 같은 일을 한다. 경합 문제도 똑같이 안고 있으므로, 직접 구현해도 같은 한계에 부딪힌다. 진단·시각화 도구라면 내장 쪽을 쓰고, 운영 노드라면 둘 다 피하고 QoS를 명시적으로 고정하는 편이 낫다.

QoS 오버라이드(파라미터로 재정의): 코드 재빌드 없이 실행 시 QoS를 바꾸는 기능이 제공된다.

rclcpp::SubscriptionOptions options;
options.qos_overriding_options =
  rclcpp::QosOverridingOptions::with_default_policies();
sub_ = create_subscription<T>("scan", rclcpp::SensorDataQoS(), cb, options);

이후 qos_overrides./scan.subscription.reliability 형태의 파라미터로 조정한다(확인 필요: 정확한 파라미터 이름 규칙). 운영에서 QoS를 튜닝해야 하는 노드라면 켜 둘 가치가 있다 — 현장에서 재빌드 없이 무선 환경에 맞출 수 있다. 대가는 파라미터 표면 증가와, "실제 QoS가 무엇인지 코드만 봐서는 모른다"는 점이다.

1.5.10 내용 필터 — 미들웨어에게 걸러 달라고 하기

번호에 대하여: 이 절은 나중에 추가되었다. 기존 번호를 밀지 않는다는 규약(CLD-03)에 따라 번호는 끝에 붙이고, 읽는 흐름을 위해 함정 카탈로그 앞에 놓았다.

QoS는 어떻게 전달할지를 정한다. 내용 필터(content filtered topic)는 무엇을 받을지를 정한다. 정책이 아니라 구독 옵션이지만, 실무에서 QoS와 같은 자리에서 판단하게 되므로 여기서 다룬다.

문제 상황은 흔하다. 로봇 20대가 한 도메인에 있고, 각 로봇이 /fleet/status에 상태를 업로드한다. 관제 노드가 3번 로봇만 보고 싶다. 구독 콜백에서 if (msg.robot_id != 3) return;으로 거르면 될까? 된다 — 다만 이미 늦었다. 그 메시지는 이미 네트워크를 건너왔고, 역직렬화됐고, 큐를 차지했다. 버리는 것은 마지막 한 걸음뿐이다.

내용 필터는 그 판단을 미들웨어로 내려보낸다.

rclcpp::SubscriptionOptions options;
options.content_filter_options.filter_expression = "robot_id = %0";
options.content_filter_options.expression_parameters = {"3"};

sub_ = create_subscription<fleet_msgs::msg::Status>(
  "/fleet/status", 10, callback, options);

if (!sub_->is_cft_enabled()) {
  RCLCPP_WARN(get_logger(), "내용 필터가 적용되지 않았다 — 전부 들어온다");
}
필드의미
filter_expressionSQL의 WHERE 절과 비슷한 식. 필드 이름과 비교 연산자를 쓴다. %0·%1… 은 자리표시자
expression_parameters자리표시자에 들어갈 값들. 최대 100개(헤더 원문)
is_cft_enabled()이 구독에 필터가 실제로 걸렸는지

문자열 상수는 식 안에서 작은따옴표로 감싼다 — expression_parameters = {"'hello'"}.

1.5.10.1 실측 — 정말 걸러지는가, 그리고 어디서 걸러지는가

// 500 ms마다 "world"와 "hello"를 번갈아 발행, 구독은 data = 'hello'만 요청
RMWis_cft_enabled()발행 15건수신
rmw_fastrtps_cpp(Jazzy 기본)1157건 — 전부 hello
rmw_cyclonedds_cpp01515건 — world 8건 포함

실행 확인(2026-08-16): 두 RMW로 같은 바이너리를 실행해 얻은 결과다. Fast DDS에서는 필터가 실제로 동작했고, Cyclone DDS에서는 필터가 적용되지 않았다.

여기서 결정적인 함정이 나온다.

함정: 필터가 지원되지 않아도 오류가 나지 않는다. 조용히 전부 들어온다. Cyclone DDS 실행에서 경고 로그 한 줄 없이 world가 8건 그대로 도착했다. 필터를 대역 절감의 근거로 삼았다면 그 가정이 소리 없이 무너진 것이고, 필터를 정확성의 근거로 삼았다면(예: "3번 로봇 명령만 온다고 가정") 더 심각한 종류의 버그가 된다. is_cft_enabled()를 확인하고, 어긋나면 로그를 남기라. 그리고 애플리케이션 레벨 검사를 없애지 말라 — 필터는 최적화이지 보증이 아니다.

필터가 어디서 평가되는지도 벤더·설정에 따라 다르다(확인 필요). 구독자 쪽에서 걸러지면 CPU·큐는 아끼지만 대역은 그대로고, 발행자 쪽에서 걸러지면 대역까지 아낀다. 대역 절감이 목적이라면 자신의 환경에서 실제 트래픽을 측정해서(1.3.9) 확인하는 것이 정본이다.

1.5.10.2 언제 쓰나 — 그리고 대안과의 비교

방법거르는 위치이득대가
콜백에서 if애플리케이션단순·이식성 100%대역·역직렬화·큐 전부 소모
토픽을 나눈다(/robot3/status)그래프 구조확실·투명·벤더 무관토픽 수 증가, 디스커버리 비용(3.3.4)
내용 필터미들웨어코드·그래프 변경 최소벤더 의존, 조용한 실패, 식의 표현력 한계

견해: 기본 선택은 "토픽을 나눈다"이다. 이름으로 갈라 두면 누가 무엇을 받는지가 그래프에 드러나고(1.1), 어느 RMW에서나 같게 동작하며, 디버깅이 쉽다. 내용 필터는 토픽을 나눌 수 없을 때 — 필드 값이 연속적이거나(임계값 이상만), 구독자마다 조건이 다르거나, 발행자를 고칠 수 없을 때 — 의 수단이다. 이것은 판단이며, 벤더가 고정된 시스템에서 대역이 병목이라면 다른 결론이 정당하다.

함정: 필터 식은 타입 정의에 묶인다. 필드 이름을 바꾸면 식이 깨지는데, 그 깨짐이 컴파일 오류가 아니라 런타임 동작 변화로 나타난다(전부 오거나, 아무것도 안 오거나). 인터페이스를 바꿀 때 필터 식도 함께 검토하라(1.2.8).

대용량 파이프라인에서 무엇을 어디서 줄일지의 큰 그림은 7.8.6, 벤더별 기능 차이의 일반론은 3.5.5·3.6에 있다.

1.5.8 함정 카탈로그

#함정증상대처
1센서(BEST_EFFORT)를 기본 QoS로 구독아무것도 안 옴SensorDataQoS()(1.5.4)
2/map을 VOLATILE로 구독지도가 안 옴transient_local
3RELIABLE이면 유실이 없다고 믿음여전히 빠짐히스토리 넘침은 별개(1.4.4)
4KEEP_ALL+RELIABLE을 제어 루프에publish() 블로킹KEEP_LAST
5depth를 크게 설정해 안전 확보지연·메모리 증가지연 예산으로 역산(1.6)
6deadline을 구독 측만 엄격히연결 자체가 안 됨양쪽 정합
7AUTOMATIC liveliness로 좀비 감지 시도데드락 노드가 "생존"으로 보고MANUAL_BY_TOPIC
8TRANSIENT_LOCAL+짧은 lifespan보관과 폐기가 충돌의도 확인
9OWNERSHIP으로 이중화 시도ROS 2 API에 없음XML 또는 앱 레벨(1.5.3.7)
10XML로 튜닝 후 벤더 교체설정 전체가 무효이식성 대가 인지(3.6)
11QoS만 조정하며 지연 문제 해결 시도안 풀림executor 층 의심(1.5.6)
12무선에서 대용량 RELIABLE재전송 폭주·대역 붕괴1.6·3.9
13내용 필터를 정확성의 보증으로 사용RMW를 바꾸자 조용히 전부 수신is_cft_enabled() 확인 + 앱 레벨 검사 유지(1.5.10.1)

1.5.9 체크포인트

  • ROS 2가 QoS를 도입하며 얻은 것과 잃은 것을 각각 말할 수 있는가.
  • 여섯 정책(History·Reliability·Durability·Deadline·Lifespan·Liveliness)의 정의와 값을 보지 않고 나열할 수 있는가.
  • "RELIABLE이 보장하는 범위"를 히스토리와 연결해 정확히 진술할 수 있는가.
  • RxO 원칙을 한 문장으로 말하고, 연결이 실패하는 세 가지 대표 조합을 즉시 댈 수 있는가.
  • 왜 기본 QoS로 센서 토픽을 구독하면 아무것도 안 오는 경우가 많은가.
  • History와 Lifespan이 호환성 판정에 들어가지 않는다는 사실의 실무적 결과는 무엇인가.
  • AUTOMATIC liveliness로는 감지할 수 없는 장애 유형은 무엇인가.
  • SystemDefaultsQoS()가 여는 통로와 그 대가를 설명할 수 있는가.
  • ROS 2가 노출하지 않는 DDS 정책 중 이중화 설계에 영향을 주는 것은 무엇이며, 대안은 무엇인가.
  • 각 정책이 발행·구독 경로의 어느 지점에서 작용하는지 그릴 수 있는가. QoS로는 절대 못 고치는 문제는 어떤 종류인가.
  • 내용 필터가 지원되지 않는 RMW로 바꿨을 때 무슨 일이 일어나는가 — 오류인가, 침묵인가. 그 사실이 설계에 주는 제약은?
  • "토픽을 나눈다"와 "내용 필터"를 같은 문제에 놓고 비교할 때, 어느 쪽을 기본으로 삼고 왜 그런가.

다음 장 1.6에서는 이 정책들을 실제 시나리오에 맞춰 조합한다 — 센서·명령·지도·상태·진단별 프로파일 설계, depth를 지연 예산에서 역산하는 법, 무선·다중 로봇에서의 함정.