ROS 2 · 기본
노드·토픽·서비스 첫걸음
파라미터 - 코드 수정 없이 동작 바꾸기
파라미터 선언·읽기·변경 콜백, YAML 파라미터 파일, ros2 param 명령
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서는 서비스로 두리에게 "목적지를 바꿔라" 같은 일회성 요청을 보내고 응답을 받는 방법을 다뤘다. 이번 장에서 다루는 파라미터는 성격이 다르다. 파라미터는 노드가 계속 들고 있는 설정값이다. 순찰 속도나 배터리 경고 기준처럼 노드가 살아있는 동안 유지되면서, 필요할 때 코드를 고치지 않고 바꿀 수 있는 값을 다룰 때 쓴다.
- 노드가 파라미터를 선언하고 읽는 절차를 이해한다
- 파라미터 값이 바뀔 때 검증하는 콜백을 작성한다
- YAML 파라미터 파일로 실행 시점에 기본값을 덮어쓴다
- ros2 param 명령으로 실행 중인 노드의 파라미터를 조회하고 바꾼다
- 파라미터와 서비스, 토픽의 용도를 구분한다
문제 상황
두리를 창고 한 곳에서만 쓸 때는 순찰 속도 0.3 m/s 를 소스코드에 그대로 적어 넣어도 문제가 없었다. 그런데 두리를 다른 창고에 옮겨 테스트하면서 상황이 달라졌다. 통로가 좁은 창고에서는 속도를 0.15 m/s 로 낮춰야 했고, 통로가 넓은 창고에서는 0.6 m/s 로 올려도 됐다. 배터리 경고 기준도 창고마다 충전기 위치가 달라서 건물별로 다르게 잡아야 했다.
이런 값을 바꿀 때마다 소스코드를 열어 숫자를 고치고 colcon build 로 다시 빌드한 다음 노드를 재실행하는 방식은 현장에서 버틸 수 없다. 현장 담당자가 파이썬 코드를 건드리게 하는 것도 바람직하지 않다. 값 하나를 잘못 넣었을 때 로봇이 위험한 속도로 움직이는 것도 막아야 한다. 파라미터는 이 세 가지 요구, 즉 재빌드 없는 값 변경, 코드를 몰라도 되는 설정 방법, 값 검증을 한꺼번에 해결하는 장치다.
파라미터란 무엇인가
파라미터는 노드 하나가 자신만 들고 있는 키-값 저장소다. 다른 노드의 파라미터를 직접 건드릴 수 없고, 오직 이름으로 지정한 노드의 파라미터만 조회하거나 바꿀 수 있다. 토픽은 값을 계속 흘려보내는 통로이고 서비스는 한 번 요청해서 한 번 응답받는 통로라면, 파라미터는 노드 내부에 붙어 있는 설정판에 가깝다.
파라미터는 declare_parameter 로 선언할 때 기본값을 함께 넘기는데, 이 기본값의 타입이 그 파라미터의 타입으로 고정된다. 0.3 을 기본값으로 선언하면 그 파라미터는 실수(double) 타입으로 고정되고, 이후 정수나 문자열로 바꾸려는 시도는 거부된다. 이 규칙 덕분에 파라미터를 읽는 코드는 항상 같은 타입을 기대할 수 있다.
파라미터 동작을 순수 파이썬으로 먼저 확인하기
ROS 2 환경 없이도 핵심 동작, 즉 "선언한 값을 읽고, 바꾸려 할 때 검증을 거친다"는 흐름은 순수 파이썬으로 흉내낼 수 있다. 아래 코드는 rclpy 없이 python3 로 바로 실행된다.
class ParameterStore:
def __init__(self):
self._values = {}
self._callback = None
def declare(self, name, default):
self._values[name] = default
def get(self, name):
return self._values[name]
def set_callback(self, callback):
self._callback = callback
def set(self, name, value):
if self._callback is not None:
ok, reason = self._callback(name, value)
if not ok:
print(f'거부: {name} = {value} ({reason})')
return False
self._values[name] = value
print(f'변경: {name} = {value}')
return True
def validate_patrol_speed(name, value):
if name == 'patrol_speed' and not (0.05 <= value <= 1.0):
return False, 'patrol_speed 는 0.05 ~ 1.0 사이여야 한다'
return True, ''
def main():
store = ParameterStore()
store.declare('patrol_speed', 0.3)
store.set_callback(validate_patrol_speed)
print(f"초기값: patrol_speed = {store.get('patrol_speed')}")
store.set('patrol_speed', 0.7)
store.set('patrol_speed', 5.0)
print(f"최종값: patrol_speed = {store.get('patrol_speed')}")
if __name__ == '__main__':
main()
python3 parameter_store_demo.py 로 실행하면 다음과 같이 출력된다.
초기값: patrol_speed = 0.3
변경: patrol_speed = 0.7
거부: patrol_speed = 5.0 (patrol_speed 는 0.05 ~ 1.0 사이여야 한다)
최종값: patrol_speed = 0.7
rclpy 의 파라미터 시스템도 구조는 같다. declare_parameter 가 store.declare 역할을, get_parameter 가 store.get 역할을, add_on_set_parameters_callback 이 validate_patrol_speed 역할을 한다.
파라미터 선언·읽기·변경 감지
rclpy 에서 파라미터를 쓰려면 세 단계를 거친다. 먼저 declare_parameter(이름, 기본값) 으로 선언한다. 선언하지 않은 이름은 조회할 수 없다. 다음으로 get_parameter(이름).value 로 현재 값을 읽는다. 마지막으로 값이 바뀔 때 검증하고 싶으면 add_on_set_parameters_callback 에 콜백 함수를 등록한다.
이 콜백은 ros2 param set 명령이든 YAML 파일 로딩이든, 파라미터 값이 바뀌려는 모든 순간에 먼저 호출된다. 콜백은 rcl_interfaces.msg.SetParametersResult 를 반환해야 하며, successful 이 False 면 값이 실제로 바뀌지 않고 이전 값이 그대로 유지된다.
| 메서드 | 역할 | 예시 |
|---|---|---|
| declare_parameter | 이름과 기본값으로 파라미터를 등록한다 | self.declare_parameter('patrol_speed', 0.3) |
| get_parameter | 현재 값을 담은 Parameter 객체를 돌려준다 | self.get_parameter('patrol_speed').value |
| add_on_set_parameters_callback | 값이 바뀌기 전에 검증할 함수를 등록한다 | self.add_on_set_parameters_callback(self.on_change) |
YAML 파라미터 파일과 ros2 param 명령
파라미터 값을 소스코드가 아니라 파일로 관리하고 싶을 때는 YAML 파일을 쓴다. 노드 이름을 최상위 키로 두고, 그 아래 ros__parameters 키에 실제 값을 적는다. 노드 이름이 실제 실행 중인 노드 이름과 정확히 일치해야 값이 적용된다.
노드를 실행한 뒤에는 ros2 param 명령으로 값을 들여다보고 바꿀 수 있다. 자세한 파라미터 개념은 공식 문서에서 확인할 수 있다.
| 명령 | 역할 | 예시 |
|---|---|---|
| list | 실행 중인 노드의 파라미터 이름을 나열한다 | ros2 param list /patrol_node |
| get | 특정 파라미터의 현재 값을 조회한다 | ros2 param get /patrol_node patrol_speed |
| set | 특정 파라미터 값을 바꾼다(콜백 검증을 거친다) | ros2 param set /patrol_node patrol_speed 0.7 |
완성 코드
patrol_node.py
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import SetParametersResult
class PatrolNode(Node):
def __init__(self):
super().__init__('patrol_node')
self.declare_parameter('patrol_speed', 0.3)
self.declare_parameter('battery_warn_level', 20)
self.declare_parameter('robot_name', 'duri')
self.add_on_set_parameters_callback(self.on_parameter_change)
self.timer = self.create_timer(2.0, self.on_timer)
def on_timer(self):
speed = self.get_parameter('patrol_speed').value
name = self.get_parameter('robot_name').value
self.get_logger().info(f'{name} 순찰 속도 {speed:.2f} m/s 로 이동 중')
def on_parameter_change(self, params):
for param in params:
if param.name == 'patrol_speed':
if not (0.05 <= param.value <= 1.0):
return SetParametersResult(
successful=False,
reason='patrol_speed 는 0.05 ~ 1.0 사이여야 한다')
if param.name == 'battery_warn_level':
if not (0 <= param.value <= 100):
return SetParametersResult(
successful=False,
reason='battery_warn_level 은 0 ~ 100 사이여야 한다')
return SetParametersResult(successful=True)
def main():
rclpy.init()
node = PatrolNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
patrol_params.yaml
patrol_node:
ros__parameters:
patrol_speed: 0.5
battery_warn_level: 25
robot_name: duri-2
줄별 해설
declare_parameter 세 번은 각각 patrol_speed(실수), battery_warn_level(정수), robot_name(문자열)을 선언한다. 기본값의 타입이 그대로 파라미터 타입이 된다.
add_on_set_parameters_callback 은 노드가 살아있는 동안 값이 바뀌려 할 때마다 on_parameter_change 를 먼저 호출하도록 등록한다. 이 등록은 생성자에서 한 번만 해두면 이후 모든 변경 시도에 적용된다.
on_timer 는 2초마다 현재 파라미터 값을 읽어 로그로 남긴다. 값을 매번 다시 읽기 때문에 실행 중에 속도가 바뀌면 다음 주기부터 곧바로 반영된다.
on_parameter_change 는 params 리스트를 순회하며 이름별로 범위를 검사한다. 범위를 벗어나면 즉시 successful=False 와 함께 reason 을 담아 반환하고, 모든 항목을 통과하면 successful=True 를 반환한다. reason 에 넣은 문구는 ros2 param set 명령을 실행한 터미널에 그대로 나타난다.
patrol_params.yaml 의 최상위 키 patrol_node 는 super().__init__('patrol_node') 에서 준 이름과 정확히 같아야 한다. 다르면 이 파일의 값은 조용히 무시된다.
실행 결과
파라미터 파일 없이 기본값으로 실행하면 다음과 같다. 로그의 시간 값은 실행할 때마다 달라지므로 초 단위 숫자만 예시로 적었다.
$ ros2 run duri_patrol patrol_node
[INFO] [1730000000.101] [patrol_node]: duri 순찰 속도 0.30 m/s 로 이동 중
[INFO] [1730000002.104] [patrol_node]: duri 순찰 속도 0.30 m/s 로 이동 중
YAML 파일을 넘겨서 실행하면 기본값이 파일 값으로 덮인다.
$ ros2 run duri_patrol patrol_node --ros-args --params-file patrol_params.yaml
[INFO] [1730000010.201] [patrol_node]: duri-2 순찰 속도 0.50 m/s 로 이동 중
다른 터미널에서 ros2 param 명령을 실행하면 다음과 같다.
$ ros2 param get /patrol_node patrol_speed
Double value is: 0.5
$ ros2 param set /patrol_node patrol_speed 1.5
Setting parameter failed: patrol_speed 는 0.05 ~ 1.0 사이여야 한다
$ ros2 param set /patrol_node patrol_speed 0.7
Set parameter successful
값을 0.7로 바꾼 뒤 노드가 찍는 로그는 다음 주기부터 바뀐 속도로 나타난다.
[INFO] [1730000014.310] [patrol_node]: duri-2 순찰 속도 0.70 m/s 로 이동 중
실무에서 자주 틀리는 것
선언 없이 바로 읽기
declare_parameter 를 부르지 않고 get_parameter 를 호출하면 예외가 발생한다.
# 틀린 코드
class PatrolNode(Node):
def __init__(self):
super().__init__('patrol_node')
speed = self.get_parameter('patrol_speed').value
# 고친 코드
class PatrolNode(Node):
def __init__(self):
super().__init__('patrol_node')
self.declare_parameter('patrol_speed', 0.3)
speed = self.get_parameter('patrol_speed').value
YAML 파일의 노드 이름 오타
최상위 키가 실제 노드 이름과 다르면 값이 조용히 무시되고 기본값이 그대로 쓰인다.
# 틀린 코드
patrol:
ros__parameters:
patrol_speed: 0.5
# 고친 코드
patrol_node:
ros__parameters:
patrol_speed: 0.5
콜백에서 조건 없이 항상 성공 반환
콜백을 등록해 놓고 검증 로직을 채우지 않으면 위험한 값도 그대로 통과한다.
# 틀린 코드
def on_parameter_change(self, params):
return SetParametersResult(successful=True)
# 고친 코드
def on_parameter_change(self, params):
for param in params:
if param.name == 'patrol_speed' and not (0.05 <= param.value <= 1.0):
return SetParametersResult(successful=False, reason='범위 초과')
return SetParametersResult(successful=True)
선언 시 타입과 다른 값으로 설정 시도
patrol_speed 를 실수 0.3 으로 선언했으므로 문자열을 넣으면 타입이 맞지 않아 거부된다.
# 틀린 명령
$ ros2 param set /patrol_node patrol_speed fast
# 고친 명령
$ ros2 param set /patrol_node patrol_speed 0.4
한눈에 보기
| 항목 | 무엇을 하는가 | 주의점 |
|---|---|---|
| declare_parameter | 기본값과 타입을 함께 선언한다 | 선언하지 않은 이름은 읽을 수 없다 |
| add_on_set_parameters_callback | 값이 바뀌기 전에 검증한다 | successful=True 를 무조건 반환하면 검증 의미가 없다 |
| YAML 파라미터 파일 | 실행 시점에 기본값을 덮어쓴다 | 최상위 키가 노드 이름과 정확히 같아야 한다 |
| ros2 param set/get | 실행 중인 노드의 값을 조회·변경한다 | 타입이 다르거나 검증에 걸리면 거부된다 |
연습 문제
- 두리의 max_speed 파라미터를 기본값 0.4 로 선언하는 코드 한 줄을 작성하라.
- /patrol_node 의 robot_name 파라미터를 "duri-3" 으로 바꾸는 ros2 param 명령을 작성하라.
- 운영자가 patrol_params.yaml 의 patrol_speed 값을 0.8 로 바꾸고 노드를 --params-file 옵션으로 실행했는데도 로그에는 여전히 0.3 이 찍힌다. 원인으로 가장 가능성이 높은 것을 설명하라.
- 배터리 잔량이 특정 값 이하가 될 때 두리가 충전소로 돌아가게 하려면, 이 동작을 서비스로 구현하는 것과 파라미터로 구현하는 것 중 어느 쪽이 더 알맞은지, 그리고 왜 그런지 설명하라.
정답과 해설
기본값 0.4 를 넘기면 max_speed 는 실수 타입으로 고정된다.self.declare_parameter('max_speed', 0.4)
노드 이름과 파라미터 이름, 새 값 순서로 적는다.ros2 param set /patrol_node robot_name duri-3- 가장 가능성이 높은 원인은 YAML 파일의 최상위 키가 실제 노드 이름(patrol_node)과 다르게 적혀 있는 경우다. 키가 다르면 파일 전체가 무시되고 소스코드의 기본값 0.3 이 그대로 쓰인다. patrol_params.yaml 의 최상위 키를 확인해야 한다.
- 충전소로 돌아가라는 동작 자체는 한 번 요청해서 처리하고 끝나는 일이므로 서비스로 구현하는 쪽이 알맞다. 반면 배터리 경고 기준값(예: battery_warn_level)처럼 노드가 계속 들고 있으면서 필요할 때 바꾸는 설정값은 파라미터로 둔다. 즉 "지금 이 동작을 해라"는 서비스, "앞으로 이 기준으로 판단해라"는 파라미터로 나눠 생각하면 된다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.