QoS - 신뢰성·내구성·깊이로 통신 품질 맞추기
이 장에서 배우는 것
두리가 실외로 나가면 통신 조건이 달라진다. 무선 연결에서는 센서 측정값 일부가 빠질 수 있고, 경로를 표시하는 프로그램은 로봇보다 늦게 시작될 수 있다. 발행자와 구독자가 같은 토픽 이름과 메시지 형식을 사용하더라도 통신 정책이 맞지 않으면 데이터가 전달되지 않는다. 이제는 무엇을 보내는지뿐 아니라 어떤 조건으로 전달할지도 정해야 한다.
서비스 품질(Quality of Service, QoS)은 이러한 전달 조건을 표현한다. 이 장에서는 두리의 반복 측정값과 최신 배송 경로를 예로 들어 신뢰성, 내구성, 이력 깊이를 조합한다. 실제 ROS 프로그램과 함께 ROS 설치 없이 정책의 핵심을 확인하는 순수 Python 프로그램을 작성한다.
- RELIABLE과 BEST_EFFORT가 전달 과정에서 무엇을 다르게 요구하는지 설명한다.
- TRANSIENT_LOCAL을 이용해 늦게 접속한 구독자에게 최신 상태를 제공한다.
- KEEP_LAST의 깊이와 센서 데이터용 설정의 관계를 이해한다.
- 발행자의 제공 조건과 구독자의 요구 조건을 비교해 무수신 문제를 진단한다.
문제 상황
두리는 거리 측정값을 계속 발행하고, 운영 화면은 그 값을 받아 주변 상황을 표시한다. 개발실에서는 정상적으로 보이던 화면이 실외 시험에서 멈춘다. 토픽 목록에는 측정 토픽이 있고 발행자도 실행 중이다. 문제를 살펴보니 센서 드라이버는 BEST_EFFORT로 발행하지만 화면은 RELIABLE로 구독하고 있다. 메시지 형식이 맞아도 이 두 끝점은 서로 연결되지 않는다.
다른 문제도 있다. 배송 경로를 한 번 발행한 뒤 운영 화면을 시작하면 화면이 비어 있다. 경로 발행자는 종료되지 않았지만 이미 보낸 경로를 새 구독자에게 전달할 정책을 사용하지 않았다. 반복 발행하는 측정값에서는 다음 메시지를 기다리면 되지만, 변경될 때만 발행하는 경로에서는 기다려도 아무것도 오지 않을 수 있다.
두 문제를 모두 “통신이 불안정하다”라고 묶으면 해결이 어려워진다. 첫 번째는 정책의 호환성 문제이고, 두 번째는 늦은 참여자에게 과거 데이터를 제공할지에 관한 문제다. 여기에 처리 지연으로 대기 중인 메시지가 늘어나는 문제까지 더해지면 깊이 설정도 살펴야 한다. 세 정책을 각각 이해한 다음 필요한 조합을 고르는 것이 출발점이다.
신뢰성은 발행자의 제공과 구독자의 요구로 맞춘다
신뢰성(reliability)은 데이터 전달을 확인하고 손실 복구를 시도할지 정하는 정책이다. RELIABLE은 전달 확인과 재전송을 이용한다. BEST_EFFORT는 그러한 전달 보장을 요구하지 않는다. 그렇다고 BEST_EFFORT가 의도적으로 메시지를 버리는 것은 아니다. 통신과 처리가 원활하면 연속된 메시지를 모두 받을 수도 있다.
RELIABLE을 사용한다고 응용 프로그램이 발행된 데이터를 모두 처리하는 것은 아니다. 이력 정책, 자원 한도, 연결 상태, 프로세스 종료 등이 결과에 영향을 준다. 또한 전송 계층의 전달 확인은 구독 콜백의 업무 처리 완료를 뜻하지 않는다. 예를 들어 경로 메시지가 전달되었다는 사실만으로 두리가 그 경로를 주행했다고 판단할 수는 없다.
발행자는 제공할 수 있는 조건을 알리고, 구독자는 자신이 요구하는 조건을 알린다. 신뢰성에서는 발행자의 제공 수준이 구독자의 요구를 만족해야 한다. RELIABLE 발행자는 BEST_EFFORT 구독자의 요구를 만족하지만, BEST_EFFORT 발행자는 RELIABLE 구독자의 요구를 만족하지 못한다. 양쪽 값을 무조건 같게 만드는 규칙으로 이해하면 이 방향성을 놓친다.
| 발행자 | 구독자 | 연결 가능 여부 |
|---|---|---|
| BEST_EFFORT | BEST_EFFORT | 가능 |
| BEST_EFFORT | RELIABLE | 불가능 |
| RELIABLE | BEST_EFFORT | 가능 |
| RELIABLE | RELIABLE | 가능 |
두리의 거리 측정값은 짧은 간격으로 계속 갱신된다. 오래된 값 하나를 복구하는 동안 새로운 값의 처리가 늦어지는 것보다 최근 값을 빠르게 받는 편이 나을 수 있다. 반대로 빠뜨리면 안 되는 데이터라면 RELIABLE을 검토해야 한다. 다만 선택은 데이터 이름만으로 정하지 않는다. 손실을 허용하는 정도, 발행 빈도, 지연 한도, 응용 수준의 확인 절차를 함께 고려한다.
내구성은 늦게 참여한 구독자를 위한 정책이다
내구성(durability)은 새로 연결된 구독자가 발행자의 이전 데이터를 받을 수 있는지와 관련된다. VOLATILE은 늦게 참여한 구독자를 위한 과거 데이터 보관을 요구하지 않는다. TRANSIENT_LOCAL은 발행자가 살아 있는 동안 이력 정책과 자원 한도 안에서 데이터를 보관하고, 해당 내구성을 요구하는 새 구독자에게 제공할 수 있게 한다.
두리의 최신 배송 경로처럼 “현재 유효한 상태”를 나타내는 데이터에는 TRANSIENT_LOCAL과 KEEP_LAST, 깊이 1의 조합이 유용하다. 경로가 여러 번 바뀌더라도 새 운영 화면에는 가장 최근 경로 하나가 필요하기 때문이다. 이 조합은 변경 이력 전체를 저장하는 기록 장치가 아니다. 발행자 프로세스가 끝나면 그 발행자가 보관하던 데이터도 더는 기대할 수 없다.
내구성 역시 방향이 있다. VOLATILE 발행자는 TRANSIENT_LOCAL 구독자의 요구를 만족하지 못한다. TRANSIENT_LOCAL 발행자와 VOLATILE 구독자는 연결할 수 있지만, 이 조합을 늦은 구독자의 과거 데이터 수신 보장으로 해석해서는 안 된다. 이전 상태를 받는 기능이 목적이라면 양쪽에 TRANSIENT_LOCAL을 명시한다.
이 장의 경로는 실행 명령이 아니라 화면에 표시할 상태 정보다. 과거 메시지를 새 구독자에게 제공하는 기능은 상태 배포에는 편리하지만, 수신할 때마다 동작을 실행하는 메시지에 그대로 적용하면 재실행을 일으킬 수 있다. TRANSIENT_LOCAL을 선택하기 전에 메시지가 현재 상태인지, 한 번 처리해야 할 요청인지 구분해야 한다.
깊이와 센서 데이터용 설정을 함께 읽는다
이력(history)은 보관할 샘플의 범위를 정한다. KEEP_LAST에서는 깊이(depth)가 최근 몇 개를 유지할지 나타낸다. 깊이 3의 이력에 1, 2, 3이 들어 있는 상태에서 4가 추가되면 최근 세 개인 2, 3, 4가 남는 식으로 이해할 수 있다. KEEP_ALL은 가능한 이력을 유지하는 정책이지만, 이 경우에도 구현과 자원 한도가 적용된다. 메모리를 제한 없이 쓸 수 있다는 뜻은 아니다.
깊이는 발행자와 구독자 각각의 설정이다. 발행자에서는 전송과 재전송, 늦은 참여자에게 제공할 이력 등에 관계하고, 구독자에서는 아직 응용 프로그램이 가져가지 않은 수신 데이터의 보관에 관계한다. 양쪽 깊이가 같아야 연결되는 것은 아니다. 깊이 10인 발행자와 깊이 3인 구독자도 다른 정책이 맞으면 연결할 수 있다.
또한 깊이는 시간을 지정하지 않는다. 20Hz로 들어오는 데이터 열 개가 나타내는 시간 범위는 대략 0.5초지만, 이것이 0.5초의 최대 지연을 보장하지는 않는다. 처리 시간이 계속 수신 간격보다 길다면 깊이를 늘려도 처리 능력은 좋아지지 않는다. 일시적인 지연을 흡수할 여유는 늘어날 수 있지만, 오래된 데이터를 뒤늦게 처리하는 상황도 생길 수 있다.
Jazzy의 rclpy에는 센서 데이터용 설정인 qos_profile_sensor_data가 있다. 주요 값은 BEST_EFFORT, VOLATILE, KEEP_LAST, 깊이 5다. 반복 측정값의 최신성을 중시하는 출발점으로 사용할 수 있다. 모든 센서 드라이버가 이 설정을 사용한다는 뜻은 아니므로 실제 끝점 정보는 따로 확인해야 한다.
from rclpy.qos import qos_profile_sensor_data
subscription = node.create_subscription(
String, "/duri/range_sample", callback, qos_profile_sensor_data
)
위 조각은 센서 값을 문자열로 표현하는 이 장의 예시다. 실제 거리 센서의 메시지 형식을 정하는 코드는 아니다. 다음 완성 프로그램도 QoS 비교에 집중하기 위해 두 토픽 모두 String을 사용한다. 명시적인 설정과 재현 가능한 차이를 보여주려고 센서용 주요 값을 직접 작성한다.
정책의 정확한 의미와 호환성 표는 ROS 2 Jazzy QoS 설명에서 확인할 수 있다. Python 설정 객체의 인자는 rclpy QoS API를 참고한다. 실제 운용에서는 기본값을 추측하기보다 중요한 정책을 코드에 명시하는 편이 비교하기 쉽다.
완성 코드
첫 파일은 ROS 없이 실행한다. 신뢰성과 내구성의 호환 여부, 늦은 구독자에게 제공할 이력, KEEP_LAST의 보관 결과를 작은 모델로 재현한다. 두 번째 파일은 ROS 2 Jazzy와 rclpy, std_msgs가 준비된 Python 환경에서 실행한다. macOS와 Linux 모두 해당 환경이 현재 터미널에 적용되어 있어야 한다. 운영체제의 기본 python3가 ROS 패키지를 찾는다고 가정하지 않는다.
qos_model.py
from collections import deque
from dataclasses import dataclass
@dataclass(frozen=True)
class Policy:
reliable: bool
transient: bool
depth: int
def __post_init__(self):
if self.depth < 1:
raise ValueError("depth must be positive")
def compatible(offered, requested):
if requested.reliable and not offered.reliable:
return False
if requested.transient and not offered.transient:
return False
return True
def late_join(offered, requested, published):
if not compatible(offered, requested):
return "incompatible", []
if not (offered.transient and requested.transient):
return "matched", []
writer_history = deque(published, maxlen=offered.depth)
reader_history = deque(writer_history, maxlen=requested.depth)
return "matched", list(reader_history)
def main():
sensor = Policy(False, False, 5)
strict = Policy(True, False, 5)
route = Policy(True, True, 1)
volatile = Policy(True, False, 1)
print("sensor -> reliable:", compatible(sensor, strict))
print("reliable -> sensor:", compatible(strict, sensor))
status, values = late_join(route, route, ["A", "B", "C"])
print("late transient:", status, values)
status, values = late_join(volatile, route, ["A", "B", "C"])
print("late incompatible:", status, values)
status, values = late_join(volatile, volatile, ["A", "B", "C"])
print("late volatile:", status, values)
pending = deque(maxlen=2)
for number in range(1, 6):
pending.append(number)
print("pending depth=2:", list(pending))
if __name__ == "__main__":
main()
このモデルに相当する日本語の説明は不要であり、ここではモデルの範囲を韓国語で定める。
이 모델은 분산 통신을 구현하지 않는다. 과거 발행이 모두 끝난 뒤 구독자가 들어오며, 이력이 한꺼번에 구독자 쪽에 도착하고 아직 처리되지 않은 상황을 가정한다. 실제 통신의 발견 시간, 재전송, 패킷 손실, 자원 제한은 다루지 않는다. 특히 연결된 VOLATILE 구독자의 과거 데이터 처리는 단순화하여 빈 목록으로 표현한다. 이 모델의 결과를 모든 실제 통신 구현의 상세 동작으로 확대해서는 안 된다.
duri_qos.py
import argparse
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from rclpy.qos import (
DurabilityPolicy,
HistoryPolicy,
QoSProfile,
ReliabilityPolicy,
)
from std_msgs.msg import String
def make_qos(name):
sensor = name.startswith("sensor")
return QoSProfile(
history=HistoryPolicy.KEEP_LAST,
depth=5 if sensor else 1,
reliability=(
ReliabilityPolicy.BEST_EFFORT
if name == "sensor"
else ReliabilityPolicy.RELIABLE
),
durability=(
DurabilityPolicy.TRANSIENT_LOCAL
if name == "route"
else DurabilityPolicy.VOLATILE
),
)
class DuriQoS(Node):
def __init__(self, role, profile):
super().__init__(f"duri_{role}")
self.sensor = profile.startswith("sensor")
self.topic = "/duri/range_sample" if self.sensor else "/duri/route"
self.sequence = 0
qos = make_qos(profile)
if role == "pub":
self.publisher = self.create_publisher(String, self.topic, qos)
self.timer = self.create_timer(1.0, self.publish_sample)
else:
self.subscription = self.create_subscription(
String, self.topic, self.receive_sample, qos
)
def publish_sample(self):
self.sequence += 1
message = String()
message.data = (
f"range sample {self.sequence}"
if self.sensor
else "route: depot -> gate"
)
self.publisher.publish(message)
print(f"PUB {self.topic}: {message.data}", flush=True)
if not self.sensor:
self.timer.cancel()
def receive_sample(self, message):
print(f"SUB {self.topic}: {message.data}", flush=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("role", choices=["pub", "sub"])
parser.add_argument(
"profile",
choices=["sensor", "sensor-request", "route", "route-volatile"],
)
args = parser.parse_args()
rclpy.init(args=[])
node = None
try:
node = DuriQoS(args.role, args.profile)
rclpy.spin(node)
except (KeyboardInterrupt, ExternalShutdownException):
pass
finally:
if node is not None:
node.destroy_node()
if rclpy.ok():
rclpy.shutdown()
if __name__ == "__main__":
main()
프로그램은 인자를 자체적으로 처리하므로 rclpy.init(args=[])에 빈 목록을 전달한다. 이 예제에는 ROS 명령행 인자를 별도로 전달하지 않는다. 경로 발행자는 첫 발행 후 타이머만 취소하고 계속 실행된다. 이렇게 해야 늦게 시작한 구독자에게 제공할 이력을 발행자가 유지할 수 있다.
줄별 해설
순수 Python 코드의 @dataclass(frozen=True)는 정책을 생성한 뒤 필드를 바꾸지 못하게 한다. reliable과 transient는 이 장에서 비교하는 두 선택지만 표현하는 불리언 값이다. 실제 rclpy 열거형을 대체하려는 자료 구조는 아니다. __post_init__의 검사로 깊이가 0 이하인 모델 입력을 막는다.
compatible()의 첫 조건은 구독자가 신뢰성을 요구하는데 발행자가 제공하지 못하는 경우다. 두 번째 조건은 내구성의 같은 방향을 검사한다. 둘 중 하나라도 만족하지 못하면 False를 반환한다. 이 함수는 두 정책만 확인하므로 실제 시스템에서 모든 QoS 호환성을 판정하는 도구로 사용할 수는 없다.
late_join()은 연결 불가와 연결은 되지만 과거 데이터가 없는 상태를 다른 결과로 돌려준다. 두 상태를 구분해야 무수신 원인을 올바르게 해석할 수 있다. writer_history는 발행자 깊이로 이전 데이터를 줄이고, reader_history는 그 결과를 구독자 깊이로 다시 제한한다. 이 두 줄은 모델의 일괄 도착 가정 아래 양쪽 보관 한도가 별개임을 보여준다.
pending에 1부터 5까지 넣는 반복문은 콜백이 데이터를 가져가지 않는 동안 최근 두 개만 남는 상황을 표현한다. 실제 수신 이력이 Python deque로 구현된다는 뜻은 아니다. 또한 네트워크 손실과 수신 측 이력에서 오래된 데이터가 밀려나는 일을 같은 현상으로 취급하지 않는다.
| 코드 | 역할 | 읽을 때 확인할 점 |
|---|---|---|
| name.startswith("sensor") | 센서 계열 설정을 구분한다 | 두 센서 설정이 같은 토픽과 깊이를 사용한다 |
| depth=5 if sensor else 1 | 보관할 최근 샘플 수를 정한다 | 센서는 5, 경로는 1이다 |
| name == "sensor" | BEST_EFFORT를 선택한다 | sensor-request는 RELIABLE이다 |
| name == "route" | TRANSIENT_LOCAL을 선택한다 | route-volatile은 VOLATILE이다 |
| create_subscription(..., qos) | 구독자가 요구할 조건을 지정한다 | 발행자의 제공 조건과 비교한다 |
| self.timer.cancel() | 경로의 반복 발행을 멈춘다 | 노드와 발행자는 살아 있다 |
DuriQoS.__init__()은 역할이 pub이면 발행자와 타이머를 만들고, sub이면 구독자를 만든다. self.subscription에 구독 객체를 보관해 노드가 실행되는 동안 참조를 유지한다. 센서 설정의 차이는 신뢰성 하나이며, 경로 설정의 차이는 내구성 하나다. 한 번에 한 정책만 바꾸면 원인과 결과를 비교하기 쉽다.
publish_sample()은 센서 모드에서 매초 번호가 증가하는 문자열을 발행한다. 경로 모드에서는 같은 함수가 한 번 실행된 뒤 타이머를 취소한다. print(..., flush=True)는 출력이 버퍼에 머물지 않게 해 터미널에서 순서를 확인하기 쉽게 한다. 출력이 보인다는 사실은 발행 함수가 실행되었다는 증거이며, 구독자가 받았다는 증거는 아니다.
finally에서는 노드를 정리하고 문맥이 아직 활성 상태일 때 종료한다. 이 처리는 Ctrl+C로 실험을 멈추는 흐름도 포함한다. 경로 이력을 시험할 때는 발행자를 계속 켜 두었다가 구독 결과를 확인한 후 종료해야 한다.
실행 결과
두 파일을 같은 디렉터리에 저장한다. 다음 명령은 경고를 오류로 취급하여 문법 컴파일을 확인하고, 순수 Python 모델을 실행한다. 컴파일은 import한 ROS 패키지를 실행하지 않으므로 ROS가 없는 환경에서도 가능하다. 성공한 컴파일 명령에는 출력이 없다. 아래 결과는 코드에서 도출한 예상 출력이며, 이 원고에서 도구를 실행해 검증한 기록은 아니다.
python3 -W error -m py_compile qos_model.py duri_qos.py
python3 qos_model.py
sensor -> reliable: False
reliable -> sensor: True
late transient: matched ['C']
late incompatible: incompatible []
late volatile: matched []
pending depth=2: [4, 5]
ROS 실행에서는 먼저 터미널 A에서 경로 발행자를 시작한다. 출력이 나타난 뒤에도 프로세스를 유지한다. 두 터미널은 같은 ROS 도메인에 속하고 서로 발견 가능한 상태여야 한다.
python3 duri_qos.py pub route
PUB /duri/route: route: depot -> gate
위 출력이 나온 뒤 터미널 B에서 구독자를 시작한다. 두 끝점이 발견되고 정상 연결되면 발행자가 다시 발행하지 않아도 다음 줄을 받는다. 지연 시간 자체는 이 예제의 고정 출력에 포함하지 않는다.
python3 duri_qos.py sub route
SUB /duri/route: route: depot -> gate
다음으로 두 프로세스를 종료하고 센서 불일치를 재현한다. 터미널 A와 B에서 각각 아래 명령을 실행한다. 발행자는 range sample 1부터 번호를 증가시키지만, 구독자에는 SUB로 시작하는 줄이 나오지 않는다. 구현에 따라 QoS 불일치 경고가 추가로 표시될 수 있어 그 경고 문구는 예상 출력으로 고정하지 않는다.
python3 duri_qos.py pub sensor
python3 duri_qos.py sub sensor-request
구독자를 종료하고 다음 명령으로 다시 실행하면 이후에 도착하는 측정값을 받을 수 있다. 처음 수신하는 번호는 구독자를 시작한 시점과 발견 시간에 따라 달라진다. 이미 발행된 센서 값이 번호 1부터 재생되는 실험은 아니다.
python3 duri_qos.py sub sensor
불일치 상태에서 별도 터미널로 다음 명령을 실행하면 발행자와 구독자 끝점의 QoS를 비교할 수 있다. 출력의 노드 정보와 식별자는 실행 환경에 따라 달라진다. 발행자의 신뢰성이 BEST_EFFORT이고 구독자의 신뢰성이 RELIABLE인지 확인한다.
ros2 topic info /duri/range_sample --verbose
진단용 구독도 정책을 맞춰야 한다. 센서 토픽과 저장된 경로를 각각 관찰할 때는 다음처럼 필요한 조건을 명시할 수 있다. 경로 명령은 route 발행자가 살아 있을 때 사용한다.
ros2 topic echo /duri/range_sample std_msgs/msg/String --qos-reliability best_effort
ros2 topic echo /duri/route std_msgs/msg/String --qos-reliability reliable --qos-durability transient_local
실무에서 자주 틀리는 것
정수 깊이만 지정하면 센서 설정이 된다고 생각한다
아래의 10은 KEEP_LAST의 깊이를 지정한다. 센서 데이터용 BEST_EFFORT 설정을 선택하지는 않는다. rclpy의 기본 신뢰성은 RELIABLE이므로 BEST_EFFORT 센서 발행자와 맞지 않을 수 있다. 아래 조각들은 String, node, callback이 준비되어 있다고 가정한다.
# 잘못된 가정: 깊이만 주면 센서 발행자와 연결된다.
sub = node.create_subscription(
String, "/duri/range_sample", callback, 10
)
# 수정: 실제 발행 조건에 맞는 센서 데이터용 설정을 사용한다.
from rclpy.qos import qos_profile_sensor_data
sub = node.create_subscription(
String, "/duri/range_sample", callback, qos_profile_sensor_data
)
구독자의 내구성만 올리면 이전 상태를 받는다고 생각한다
구독자가 TRANSIENT_LOCAL을 요구해도 VOLATILE 발행자가 과거 데이터를 보관하도록 소급해서 바뀌지는 않는다. 이 조합은 내구성 요구부터 맞지 않는다. 완성 코드의 make_qos를 사용하면 잘못된 조합과 수정된 조합은 다음과 같다.
# 잘못된 조합
pub_qos = make_qos("route-volatile")
sub_qos = make_qos("route")
# 수정: 양쪽에 같은 상태 보관 의도를 표현한다.
pub_qos = make_qos("route")
sub_qos = make_qos("route")
이 변수들은 끝점을 생성할 때 전달해야 한다. 이미 만들어진 끝점의 설정이 변수 재대입만으로 변경되는 것은 아니다. 실험에서는 기존 프로세스를 종료하고 수정한 설정으로 다시 실행하는 편이 분명하다.
한 번 발행한 뒤 발행자를 종료한다
TRANSIENT_LOCAL의 이력은 해당 발행자의 수명에 묶여 있다. 발행 직후 노드를 없애면 늦게 접속한 화면이 데이터를 받을 근거가 사라진다. 다음 코드는 종료 흐름의 차이를 보여주는 조각이다.
# 잘못된 흐름: 이후에 들어올 구독자를 위해 남는 발행자가 없다.
publisher.publish(message)
node.destroy_node()
rclpy.shutdown()
# 수정: 발행자를 유지하며 새 구독자의 접속을 기다린다.
publisher.publish(message)
try:
rclpy.spin(node)
finally:
node.destroy_node()
if rclpy.ok():
rclpy.shutdown()
발행자 재시작 뒤에도 상태를 복구해야 한다면 별도 저장소에서 상태를 읽고 다시 발행하는 설계가 필요하다. 내구성 정책 하나가 디스크 저장과 복구 절차까지 제공하지는 않는다.
깊이를 크게 하면 오래된 데이터 문제가 해결된다고 생각한다
처리가 느릴 때 깊이를 크게 잡으면 많은 데이터를 보관할 수 있지만, 최신 값에 도달하기까지 기다릴 작업도 많아질 수 있다. 최근 관측 하나만 의미 있는 화면이라면 작은 깊이가 의도에 맞는다. 다음 코드는 이 장의 센서 설정을 바꾼 예다.
# 잘못된 선택: 최신 값만 필요한데 많은 대기 데이터를 허용한다.
qos = make_qos("sensor")
qos.depth = 1000
# 수정: 최신 관측 중심의 보관 범위를 표현한다.
qos = make_qos("sensor")
qos.depth = 1
이 설정 역시 끝점을 만들기 전에 적용한다. 깊이 1은 이미 처리 중인 콜백을 중단하거나 새로운 값으로 바꿔 주지 않는다. 모든 측정값을 기록해야 하는 요구라면 작은 깊이로 바꾸는 것이 해답이 아니며, 처리량과 기록 경로부터 점검해야 한다.
한눈에 보기
| 목적 또는 증상 | 정책 | 판단 기준 | 주의점 |
|---|---|---|---|
| 반복 측정의 최신성 | BEST_EFFORT | 일부 손실을 허용할 수 있는가 | 모든 값의 도착을 기대하지 않는다 |
| 전달 손실 복구 | RELIABLE | 전달 확인과 재전송이 필요한가 | 업무 처리 완료를 뜻하지 않는다 |
| 늦은 화면에 현재 상태 제공 | TRANSIENT_LOCAL | 접속 이전 데이터가 필요한가 | 발행자의 수명과 이력 한도가 적용된다 |
| 접속 이후 데이터 사용 | VOLATILE | 다음 갱신을 기다려도 되는가 | 일회성 상태 발행을 놓칠 수 있다 |
| 대기 데이터 범위 제한 | KEEP_LAST와 깊이 | 최근 몇 개를 유지할 것인가 | 깊이는 시간 제한이 아니다 |
| 토픽은 있지만 무수신 | 제공·요구 비교 | 발행자가 구독자의 요구를 만족하는가 | 깊이를 늘려도 불일치는 해결되지 않는다 |
무수신을 조사할 때는 먼저 실제 발행 여부와 토픽 형식을 확인하고, 이어 끝점별 정책을 비교한다. 연결 불일치가 없다면 구독 전에 한 번만 발행된 것은 아닌지 살핀다. 그다음 수신 처리 지연과 이력 범위를 확인한다. 정책이 같더라도 발견이나 네트워크 문제는 남을 수 있으므로 QoS만으로 모든 원인을 설명하지 않는다. 다음 장에서는 수신한 작업을 실행기와 콜백 그룹이 어떻게 처리하는지 살펴본다.
연습 문제
- 발행자는 RELIABLE과 VOLATILE, 구독자는 BEST_EFFORT와 TRANSIENT_LOCAL을 사용한다. 이 장의 두 호환성 기준으로 연결 가능 여부를 판단하고 이유를 설명하라.
- 순수 Python 모델에서 발행자 정책을
Policy(True, True, 3), 구독자 정책을Policy(True, True, 2)로 바꾸고, 1부터 5까지 발행한 뒤late_join()을 호출하라. 결과와 각 단계의 이력을 적어라. - route 발행자가 경로를 한 번 출력한 뒤 같은 route 설정의 구독자를 세 번 순서대로 시작하고 종료한다. 발행자는 계속 실행 중이며 통신은 정상이다. 각 구독자가 경로를 받을 수 있는 이유를 설명하라. 발행자를 먼저 종료하면 무엇이 달라지는지도 적어라.
- 거리 측정 발행자는 BEST_EFFORT, 구독자는 RELIABLE이다. 구독 깊이를 5에서 100으로 늘려도 무수신 상태다. 수정할 정책을 적고, 수정 후에도 센서 번호가 1부터 시작하지 않을 수 있는 이유를 설명하라.
정답과 해설
- 연결되지 않는다. 신뢰성에서는 RELIABLE 발행자가 BEST_EFFORT 구독자의 요구를 만족한다. 그러나 내구성에서는 VOLATILE 발행자가 TRANSIENT_LOCAL 요구를 만족하지 못한다. 두 정책 중 하나만 호환되어서는 연결할 수 없다.
- 결과는
('matched', [4, 5])다. 발행자 이력에는 최근 세 개인 3, 4, 5가 남고, 모델의 구독자 이력에는 그중 최근 두 개인 4, 5가 남는다. 실제 시스템에서는 이력 도착 중에도 응용 프로그램이 데이터를 가져갈 수 있으므로 이 모델의 최종 목록을 일반적인 수신 횟수 공식으로 사용해서는 안 된다. - 각 구독자는 살아 있는 발행자가 보관한 최신 경로를 받을 수 있다. 한 구독자가 받았다고 이력이 소모되는 구조가 아니다. 발행자를 먼저 종료하면 새 구독자에게 데이터를 제공할 해당 발행자가 없어지므로 같은 결과를 기대할 수 없다.
- 센서의 손실 허용 의도가 맞다면 구독자를 BEST_EFFORT로 바꾼다. 신뢰성 요구를 유지해야 한다면 발행자도 RELIABLE을 제공하도록 설계를 바꿔야 한다. 깊이는 이 불일치를 해결하지 않는다. 센서의 VOLATILE 설정에서는 구독 이전의 측정값 재생을 기대하지 않으므로, 정상 연결 뒤 처음 보이는 번호가 1보다 클 수 있다.