ROS 2 토픽 이름이 같은데 메시지가 안 올 때: QoS 호환성을 먼저 계산하기
publisher: BEST_EFFORT, VOLATILE, depth=100
subscription: RELIABLE, VOLATILE, depth=10
ROS 2 토픽 이름이 같은데 메시지가 안 올 때, 이 두 줄에서 먼저 볼 값은 depth 100과 10이 아니다. BEST_EFFORT를 제공하는 publisher가 RELIABLE을 요청하는 subscription을 만족할 수 있는지부터 읽어야 한다. 답이 아니면 두 endpoint는 연결되지 않으며, 더 큰 queue도 이 판정을 바꾸지 못한다.
이 글에서는 이런 설정을 위에서 아래로 읽다가 처음 0이 나온 정책에서 멈추는 방법을 쓴다. 필요한 선수 지식은 publisher가 메시지를 보내고 subscription이 받는다는 점, 같은 topic에서 message type도 같아야 한다는 점, 모든 조건이 참이어야 하는 논리 AND뿐이다. 계산을 마치면 “QoS 불일치부터 고칠지, 연결 뒤의 보관·처리 문제를 조사할지”를 나눌 수 있다.
첫 번째 0은 reliability에서 나올 수 있다
QoS(Quality of Service, 통신 품질 정책)는 ROS 2 통신의 전달·보관·시간 조건을 묶은 설정이다. Publisher는 자신이 줄 수 있는 품질을 제공(Offered)하고, subscription은 받아들일 최소 품질을 요청(Requested)한다. 두 값이 똑같아야 하는 대칭 비교가 아니라, 제공값이 요청값을 만족하는지 보는 방향성 있는 비교다.
첫 분기는 reliability(신뢰성)다. BEST_EFFORT는 전달을 시도하지만 손실될 수 있고, RELIABLE은 재시도를 포함해 전달을 보장하는 방향의 정책이다. 다만 RELIABLE이 응용 전체의 무손실, 지연시간 상한이나 안전까지 보장한다는 뜻은 아니다. 2026년 9월 20일 KST에 확인한 ROS 2 QoS 공식 문서의 고정 커밋의 호환표를 0과 1로 옮기면 다음과 같다.
| Publisher 제공 | Subscription 요청 | 호환 | R |
|---|---|---|---|
| Best effort | Best effort | 예 | 1 |
| Best effort | Reliable | 아니요 | 0 |
| Reliable | Best effort | 예 | 1 |
| Reliable | Reliable | 예 | 1 |
도입의 설정은 첫 표의 둘째 줄에 걸린다. R=0이므로 여기서 멈춘다. Reliable subscription의 요청이 best-effort publisher의 제공 범위보다 엄격하기 때문이다. 반대 방향인 reliable publisher와 best-effort subscription은 R=1이다. “값이 다르면 실패”라고 외우면 이 비대칭을 놓친다.
R=0인 상태에서 depth를 10에서 100으로 늘려도 reliability 제공값은 바뀌지 않는다. 이 경우 기록할 첫 수정 후보는 queue 크기가 아니라 reliability 불일치다.
Reliability가 1일 때만 durability로 내려간다
Reliability가 통과했다면 durability(지속성)를 같은 방향으로 읽는다. TRANSIENT_LOCAL은 늦게 연결된 subscription을 위해 publisher가 표본을 보관하는 정책이고, VOLATILE은 과거 표본을 보관하려고 하지 않는다.
| Publisher 제공 | Subscription 요청 | 호환 | 구독자가 받는 범위 | D |
|---|---|---|---|---|
| Volatile | Volatile | 예 | 연결 뒤 새 메시지 | 1 |
| Volatile | Transient local | 아니요 | 통신 없음 | 0 |
| Transient local | Volatile | 예 | 연결 뒤 새 메시지 | 1 |
| Transient local | Transient local | 예 | 새 메시지와 보관된 과거 메시지 | 1 |
예를 들어 publisher가 RELIABLE, VOLATILE을 제공하고 subscription이 BEST_EFFORT, TRANSIENT_LOCAL을 요청한다고 하자. Reliability는 R=1이지만, volatile publisher는 transient-local 요청을 만족하지 못하므로 D=0이다. 진단은 두 번째 표에서 멈춘다.
이때도 depth를 늘릴 차례가 아니다. Publisher가 과거 표본을 제공해야 하는지, 아니면 subscription이 정말 과거 표본을 요구해야 하는지를 먼저 결정한다. 반대로 transient-local publisher와 volatile subscription은 호환되지만, 그 subscription은 연결 뒤의 새 메시지만 받는다.

