제4부 빌드·패키징·배포
4.1 ament 빌드 시스템의 구조
4.1.1 이 장에서 배우는 것 / 선수 지식
여기까지 ROS 2가 어떻게 동작하는지를 봤다. 제4부는 어떻게 만들어지는지를 다룬다. 소스 파일이 패키지가 되고, 패키지가 워크스페이스가 되고, 워크스페이스가 로봇 위의 바이너리가 되기까지.
첫 장의 주제는 ament다. 그런데 ament는 흔히 오해된다 — "ROS 2의 빌드 시스템"이라고 부르지만, 정확히는 빌드 시스템이 아니다. 빌드는 CMake와 setuptools가 하고, ament는 그 위에 패키지를 발견하고 연결하는 규약을 추가한다. 이 구분을 이해하면 "왜 빌드는 됐는데 런타임에 못 찾지" 같은 문제가 즉시 설명된다.
배우는 것:
- ament의 정체 — 무엇을 하고 무엇을 하지 않는가.
- ament 인덱스 — ROS 2가 패키지를 찾는 실제 메커니즘.
- 설치 레이아웃 —
lib·share·include에 무엇이 왜 들어가는가. - 환경 훅 —
setup.bash가 자동 생성되는 원리. - 빌드 타입 —
ament_cmake·ament_python·혼합. - merge vs isolated 설치 — 두 레이아웃의 차이와 함정.
- catkin과의 관계 — ROS 1 경험자가 알아야 할 변화.
선수 지식: 0.5(워크스페이스·오버레이), 0.6(첫 패키지). colcon은 4.5, package.xml 상세는 4.4에서 다룬다.
정직: ament의 CMake 매크로·인덱스 구조는
ament/ament_cmake·ament/ament_package저장소에 근거한다. 매크로 이름·권장 사용법은 배포판마다 변해 왔으며(특히ament_target_dependencies의 위치 — 4.2), 아래 서술은 Jazzy 기준으로 작성했으나 이 환경에서 빌드 검증하지 않았다(목차 3). 145차(124번째 회차)가 이 장의 핵심 주장들을 Humble·Jazzy 양쪽에서 실행 확인했다 —DESTINATION bin함정은 정확히 맞았고,ament_package()위치 함정은 서술이 부정확하다는 것도 함께 확인됐다. 4.1.4·4.1.10을 보라.
4.1.2 ament는 무엇이고 무엇이 아닌가
| 층 | 하는 일 | 하지 않는 일 |
|---|---|---|
| colcon | 패키지 순서 결정, 병렬 실행, 환경 구성 | 컴파일 |
| CMake·setuptools | 실제 컴파일·설치 | 패키지 발견 |
| ament | 인덱스 등록·의존 전파·환경 훅·설치 규약 | 컴파일, 워크스페이스 관리 |
ament가 실제로 제공하는 것 넷:
- 패키지 인덱스 — "이 접두어에 어떤 ROS 패키지가 설치되어 있는가"를 빠르게 알 수 있는 색인(4.1.3).
- 의존 전파 규약 — 패키지 A가 B를 쓸 때 헤더 경로·라이브러리·컴파일 옵션이 자동으로 따라오게 하는 CMake 규약(4.2).
- 환경 훅 — 설치된 패키지가 필요로 하는 환경변수(
PYTHONPATH·LD_LIBRARY_PATH등)를 선언하고, 그것을 모아setup.bash를 생성하는 체계. - 설치 레이아웃 규약 — 무엇을 어디에 설치할지의 합의(4.1.4).
이 넷은 전부 "빌드 이후"의 문제다. 그래서 빌드가 성공했는데 런타임에 실패하는 상황이 ROS 2에서 흔하다 — 컴파일은 CMake가 했고, 발견은 ament가 하는데, 후자만 잘못될 수 있기 때문이다.
| 증상 | 대개 어느 층 |
|---|---|
| 컴파일 오류·링크 오류 | CMake·컴파일러 |
빌드는 되는데 ros2 run이 실행 파일을 못 찾음 | ament 설치 규약 |
패키지가 ros2 pkg list에 안 나옴 | ament 인덱스 |
| launch 파일·설정 파일을 런타임에 못 찾음 | 설치 누락(4.1.4) |
| 다른 패키지의 헤더를 못 찾음 | 의존 전파(4.2) |
4.1.3 ament 인덱스 — 패키지를 찾는 메커니즘
ros2 pkg list는 어떻게 패키지를 아는가? 디렉터리를 뒤지는 것이 아니라 색인 파일을 읽는다.
인덱스는 <install prefix>/share/ament_index/resource_index/ 아래에 자원 종류별 디렉터리를 두고, 그 안에 패키지 이름을 파일명으로 둔다.
| 경로 | 파일 내용 |
|---|---|
resource_index/packages/my_robot_driver | 빈 파일 — 존재 자체가 등록 |
resource_index/packages/my_robot_msgs | 빈 파일 |
resource_index/rclcpp_components/my_pkg | 등록된 컴포넌트 목록(1.13) |
resource_index/rosidl_interfaces/my_robot_msgs | 제공하는 인터페이스 목록(1.2) |
| 자원 종류 | 용도 |
|---|---|
packages | 패키지 존재 등록. ros2 pkg list의 출처 |
rosidl_interfaces | 이 패키지가 제공하는 msg·srv·action(1.2.10) |
rclcpp_components | 등록된 컴포넌트(1.13.3) |
ros2cli.command·ros2cli.verb.* | CLI 확장(0.7.2) |
pluginlib 관련 | 플러그인 등록(4.2) |
빈 파일의 존재가 등록이다 — 내용을 파싱할 필요 없이 디렉터리 목록만 읽으면 되므로 매우 빠르다. 이것이 ROS 1의 패키지 탐색(디렉터리 재귀 탐색)보다 개선된 점이다.
탐색 경로는 AMENT_PREFIX_PATH가 정한다(0.5).
echo $AMENT_PREFIX_PATH
# /home/user/ws/install/my_pkg:/opt/ros/jazzy
# 특정 패키지의 설치 위치
ros2 pkg prefix my_robot_driver
# 인덱스 직접 확인
ls /opt/ros/jazzy/share/ament_index/resource_index/packages | head
함정: 패키지가 목록에 없다면 인덱스에 등록이 안 된 것이다. 대개 원인은 셋 — ① 워크스페이스를 소싱하지 않음(0.5), ②
ament_package()호출 누락, ③ 빌드가 실제로는 실패했는데 알아채지 못함. 이 순서로 확인한다.
# ② 확인: install 디렉터리에 등록됐는가
find install -path '*resource_index/packages/*' | head
4.1.4 설치 레이아웃
ament는 FHS(파일 시스템 계층 표준)에 가까운 레이아웃을 규정한다.
| 경로 | 내용 | 누가 쓰나 |
|---|---|---|
lib/ | 공유 라이브러리(.so) | 링커·dlopen |
lib/<pkg>/ | 실행 파일 | ros2 run |
include/<pkg>/ | 헤더 | 의존 패키지 컴파일 |
share/<pkg>/ | launch·config·URDF·메시지 정의 등 | 런타임 리소스 |
share/<pkg>/cmake/ | CMake config 파일 | find_package |
share/ament_index/ | 인덱스(4.1.3) | ROS 도구 |
lib/python3.X/site-packages/ | Python 모듈 | Python 임포트 |
lib/<pkg>/에 실행 파일을 두는 것이 ROS 2의 규약이다. ros2 run my_pkg my_node는 lib/my_pkg/my_node를 찾는다.
install(TARGETS my_node
DESTINATION lib/${PROJECT_NAME}) # ← 이 경로여야 ros2 run이 찾는다
함정:
DESTINATION bin에 설치하면ros2 run이 못 찾는다. 컴파일도 되고 파일도 있는데 실행이 안 되는 전형적 사례다. 직접 경로로 실행하면 되므로 더 혼란스럽다.
실행 확인(145차, Jazzy).
install(TARGETS mynode DESTINATION bin)으로 빌드한 뒤 정확히 이 함정 그대로 재현됐다 —ros2 run cpptest_pkg mynode는No executable found로 실패했고ros2 pkg executables cpptest_pkg도 빈 목록을 냈지만,install/cpptest_pkg/bin/mynode를 직접 실행하니 정상 동작했다(MYNODE_RAN). 목차 3.
런타임 리소스는 반드시 명시적으로 설치해야 한다.
install(DIRECTORY launch config urdf rviz
DESTINATION share/${PROJECT_NAME})
# 코드에서 찾기
from ament_index_python.packages import get_package_share_directory
config = os.path.join(get_package_share_directory('my_pkg'), 'config', 'params.yaml')
#include "ament_index_cpp/get_package_share_directory.hpp"
auto share = ament_index_cpp::get_package_share_directory("my_pkg");
함정: "소스에 있으니 되겠지"가 안 통한다. 소스 디렉터리에서 실행할 때는 우연히 동작할 수 있지만, 설치본에서는 파일이 없다. 설치 규칙을 안 쓴 launch·config 파일은 배포 시점에 사라진다 — 개발 중에는 드러나지 않다가 로봇에 배포하고 발견하는 대표적 문제다.
검증 습관:
# 설치본에 실제로 무엇이 들어갔는가
find install/my_pkg -type f | sort
# 소스에서 나와 설치본만으로 실행해 본다
cd /tmp && source ~/ws/install/setup.bash && ros2 launch my_pkg bringup.launch.py
4.1.5 환경 훅과 setup.bash 생성
source install/setup.bash 한 줄이 무엇을 하는가.
패키지가 자기 훅을 선언할 수 있다.
# 예: 커스텀 환경변수를 설치 시 등록
ament_environment_hooks("${CMAKE_CURRENT_SOURCE_DIR}/env-hooks/my_pkg.sh.in")
이것이 예를 들어 GAZEBO_MODEL_PATH(10.2)나 플러그인 경로를 자동으로 설정하는 방식이다.
setup.bash와 local_setup.bash의 차이가 중요하다:
| 스크립트 | 동작 |
|---|---|
setup.bash | 상위 워크스페이스(언더레이)를 먼저 소싱한 뒤 이 워크스페이스 적용 |
local_setup.bash | 이 워크스페이스만 적용 |
# 일반적: 오버레이 체인 전체
source ~/ws/install/setup.bash
# 언더레이를 이미 소싱했고 이것만 추가하고 싶을 때
source /opt/ros/jazzy/setup.bash
source ~/ws/install/local_setup.bash
함정:
setup.bash를 여러 워크스페이스에서 반복 소싱하면 경로가 중복·혼재된다(0.5의 셸 오염 함정). 새 터미널에서 시작하는 것이 가장 안전하며, 스크립트에서는 소싱 순서를 명시적으로 관리한다.
4.1.6 빌드 타입
package.xml의 <export>에서 선언한다(4.4).
<export>
<build_type>ament_cmake</build_type>
</export>
| 빌드 타입 | 대상 | 빌드 도구 |
|---|---|---|
ament_cmake | C++ 패키지, 인터페이스 패키지 | CMake |
ament_python | 순수 Python 패키지 | setuptools(4.3) |
ament_cmake_python | 혼합(C++ + Python 모듈) | CMake + setuptools |
cmake | ament 규약을 안 쓰는 순수 CMake | CMake |
혼합 패키지가 필요한 경우: C++ 노드와 Python 유틸리티를 한 패키지에 두거나, 인터페이스 패키지에서 Python 헬퍼를 함께 제공할 때.
# ament_cmake_python 사용 시
find_package(ament_cmake_python REQUIRED)
ament_python_install_package(${PROJECT_NAME})
install(PROGRAMS scripts/my_script.py DESTINATION lib/${PROJECT_NAME})
견해: 혼합 패키지는 가능하지만 권하지 않는다. 빌드 규칙이 복잡해지고 오류 원인이 두 배가 된다. C++ 패키지와 Python 패키지를 나누는 편이 대개 낫다 — 인터페이스 패키지를 분리하라는 1.2.5의 조언과 같은 논리다. 단, 컴포넌트와 그 Python 런처처럼 결합이 강한 경우는 예외로 볼 수 있다.
4.1.7 merge install vs isolated install
colcon이 설치본을 배치하는 두 방식이다(4.5).
| 축 | isolated(기본) | merge(--merge-install) |
|---|---|---|
| 구조 | 패키지마다 접두어 디렉터리 | 하나의 접두어에 통합 |
AMENT_PREFIX_PATH | 패키지 수만큼 항목 | 하나 |
| 패키지 제거 | 디렉터리 삭제로 가능 | 어려움 |
| 환경변수 길이 | 매우 길어질 수 있다 | 짧다 |
| 배포 | 디렉터리가 많다 | 단순 |
| 디버깅 | 어느 패키지 것인지 명확 | 섞여 있다 |
AMENT_PREFIX_PATH가 길어지는 문제가 실질적이다. 패키지 200개를 isolated로 빌드하면 경로 항목이 200개가 되고, 환경변수 길이 한계나 셸 성능 문제가 생길 수 있다. 대규모 워크스페이스에서 --merge-install을 쓰는 이유다.
colcon build --merge-install
echo $AMENT_PREFIX_PATH | tr ':' '\n' | wc -l # 항목 수 확인
함정: merge와 isolated를 섞으면 안 된다. 같은
install디렉터리에 두 방식이 섞이면 예측 불가능한 상태가 된다. 방식을 바꾸려면install·build디렉터리를 지우고 다시 빌드해야 한다.
rm -rf build install log && colcon build --merge-install
견해: 개발 중에는 기본(isolated), 배포·CI에서는
--merge-install이 흔한 조합이다. 다만 개발과 배포의 설치 레이아웃이 다르면 배포에서만 나타나는 문제가 생길 수 있으므로, CI에서 배포와 같은 방식으로 한 번은 빌드·시험하는 것이 좋다(4.10).
4.1.8 catkin에서 무엇이 바뀌었나
ROS 1 경험자를 위한 대조다.
| 축 | catkin(ROS 1) | ament(ROS 2) |
|---|---|---|
| 워크스페이스 빌드 | catkin_make·catkin build | colcon(별도 도구) |
| devel space | 있음 — 빌드 결과를 소스에서 바로 사용 | 없음 |
| 패키지 발견 | 디렉터리 탐색 | 인덱스(4.1.3) |
| 설치 필수 여부 | devel space로 생략 가능 | 설치가 기본 |
| CMake 매크로 | catkin_package() | ament_package() |
| 의존 전파 | catkin_INCLUDE_DIRS 등 통합 변수 | 패키지별 타깃(4.2) |
| 메시지 생성 | add_message_files·generate_messages | rosidl_generate_interfaces(1.2.5) |
| Python | catkin_python_setup() | ament_python 또는 ament_cmake_python |
"devel space가 없다"가 가장 큰 실무 변화다. ROS 1에서는 빌드 후 소스 트리의 파일을 그대로 참조할 수 있었지만, ROS 2에서는 설치 단계를 거쳐야 한다. 그래서 4.1.4에서 본 "설치 규칙을 안 쓰면 파일이 사라진다" 문제가 ROS 2 특유의 함정이 된다.
--symlink-install이 이 불편을 완화한다(4.3·4.5) — 복사 대신 심볼릭 링크를 만들어 소스 수정이 즉시 반영되게 한다. Python·설정 파일 개발에서 사실상 필수다.
colcon build --symlink-install
함정:
--symlink-install도 모든 경우를 해결하지는 못한다. 새 파일 추가·setup.py변경·CMake 규칙 변경은 재빌드가 필요하다. 그리고 심링크된 워크스페이스를 그대로 배포하면 소스가 없는 곳에서 깨진다 — 배포본은 심링크 없이 빌드해야 한다.
4.1.9 진단 — 빌드는 됐는데 안 될 때
# 패키지 등록 확인
ros2 pkg list | grep my_pkg
ros2 pkg prefix my_pkg
# 실행 파일 확인
ros2 pkg executables my_pkg
ls install/my_pkg/lib/my_pkg/
# 설치본 내용 전체
find install/my_pkg -type f | sort
# 라이브러리 의존 확인
ldd install/my_pkg/lib/my_pkg/my_node | grep 'not found'
# 환경 확인
echo $AMENT_PREFIX_PATH | tr ':' '\n'
echo $LD_LIBRARY_PATH | tr ':' '\n'
ldd ... | grep 'not found'가 특히 유용하다 — 링크는 됐는데 런타임에 라이브러리를 못 찾는 경우를 즉시 드러낸다.
4.1.10 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | ament_package() 누락 | 인덱스 미등록 | CMakeLists 마지막에 호출 |
| 2 | ament_package()를 중간에 호출 | 이후 설치 규칙 무시 | 반드시 마지막 |
| 3 | 실행 파일을 bin에 설치 | ros2 run이 못 찾음 | lib/${PROJECT_NAME} |
| 4 | launch·config 설치 규칙 누락 | 배포본에서 파일 없음 | install(DIRECTORY …) |
| 5 | 소스 디렉터리 기준 경로 하드코딩 | 설치본에서 실패 | get_package_share_directory |
| 6 | 워크스페이스 미소싱 | 패키지 없음 | source install/setup.bash |
| 7 | setup.bash 반복 소싱 | 경로 오염 | 새 터미널(0.5) |
| 8 | merge·isolated 혼용 | 예측 불가 상태 | build·install 삭제 후 재빌드 |
| 9 | 심링크 워크스페이스 배포 | 대상에서 깨짐 | 배포본은 심링크 없이 |
| 10 | 개발과 배포 레이아웃 상이 | 배포에서만 실패 | CI에서 배포 방식 검증 |
| 11 | 혼합 빌드 타입 남용 | 빌드 복잡·오류 증가 | 패키지 분리 |
| 12 | 빌드 실패를 못 알아챔 | "패키지가 없다" | 빌드 로그 확인(4.5) |
2번을 강조한다. ament_package()는 CMakeLists.txt의 마지막 호출이어야 한다. 그 뒤에 오는 install() 등은 무시되며, 오류 메시지가 나지 않아 원인을 찾기 어렵다. (→ 145차 정정: "무시되고 조용하다"는 서술은 부정확하다 — 실제로는 매크로별로 갈린다. 아래 실행 확인 참고.)
실행 확인(145차, Humble·Jazzy 양쪽) — "무시되며 조용히 실패한다"는 서술을 정정한다.
ament_package()를 CMakeLists.txt 중간에 두고 그 뒤에 두 종류의 코드를 각각 시험했다.
install(TARGETS mynode DESTINATION lib/${PROJECT_NAME})를ament_package()뒤에 두었더니 — 무시되지 않았다. 빌드가 정상 완료됐고,ros2 pkg executables에도 나왔고,ros2 run도 정상 동작했다(MYNODE_RAN, Humble·Jazzy 둘 다 동일). 이 경우엔 순서가 문제가 되지 않는다.ament_export_targets(...)·ament_export_dependencies(...)를ament_package()뒤에 두었더니 — 조용히 무시되지 않고, 그 자리에서 명확한 CMake 오류로 빌드가 즉시 실패했다:ament_export_targets() must be called before ament_package(),ament_export_dependencies() must be called before ament_package(). 원인이 오류 메시지에 정확히 적혀 있어 진단이 어렵지 않다.결론: 이 함정의 실제 위험은 "조용한 무시"가 아니라 "매크로마다 다른 결과"다 — 어떤 것(
install)은 순서와 무관하게 별도 처리 없이 동작하고, 어떤 것(ament_export_*)은 순서를 어기면 명확한 오류로 즉시 막힌다. "무시되고 오류 메시지가 안 난다"는 서술은 시험한 두 경우 모두에 해당하지 않았다 — 그래도 "ament_package()는 마지막에"라는 실무 조언 자체는 여전히 옳다(export 계열 매크로에서 확실히 걸리므로). 목차 3,tools/gap_harness/gap145.
4.1.11 체크포인트
- ament가 하는 일 네 가지와 하지 않는 일을 구분할 수 있는가. "빌드는 됐는데 런타임에 실패"가 왜 ROS 2에서 흔한가.
- ament 인덱스가 무엇이며, 빈 파일의 존재가 등록인 이유는?
ros2 pkg list의 출처는? - 실행 파일을
lib/<pkg>/에 설치해야 하는 이유와,bin에 설치했을 때의 증상은? - launch·config 파일이 배포본에서 사라지는 이유와, 그것을 개발 중에 발견하는 방법은?
setup.bash와local_setup.bash의 차이를 말할 수 있는가.- 네 가지 빌드 타입과 각각의 대상은? 혼합 패키지를 권하지 않는 이유는?
- merge install과 isolated install의 차이 여섯 가지를 들 수 있는가. 대규모 워크스페이스에서 merge를 쓰는 실질적 이유는?
- catkin의 devel space가 없어진 것이 실무에 어떤 변화를 가져왔는가.
--symlink-install이 무엇을 완화하고 무엇을 못 하는가. - "빌드는 됐는데 실행이 안 된다"를 진단하는 순서와 각 단계의 명령을 말할 수 있는가.
ament_package()의 호출 위치가 왜 중요한가.
다음 장 4.2는 ament_cmake의 내부로 들어간다 — 의존 전파, 라이브러리 내보내기, 심볼 가시성, 그리고 현대 CMake 방식으로의 전환.