제14부 시스템 설계·품질·운영
14.4 launch 심층 II — 대규모 구성
14.4.1 이 장에서 배우는 것 / 선수 지식
작은 시스템의 launch 파일은 한 개면 충분하다. 그런데 노드가 서른 개가 되고, 시뮬과 실기를 모두 지원하고, 로봇이 두 종류이고, 여러 대를 동시에 실행해야 하면 — launch 파일 자체가 유지보수 대상이 된다.
이 장은 그 규모에서의 구성 전략을 다룬다. 목표는 "바꿀 곳이 한 군데" 다. 센서 하나를 추가할 때 launch 파일 다섯 개를 고쳐야 한다면 구조가 잘못된 것이다.
배우는 것:
- 계층 구조 — 어떻게 나눌 것인가.
- 인자 전파 — 위에서 아래로.
- 설정 파일 조직 — YAML을 어떻게 둘 것인가.
- 변형 다루기 — 로봇·환경·모드.
- 다중 로봇 — 네임스페이스 전략.
- 재사용 도구 — 공통 함수·패턴.
- 검증 — launch를 시험하기.
- 병합 의미론 실측(14.4.11) — 딥 머지, 그리고 순서에 우선하는 구체성 규칙.
선수 지식: 14.3(launch 구조), 4.2(패키지 구성), 1.1.6(네임스페이스), 14.2(오케스트레이션), 14.9(플릿 — 함께 읽으면 좋다).
정직: 이 장은 구성 전략을 다루며, 유일한 정답을 제시하지 않는다. 팀 규모·로봇 종류·운영 방식에 따라 적합한 구조가 다르다. API 세부는 배포판 의존이다
(확인 필요). 이 환경에서 검증하지 않았다(목차 3). 다만 파라미터 병합 규칙은 실물로 확인했다(실행 확인 — 14.4.11).
14.4.2 계층 구조
| 계층 | 책임 |
|---|---|
| 최상위 | 전체 조합·모드 선택 |
| 서브시스템 | 한 기능 묶음 |
| 개별 컴포넌트 | 노드 하나 또는 소수 |
| 원칙 | 설명 |
|---|---|
| 각 계층은 자기 인자만 | 하위로 전달 |
| 하위는 상위를 모른다 | 재사용 가능 |
| 기본값 제공 | 단독 실행 가능 |
| 하나의 책임 | 14.1의 원칙 적용 |
견해: "각 서브시스템 launch가 단독으로 실행 가능해야 한다"가 좋은 규율이다.
ros2 launch my_robot_bringup perception.launch.py만으로 인지 파이프라인이 기동되면, 부분 시험과 디버깅이 쉬워진다. 기본값을 잘 주는 것이 그 조건이다.
견해: 패키지 분리도 함께 고려하라(4.2).
<robot>_description(URDF)·<robot>_bringup(launch·config)·기능별 패키지로 나누는 것이 관행이며, bringup 패키지가 launch와 설정의 집합소가 된다.
전형적 파일 구성:
| 파일 | 역할 |
|---|---|
robot.launch.py | 전체 — 인자로 모드 선택 |
hardware.launch.py | 드라이버·ros2_control |
localization.launch.py | tf·측위 |
perception.launch.py | 인지 |
navigation.launch.py | Nav2 |
manipulation.launch.py | MoveIt |
sim.launch.py | 시뮬 전용(10.2) |
rviz.launch.py | 시각화 |
14.4.3 인자 전파
상위 인자를 하위로 넘기는 것이 반복 작업이 된다.
# 공통 인자를 모아 두는 패턴
COMMON_ARGS = ['use_sim_time', 'robot_name', 'namespace', 'log_level']
def pass_args(names):
return {n: LaunchConfiguration(n) for n in names}.items()
IncludeLaunchDescription(
PythonLaunchDescriptionSource(PathJoinSubstitution([
FindPackageShare('my_bringup'), 'launch', 'navigation.launch.py'])),
launch_arguments=pass_args(COMMON_ARGS + ['map_file']))
| 문제 | 대처 |
|---|---|
| 인자 전달 누락 | 헬퍼 함수·목록 |
| 이름 불일치 | 규약·상수 |
| 기본값 중복 | 한 곳에 정의 |
| 인자 폭증 | 그룹화·설정 파일로 |
견해: 인자가 열 개를 넘으면 설정 파일로 옮기는 것을 검토하라. launch 인자는 "실행할 때마다 바뀌는 소수"에 적합하고, 나머지는 YAML이 낫다.
use_sim_time·robot_name·mode정도가 인자, 나머지는 파일이 흔한 경계다.
견해:
SetParameter로 전역 파라미터를 주면 인자 전파가 크게 준다(14.3.5).use_sim_time을 최상위GroupAction에서 한 번 설정하면 하위 전부에 적용되므로, 모든 launch에 인자로 넘길 필요가 없다. 다만 범위를 인지해야 한다.
함정: 하위 launch가 자기 기본값을 가지면 상위 값과 충돌할 수 있다. 상위가 넘기지 않으면 하위 기본값이 쓰이는데, 그것이 상위 의도와 다를 수 있다. 핵심 인자는 반드시 명시적으로 전달하는 규율이 안전하다.
14.4.4 설정 파일 조직
| 조직 방식 | 특성 |
|---|---|
| 기능별 파일 | nav2.yaml·controllers.yaml |
| 로봇별 디렉터리 | 변형 관리 |
| 계층적 병합 | 공통 + 덮어쓰기 |
| 단일 대형 파일 | 단순하나 관리 어려움 |
견해: "공통 + 덮어쓰기" 구조가 여러 로봇·환경을 다룰 때 유리하다. 파라미터 목록의 뒤가 앞을 덮어쓰는 성질(14.3.8)을 이용해,
[common.yaml, robot_a.yaml, sim_overrides.yaml]순으로 주면 된다.
parameters=[
PathJoinSubstitution([cfg, 'common', 'nav2.yaml']),
PathJoinSubstitution([cfg, LaunchConfiguration('robot_name'), 'nav2.yaml']),
{'use_sim_time': LaunchConfiguration('use_sim_time')},
]
함정: 덮어쓰기에는 순서보다 강한 규칙이 있다 — 지정자의 구체성이다(실행 확인, 14.4.11.2). 중첩 구조의 부분 덮어쓰기는 기대대로 동작하지만(딥 머지, 14.4.11.1), 나중에 준
/**:파일은 앞선/노드이름:항목을 덮지 못한다.sim_overrides.yaml을/**:로 쓸 계획이라면 14.4.11.4를 먼저 보라.ros2 param get으로 최종 값을 검증하는 것이 확실하다.
견해: 설정 파일에 주석으로 "왜 이 값인가"를 남기라. 튜닝된 값의 근거가 없으면 나중에 아무도 바꾸지 못하거나, 반대로 함부로 바꾼다. 측정 근거·날짜·담당을 적으면 지식이 유지된다.
견해: 설정을 버전 관리하고 로봇별 차이를 diff로 볼 수 있게 하라. 로봇 A는 되는데 B는 안 될 때, 설정 diff가 첫 번째 단서다. 파일이 잘 조직되어 있으면 이 비교가 쉽다.
14.4.5 변형 다루기
같은 코드로 여러 구성을 지원한다.
| 변형 축 | 예 |
|---|---|
| 실행 환경 | mock · sim · real(10.4.3) |
| 로봇 모델 | A형·B형·세대 |
| 하드웨어 옵션 | 센서 유무·팔 유무 |
| 운영 환경 | 공장·실험실·전시 |
| 모드 | 항법·조작·점검 |
# 환경 축 — 조건으로 분기
Node(package='my_driver', executable='real_driver',
condition=IfCondition(PythonExpression(
["'", LaunchConfiguration('backend'), "' == 'real'"])))
IncludeLaunchDescription(sim_launch,
condition=IfCondition(PythonExpression(
["'", LaunchConfiguration('backend'), "' == 'sim'"])))
견해: 변형 축이 셋을 넘으면
OpaqueFunction이 명확하다(14.3.6). 조건 조합이 복잡해지면PythonExpression이 읽을 수 없게 되므로, 실행 시점에 값을 읽어 Python으로 구성하는 편이 유지보수에 낫다.
견해: 변형을 "조합 폭발"로 만들지 말라. 모든 조합을 지원하려 하면 시험이 불가능해진다. 실제로 쓰는 조합만 정의하고 목록화하는 것이 실용적이다 — 그 목록이 곧 시험 대상이 된다(14.6).
함정: URDF·설정·launch가 각자 다른 방식으로 변형을 다루면 어긋난다. 5.6.6·10.4.3에서 강조한 단일 출처 원칙을 여기서도 지켜야 한다 — 하나의 인자(
backend)가 xacro·launch·설정을 함께 결정하도록.
14.4.6 다중 로봇
| 전략 | 특성 |
|---|---|
| ① 네임스페이스 | 서로 볼 수 있음·트래픽 공유 |
| ② 도메인 분리 | 격리·통신하려면 브리지 |
| ③ 완전 분리 | 최대 격리·복잡 |
# 네임스페이스 전략
GroupAction([
PushRosNamespace(LaunchConfiguration('robot_name')),
SetParameter('use_sim_time', LaunchConfiguration('use_sim_time')),
IncludeLaunchDescription(robot_launch, launch_arguments=...),
])
| 주의 | 설명 |
|---|---|
| tf 프레임 접두 | robot1/base_link(5.1.8) |
| 절대 이름 토픽 | 네임스페이스 무시됨(1.1.5) |
| 전역 토픽 | /clock·/tf는 공유 여부 결정 |
| 파라미터 YAML | 네임스페이스 포함 |
| 디스커버리 부하 | 로봇 수에 비례(3.3.4) |
함정:
/tf를 여러 로봇이 공유하면 프레임 이름이 충돌한다. 각 로봇의base_link가 같은 이름이면 tf 트리가 손상된다(5.1.4). 프레임에 접두사를 붙이거나(robot1/base_link),/tf를 네임스페이스 안으로 넣어야 한다.
함정: 코드에 절대 토픽 이름(
/scan)이 하드코딩되어 있으면 네임스페이스가 적용되지 않는다(1.1.5). 다중 로봇을 지원하려면 노드가 상대 이름을 쓰도록 작성해야 하며, 이것은 코드 수준의 준비다.
견해: 시뮬에서는 네임스페이스, 실기에서는 도메인 분리가 흔한 조합이다. 시뮬은 한 프로세스에서 여러 로봇을 다루므로 네임스페이스가 자연스럽고, 실기는 로봇마다 별도 컴퓨터이므로 도메인 분리가 격리와 부하 면에서 유리하다(14.9).
견해: 로봇 수가 늘면 디스커버리가 병목이 된다(3.3.4·3.4). 수십 대 규모에서는 Discovery Server나 도메인 분리를 고려해야 하며, 이것은 launch 구성보다 상위의 아키텍처 결정이다.
14.4.7 재사용 도구
공통 패턴을 함수로 만든다.
# my_bringup/launch_utils.py (패키지 내 파이썬 모듈)
def robot_node(package, executable, name, cfg_files, extra=None, **kw):
"""표준 인자·파라미터·리매핑을 적용한 Node 생성."""
params = list(cfg_files)
params.append({'use_sim_time': LaunchConfiguration('use_sim_time')})
if extra:
params.append(extra)
return Node(package=package, executable=executable, name=name,
parameters=params,
arguments=['--ros-args', '--log-level',
LaunchConfiguration('log_level')],
output='screen', **kw)
| 재사용 대상 | 예 |
|---|---|
| 노드 생성 헬퍼 | 공통 인자 적용 |
| 인자 선언 묶음 | 표준 인자 세트 |
| include 헬퍼 | 경로·인자 전파 |
| 조건 표현 | 자주 쓰는 조건 |
| enclave 지정 | 보안(13.2.6) |
견해: 헬퍼 함수가 일관성을 보장하는 가장 값싼 수단이다. 로그 레벨·
use_sim_time·enclave를 노드마다 수동으로 넣으면 빠뜨린다. 한 함수를 거치게 하면 전부에 적용된다.
함정: launch 유틸리티 모듈을 import하려면 패키지가 설치되어 있어야 한다.
setup.py·CMakeLists.txt에 포함시키고, 개발 중에는--symlink-install을 쓰면 편리하다(4.5).
견해: 다만 추상화를 과하게 하면 launch를 읽어도 무엇이 실행되는지 모르게 된다. launch의 가치 하나는 "구성을 읽을 수 있다" 는 것이므로, 헬퍼는 얇게 유지하는 것이 좋다.
14.4.8 검증
launch도 시험 대상이다.
| 시험 | 방법 |
|---|---|
| 구문·구성 오류 | import·기술 시점 실행 |
| 인자 목록 | --show-args |
| 노드 기동 | launch_testing(14.6) |
| 파라미터 적용 | ros2 param dump 비교 |
| 변형 조합 | 정의된 조합 전부 |
| 종료 | 정상 종료되는가 |
# 최소 시험: 기술이 오류 없이 만들어지는가
def test_launch_description_builds():
from my_bringup.launch.robot_launch import generate_launch_description
ld = generate_launch_description()
assert ld is not None
견해: "launch 파일이 오류 없이 기동되는가"를 CI에 넣는 것만으로도 가치가 크다(4.10). launch 오류는 대개 현장에서 발견되는데, 경로 오타·인자 누락은 CI에서 검출할 수 있다.
견해: 정의된 변형 조합 목록을 CI에서 전부 기동해 보라(14.4.5). 조합이 다섯 개면 다섯 번 실행하고 노드가 다 기동됐는지 확인하는 것으로, "이 조합은 기동되지 않는다"를 배포 전에 알 수 있다.
함정: CI에서 하드웨어가 없어 실패하는 노드가 있다. mock 백엔드(6.2.8)나 시뮬 조합만 시험하거나, 기동 확인 대상을 선별해야 한다.
14.4.11 파라미터 병합 의미론을 실제로 확인했다 — 실측
14.4.4는 "뒤가 앞을 덮어쓰는 성질을 이용해 [common, robot_a, sim_overrides] 순으로 주면 된다"고 적고, 중첩 구조의 부분 덮어쓰기가 기대대로 되는지 확인해야 한다(확인 필요)고 남겼다. 확인했다(실행 확인, Jazzy).
방법:
automatically_declare_parameters_from_overrides=True로 노드를 실행하고 최종 파라미터를 전부 출력했다. 파일 순서·지정자·명령줄을 조합해 각각의 결과를 봤다.
14.4.11.1 부분 덮어쓰기는 딥 머지다 — 기대대로 동작한다
A.yaml이 top_a·shared·sub.x·sub.y를, B.yaml이 shared·sub.y만 지정한 경우:
| 구성 | 결과 |
|---|---|
| A만 | top_a=1 shared=from_A sub.x=10 sub.y=20 |
| B만 | shared=from_B sub.y=99 |
| A → B | top_a=1 유지됨 · shared=from_B · sub.x=10 유지됨 · sub.y=99 |
B가 언급하지 않은 키는 그대로 남는다. 파일 전체 교체가 아니라 키 단위 병합이며, 중첩 구조도 부분 덮어쓰기가 된다(sub.y만 바꿔도 sub.x는 유지). 14.4.4가 권한 "공통 + 덮어쓰기" 구조가 의도대로 작동한다.
순서를 뒤집으면(B → A) shared=from_A·sub.y=20이 되어 뒤가 앞을 덮는 성질도 확인된다.
14.4.11.2 그러나 순서보다 강한 것이 있다 — 지정자의 구체성
여기서 이 절의 핵심이 나온다. common.yaml(/**:)·robot_a.yaml(/pnode:)·wild_after.yaml(/**:)을 이 순서대로 줬다.
| 파일 순서 | mode의 최종 값 |
|---|---|
공통(/**:) → 로봇별(/pnode:) | robot_a — 로봇별이 우선한다 |
로봇별(/pnode:) → 공통(/**:) | common — 뒤가 우선한다 |
| 공통 → 로봇별 → 와일드카드(가장 나중) | robot_a ← 나중 파일이 졌다 |
| 공통 → 와일드카드(로봇별 없이) | wildcard_later — 순서대로 |
| 공통 → 로봇별 → 노드 이름으로 지정한 나중 파일 | specific_later |
가장 나중 파일인데도 /**:이면 앞선 /노드이름:에 우선하지 못한다(3회 반복 동일). 로봇별 파일을 빼면 같은 파일이 우선하므로, 순서 규칙이 깨진 것이 아니라 구체성이 먼저 적용되는 것이다.
규칙은 두 단계다.
- 지정자의 구체성이 먼저 —
/노드이름:>/**:- 같은 구체성 안에서 뒤가 앞을 덮는다
14.4.4의 "뒤가 앞을 덮는다"는 ②에만 해당한다.
14.4.11.3 가장 실무적인 함정 — 명령줄 -p도 진다
| 구성 | 결과 |
|---|---|
/**: 파일 + -p mode:=from_cli | from_cli — 명령줄이 우선한다 |
/노드이름: 파일 + -p mode:=from_cli | robot_a — 명령줄이 진다 |
명령줄 -p는 와일드카드와 같은 수준으로 취급된다. 노드 이름으로 지정된 YAML 항목은 명령줄로 덮을 수 없다.
이것이 현장에서 피해가 크다. "급하니
-p로 값 하나만 바꿔 보자"가 조용히 무시된다 — 오류도 경고도 없고, 값이 아무 표시 없이 안 바뀐다. 11.1.14.3이 측정한 "YAML 노드 이름 불일치"와 증상이 똑같지만 원인은 정반대다: 그때는 이름이 안 맞아서 무시됐고, 이번엔 이름이 맞아서 명령줄에 우선한다.진단:
-p가 안 먹으면 어느 파일이 그 노드를 이름으로 지정하고 있는지 찾으라.ros2 param get으로 실제 값을 보는 것이 먼저다(1.9.10).
14.4.11.4 sim_overrides.yaml을 쓸 때의 결론
14.4.4의 예시 순서 [common, robot_a, sim_overrides]는 sim_overrides가 무엇으로 지정되는지에 달렸다.
sim_overrides.yaml의 지정자 | 로봇별 파일의 값을 덮는가 |
|---|---|
/**: (use_sim_time 같은 전역 설정에 자연스러운 선택) | 덮지 못한다 |
/노드이름: | 덮는다 |
use_sim_time처럼 전역으로 주고 싶은 값은/**:이 맞지만, 그 키를 로봇별 파일이 이름으로 지정해 두었다면 시뮬 오버라이드가 먹지 않는다. 전역 오버라이드를 확실히 하려면 명령줄이 아니라 같은 구체성(노드 이름)으로 지정하거나, 로봇별 파일에서 그 키를 빼야 한다.
14.4.11.5 확인하지 못한 것
- launch 파일의
parameters=[...]목록에서도 같은 규칙인지는 직접 확인하지 않았다 — 이 실험은--ros-args --params-file로 했다(확인 필요). rcl의 같은 경로를 쓰므로 같을 것으로 보이나 확인하지 않았다. /**와/*등 중간 구체성(14.3.12)이 섞였을 때의 순위는 측정하지 않았다(확인 필요).- 배열·중첩 딕셔너리의 부분 갱신(원소 단위)은 시험하지 않았다.
14.4.9 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | 단일 거대 launch | 유지 불가 | 계층화 |
| 2 | 하위가 상위를 앎 | 재사용 불가 | 인자로만 |
| 3 | 인자 전파 누락 | 기본값 적용 | 헬퍼·명시 전달 |
| 4 | 인자 폭증 | 관리 어려움 | 설정 파일로 |
| 5 | 하위 기본값 충돌 | 의도와 다름 | 핵심은 명시 전달 |
| 6 | 설정 병합 규칙 미확인 | 예상 밖 값 | param dump 검증 |
| 7 | 설정 근거 미기록 | 변경 불가 | 주석 |
| 8 | 변형 조합 폭발 | 시험 불가 | 조합 목록화 |
| 9 | 변형 축이 여러 곳에 흩어짐 | 어긋남 | 단일 출처 |
| 10 | 다중 로봇 tf 충돌 | 트리 손상 | 프레임 접두 |
| 11 | 절대 토픽 하드코딩 | NS 무효 | 상대 이름 |
| 12 | 로봇 수에 따른 디스커버리 부하 | 지연·불안정 | 도메인·서버 |
| 13 | 헬퍼 과추상화 | 읽을 수 없음 | 얇게 |
| 14 | launch 시험 없음 | 현장 발견 | CI 기동 시험 |
| 15 | CI에서 하드웨어 의존 | 실패 | mock·선별 |
| 16 | 나중 /**: 파일로 로봇별 값을 덮으려 함 | 안 덮인다 — 구체성이 순서에 우선한다 | 같은 구체성으로 지정(14.4.11.2) |
| 17 | -p로 급히 값 변경 | 노드 이름 지정 항목은 조용히 안 바뀜 | ros2 param get으로 확인(14.4.11.3) |
14.4.10 체크포인트
- launch 계층 구조의 세 층과 네 원칙을 말할 수 있는가.
- "서브시스템 launch가 단독 실행 가능해야 한다"의 이점은?
- 인자 전파의 네 문제와 대처를 들 수 있는가.
- 인자와 설정 파일의 경계를 어디에 두는가.
SetParameter가 인자 전파를 줄이는 방식과 그 주의점은?- 하위 launch의 기본값이 만드는 문제와 규율은?
- 설정 파일 조직 네 방식과, 여러 로봇에 유리한 구조는?
- 설정 병합에서 검증이 필요한 이유와 방법은?
- 설정 주석에 무엇을 남겨야 하는가.
- 변형 축 다섯 가지와, 조합 폭발을 피하는 방법은?
- 변형에서 단일 출처 원칙이 왜 중요한가.
- 다중 로봇 세 전략과 각각의 특성은?
- 다중 로봇에서 tf와 토픽 이름의 두 함정과 대처는?
- 시뮬과 실기에서 다른 전략을 쓰는 이유는?
- 재사용 헬퍼의 이점과, 과추상화의 위험은?
- launch 검증 여섯 가지와, CI에 넣을 최소 시험은?
- 파라미터 파일 병합은 전체 교체인가 키 단위 병합인가. 중첩 구조는 어떤가(14.4.11.1).
- "뒤가 앞을 덮는다"가 성립하지 않는 경우는 언제인가(14.4.11.2).
- 명령줄
-p가 조용히 무시되는 조건은(14.4.11.3).
다음 장 14.5은 시험의 세계로 들어간다 — 단위 시험과 모의 그래프로 로봇 소프트웨어를 검증하는 방법.