두 값을 곱하되 연결 보증으로 읽지 않는다
지금까지의 두 분기를 한 줄에 적기 위해 reliability 호환 여부를 R, durability 호환 여부를 D라고 두자. 호환이면 1, 아니면 0이다. 두 조건이 모두 참이어야 한다는 AND는 다음 곱셈으로 검산할 수 있다.
C = R × D
R, D, C에는 단위가 없다. C=0이면 두 정책 가운데 적어도 하나가 endpoint 연결을 막는다. C=1은 reliability와 durability만 통과했다는 뜻이다. 실제 전달 여부, 전달률, 지연, 전체 QoS 호환을 나타내는 점수나 확률이 아니다.
이 곱셈은 ROS 2 내부 구현식도 아니다. 공식 문서는 호환성에 영향을 주는 모든 정책이 맞아야 한다고 설명한다. 실제 RMW API는 QoS profile 전체를 검사해 OK, WARNING, ERROR를 반환하며, system default나 unknown 값이 있어 판정할 수 없으면 WARNING이 될 수 있다. 이런 값에는 임의로 0이나 1을 주지 말고 RMW QoS 호환성 API의 고정 소스를 기준으로 profile 전체를 확인해야 한다.
설명용 네 사례를 이 좁은 모델에 넣으면 멈출 위치가 드러난다.
| 설정 | R |
D |
C=R×D |
다음 행동 |
|---|---|---|---|---|
| Best effort → Reliable, Volatile → Volatile | 0 | 1 | 0 | Reliability에서 중단 |
| Reliable → Best effort, Volatile → Volatile | 1 | 1 | 1 | 두 정책 밖의 원인으로 이동 |
| Reliable → Best effort, Volatile → Transient local | 1 | 0 | 0 | Durability에서 중단 |
| Best effort → Reliable, depth 10 → 100 | 0 | 1 | 0 | Depth 변경을 보류 |
마지막 줄의 10과 100은 권장 depth가 아니다. Queue를 키워도 reliability 플래그가 0에서 1로 바뀌지 않는다는 반례를 위한 숫자다.
1과 1 다음에는 빈 화면의 시점을 묻는다
R=1, D=1인데도 화면이 비어 있다면 “연결 성공”이라고 끝내지 말고 언제 구독을 시작했는지 묻는다. Volatile publisher와 volatile subscription은 호환되지만 late joiner, 즉 발행 뒤 늦게 참여한 구독자는 과거 표본을 받지 않는다. Publisher가 이미 발행을 끝냈고 새 메시지도 없다면 수신 0건이 될 수 있다. 이것은 durability 불일치도 depth 부족도 아니다.
새 메시지가 계속 발행되고 endpoint도 match된 뒤 일부 표본이 빠지거나 subscription callback이 발행 속도를 따라가지 못한다면 그때 history와 depth를 조사한다. History(이력)는 표본을 어떻게 보관할지 정한다. KEEP_LAST는 최신 N개까지만 저장하며 이 N이 depth다. KEEP_ALL은 depth 대신 middleware resource limit의 영향을 받는다.
두 정책이 1이어도 deadline, liveliness, lease duration 같은 다른 QoS 조건이 남는다. Namespace/remap, domain/discovery, security, network, executor/callback 문제도 이 계산 밖이다. 따라서 기록은 “연결된다”가 아니라 “reliability와 durability 불일치는 제외했다”가 정확하다.
공개 예제는 분기표의 일관성만 검산했다
위 표는 실제 로봇 장애 기록이 아니라 공식 호환표를 부호화한 자체 예제다. 2026년 9월 20일 KST, Ubuntu runner의 Python 3.12.3에서 다음 명령으로 여섯 assertion을 실행했다.
python3 -I editorial/generated-foundations/20260920T094844Z-754ba35435-ros2-qos-compatibility-before-queue-depth/example.py
실제 표준 출력은 다음과 같았고 종료 상태는 0이었다.
[{"name": "두 정책 모두 호환", "actual": 1}, {"name": "best effort 발행자와 reliable 구독자", "actual": 0}, {"name": "reliable 발행자와 best effort 구독자", "actual": 1}, {"name": "volatile 발행자와 transient local 구독자", "actual": 0}, {"name": "reliability와 durability 모두 불일치", "actual": 0}, {"name": "depth를 10에서 100으로 늘려도 reliability 불일치는 유지", "actual": 0.0}]
기대 결과는 [1, 0, 1, 0, 0, 0.0]이었다. Assertion이 실패하거나, 0을 기대한 불일치 사례에서 1이 나오거나, 종료 상태가 0이 아니면 검산 실패다. 입력과 전체 검산 절차는 고정 커밋의 공개 예제 코드와 공개 검증 기록에서 확인할 수 있다.
이 실행은 표를 코드로 옮긴 산술 검산이다. ROS 2 node, DDS/RMW middleware, simulator나 실제 robot hardware를 실행하지 않았으며, queue overflow·전달률·latency·무선 손실·RMW별 차이를 측정하지 않았다. 공식 문서의 endpoint matching을 재현한 실험으로 확대해서는 안 된다.
실제 장치로 옮기기 전에 계산의 경계를 닫는다
이 판단법은 reliability와 durability 두 정책의 호환성만 좁혀 준다. Rolling 문서는 이후 바뀔 수 있어 본문의 기술 근거는 2026년 9월 20일 KST에 확인한 고정 커밋으로 제한했다. 특정 ROS 2 배포판과 RMW 구현에서의 실제 matching 및 queue 동작은 별도 실행 검증이 필요하다.
실물 로봇에서 QoS 설정을 바꾸는 일은 actuator 명령의 의미나 안전성을 검증하지 않는다. 장치별 제조사 매뉴얼과 현장 위험 평가를 우선하고, actuator power를 차단하거나 제조사가 정한 safe/standby mode를 사용하며, emergency stop을 작동 가능한 위치에 둔 상태에서 command topic이 아닌 무동작 진단 topic부터 확인해야 한다. 이 글은 특정 하드웨어의 실행 절차가 아니다.
이제 자신의 publisher와 subscription 설정을 두 표의 위에서부터 대입해 보자. 처음 0이 나온 칸에는 그 정책 이름을 적고 depth 변경을 보류한다. 둘 다 1이라면 late joiner인지, match 뒤 일부 표본이 밀리는지, 아니면 이 계산 밖의 조건인지 다음 조사 항목 하나를 적는다.
제휴·협찬은 없다.