AEGLOSLAB ROS 2 Jazzy 대전

제0부 개관과 출발

0.5 워크스페이스·오버레이·언더레이의 원리

0.5.1 이 장에서 배우는 것 / 선수 지식

"분명히 고쳤는데 옛날 코드가 실행된다." "삭제한 패키지가 아직 목록에 보인다." "동료 컴퓨터에서는 되는데 내 것에서는 링크 오류가 난다." ROS 2 개발에서 가장 흔하고, 가장 오래 진단 시간을 소모시키는 부류의 문제다. 원인은 거의 언제나 워크스페이스와 오버레이의 해석 규칙을 정확히 모르는 데 있다.

이 장은 그 규칙을 환경변수 수준까지 내려가서 확정한다. 이해하고 나면 위 증상들은 5분이면 진단된다.

배우는 것:

  • 워크스페이스의 물리적 구조 — src·build·install·log가 각각 무엇이고 언제 지워도 되나.
  • 오버레이와 언더레이 — 개념이 아니라 AMENT_PREFIX_PATH의 순서라는 실체.
  • setup.bash와 local_setup.bash의 차이 — 왜 둘이 있고 언제 무엇을 쓰나.
  • 패키지 해석 과정 — ros2 run이 실행 파일을 찾는 경로와 find_package가 라이브러리를 찾는 경로.
  • --symlink-install의 정확한 동작과 한계 — 무엇이 링크되고 무엇이 복사되나.
  • 재빌드·재source가 필요한 시점 표와, 대표적 오염 증상 다섯 가지의 진단·복구.
  • 워크스페이스 분리 전략 — 서드파티·자사 코드·실험을 어떻게 층으로 나눌 것인가.

선수 지식: 0.4(환경변수·AMENT_PREFIX_PATH의 존재). CMake의 기초 개념(find_package·설치 접두 경로).

정직: 아래 명령·경로는 공식 문서와 colcon·ament 동작에 근거하지만 이 집필 환경에서 실행 검증되지 않았다(목차 3). 특히 --symlink-install의 세부 동작은 빌드 타입(ament_cmake vs ament_python)과 설치 규칙에 따라 달라지므로, 팀 환경에서 한 번 직접 확인해 두기를 권한다. 예외 — 오버레이가 언더레이에 우선하는 메커니즘 자체는 93차 실측으로 확인했다(0.5.3.1). "source 순서가 우선순위를 만든다"는 이 장의 원래 설명은 표준 빌드 절차(언더레이 source 후 오버레이 빌드)를 따랐다면 더 안전한 방향으로 불완전했다 — 실제로는 순서를 잘못 지켜도 오버레이가 우선한다. 나머지 서술은 여전히 미검증이다.

0.5.2 워크스페이스의 물리적 구조

ROS 2 워크스페이스는 네 개의 디렉터리로 이루어진 관행적 구조다.

워크스페이스 루트 ~/ros2_ws src/ 소스 코드 (사람이 관리) build/ 중간 산출물 (지워도 됨) install/ 설치 결과 (지워도 됨) log/ 빌드 로그 (지워도 됨) 패키지 A package.xml 패키지 B package.xml
디렉터리내용지워도 되나비고
src/패키지 소스. package.xml이 있는 디렉터리가 곧 패키지아니오버전 관리 대상
build/CMake 캐시·오브젝트 파일·중간 산출물예지우면 전체 재빌드
install/최종 산출물(실행 파일·라이브러리·share/·환경 스크립트)예런타임이 실제로 보는 곳
log/colcon 실행 로그예실패 원인 추적용

핵심 사실 하나: ROS 2에는 ROS 1의 devel 공간이 없다. 실행되는 것은 언제나 install/ 안의 사본이며, src/의 파일이 아니다. "고쳤는데 반영이 안 된다"의 절반은 이 사실을 잊은 데서 온다(--symlink-install이 이 간극을 좁힌다 — 0.5.6).

0.5.3 오버레이·언더레이의 실체는 경로 순서다

언더레이(underlay) 는 아래에 놓인 환경, 오버레이(overlay) 는 그 위에 구성한 환경이다. 전형적으로:

언더레이: /opt/ros/jazzy apt로 설치된 코어·생태계 오버레이 1: ~/third_party_ws/install 소스로 빌드한 서드파티 오버레이 2: ~/robot_ws/install 우리 패키지 최종 환경 위쪽이 우선

이 "층"의 실체는 AMENT_PREFIX_PATH 환경변수의 콜론 구분 순서다.

$ echo "$AMENT_PREFIX_PATH"
/home/user/robot_ws/install:/home/user/third_party_ws/install:/opt/ros/jazzy

규칙은 단순하다 — 앞에 있는 경로가 우선한다. 같은 이름의 패키지가 여러 경로에 있으면 앞의 것이 쓰인다. source할 때마다 해당 경로가 앞에 덧붙는다(prepend).

따라서 source하는 순서가 우선순위를 만든다. 언더레이를 먼저, 오버레이를 나중에 source해야 오버레이가 우선한다.

source /opt/ros/jazzy/setup.bash          # 먼저: 언더레이
source ~/third_party_ws/install/setup.bash # 다음
source ~/robot_ws/install/setup.bash       # 마지막: 최우선

같은 원리로 CMAKE_PREFIX_PATH(빌드 시 find_package 탐색), LD_LIBRARY_PATH(공유 라이브러리), PYTHONPATH(Python 모듈), PATH(실행 파일)도 함께 갱신된다. 즉 오버레이는 하나의 개념이 아니라 다섯 개 경로 변수의 동시 조작이다. 이것이 "환경이 오염됐다"는 상태가 실제로 뜻하는 바다.

0.5.3.1 "순서를 지켜야 오버레이가 우선한다"는 절반만 맞다 — 실측

실행 확인(2026-08-18, ros:jazzy-ros-base, colcon) — 이름이 같은 패키지 dup_pkg를 언더레이·오버레이 두 워크스페이스에 각각 다른 내용(각각 UNDERLAY/OVERLAY를 출력)으로 빌드해, 위 "source 순서가 우선순위를 만든다"는 규칙이 실전에서 어디까지 성립하는지 재현했다.

시나리오오버레이를 빌드할 때 언더레이가 source돼 있었나사용 시점 source 순서실행 결과
A예(이 장이 권하는 표준 절차)언더레이 → 오버레이(정순)OVERLAY
B예오버레이 → 언더레이(역순)OVERLAY(안 뒤집힌다)
C아니오언더레이 → 오버레이(정순)OVERLAY
D아니오오버레이 → 언더레이(역순)UNDERLAY(뒤집힌다)

B와 D의 대비가 핵심이다. "오버레이를 나중에 source해야 우선한다"는 문장은 D에서만 문자 그대로 성립한다. B(이 장이 실제로 가르치는 표준 빌드 절차)에서는 사용 시점에 순서를 거꾸로 해도 오버레이가 그대로 우선한다 — 원인은 두 메커니즘이 겹친 것이다.

  1. install/setup.bash는 빌드 시점 환경 전체를 굽는다(0.5.4가 이미 적은 사실 그대로). B 시나리오에서 오버레이의 setup.bash를 열어 보면 COLCON_CURRENT_PREFIX="/opt/ros/jazzy" → 언더레이 경로 → 자기 자신 순서로 세 프리픽스를 차례로 다시 source하는 코드가 그대로 기록돼 있다. 오버레이 하나만 source해도 언더레이 전체가, 그것도 자기보다 먼저 함께 로드된다.
  2. AMENT_PREFIX_PATH를 채우는 prepend 함수는 이미 들어 있는 값을 앞으로 옮기지 않는다(_colcon_prefix_bash_prepend_unique_value가 중복이면 그대로 건너뛴다). 그래서 오버레이를 source해 언더레이 경로가 이미 목록에 들어간 뒤 언더레이를 다시 source해도, 그 경로는 있던 자리에 남고 앞으로 옮겨지지 않는다.

그래서 이 장 본문의 조언에는 실전에서 안전판이 하나 더 있다 — 표준 절차대로(언더레이를 source한 셸에서) 빌드했다면, 터미널에서 순서를 실수로 거꾸로 해도 오버레이는 여전히 우선한다.

대신 실제 위험은 D가 보여 주듯 다른 자리로 옮겨간다 — "언제 source했나"가 아니라 "빌드할 때 무엇이 source돼 있었나"다. 언더레이 없이 오버레이를 빌드하면(C·D), 오버레이의 setup.bash는 언더레이 체인을 전혀 기록하지 못한다. 이후 순서를 아무리 맞게 지켜도(C) 결과 자체는 같아 보이지만, 언더레이만 단독으로 source하는 습관이 없다면 언더레이 전용 패키지·도구가 전혀 보이지 않는 사고로 이어진다. 0.5.4가 "어떤 환경에서 빌드했는가가 나중의 실행 환경을 결정한다"고 적은 문장을, 이번 실측은 "그리고 빌드 이후에는 재source 순서로 되돌릴 수 없다"로 완성한다.

확인하지 못한 것 — --merge-install(단일 설치 트리) 구성에서도 같은 dedup 동작인지, 그리고 setup.bash 대신 local_setup.bash(체인을 굽지 않는 버전)만 쓸 때 위 안전판이 사라져 본문의 단순 규칙이 그대로 지배하는지는 실측하지 않았다(확인 필요).

0.5.4 setup.bash vs local_setup.bash

install/ 안에는 두 종류의 환경 스크립트가 있다. 차이를 모르면 오버레이 순서가 뒤집힌다.

스크립트하는 일언제 쓰나
setup.bash이 워크스페이스를 빌드할 때 source되어 있던 상위 환경(체인)을 먼저 복원한 뒤, 이 워크스페이스를 오버레이한다일반적인 경우 — 한 줄로 전체 체인이 복원된다
local_setup.bash이 워크스페이스만 오버레이한다. 상위 환경은 변경하지 않는다체인을 직접 통제하고 싶을 때(도구·스크립트 내부)

즉 ~/robot_ws/install/setup.bash를 source하면, 빌드 시점에 설치돼 있던 /opt/ros/jazzy와 다른 오버레이까지 자동으로 함께 복원된다. 이 체인 정보는 빌드 시점에 기록되므로, "어떤 환경에서 빌드했는가"가 나중의 실행 환경을 결정한다.

함정: 워크스페이스 A를 source한 셸에서 워크스페이스 B를 빌드하면, B의 setup.bash는 A를 체인으로 기억한다. 나중에 A를 지우거나 옮기면 B의 환경 복원이 실패하거나 조용히 잘못된 경로를 가리킨다. 빌드는 되도록 "클린 셸 + 필요한 언더레이만" 상태에서 하라.

0.5.5 패키지는 어떻게 찾아지나 — ament index

ros2 run my_pkg my_node가 실행 파일을 찾는 과정, 그리고 find_package(my_pkg REQUIRED)가 CMake 설정을 찾는 과정은 서로 다른 메커니즘을 쓴다.

패키지 my_pkg 를 찾아라 AMENT_PREFIX_PATH 를 앞에서부터 순회 각 접두 경로에서 share/ament_index/resource_index/ packages/my_pkg 존재 확인 찾으면 그 접두 경로가 이 패키지의 prefix 실행 파일: lib/my_pkg/ 리소스: share/my_pkg/
  • 런타임 탐색(ros2 run·ros2 launch): AMENT_PREFIX_PATH를 앞에서부터 순회해 share/ament_index/resource_index/packages/<패키지명> 마커 파일을 찾는다. 처음 찾은 곳이 그 패키지의 접두 경로가 된다.
  • 빌드 탐색(find_package): CMake가 CMAKE_PREFIX_PATH를 순회해 share/<패키지>/cmake/<패키지>Config.cmake를 찾는다.

진단 명령 — 이 한 줄이 오염 문제의 절반을 푼다:

ros2 pkg prefix my_pkg      # 이 패키지가 어느 접두 경로에서 오는가
ros2 pkg list | grep my_pkg # 그래프에 보이는가
echo "$AMENT_PREFIX_PATH"   # 순서 확인

ros2 pkg prefix가 예상과 다른 경로(예: /opt/ros/jazzy 또는 옛 워크스페이스)를 가리키면, 오버레이 순서가 뒤집혔거나 옛 설치가 남아 있는 것이다.

0.5.6 --symlink-install의 정확한 동작과 한계

colcon build --symlink-install

이 옵션은 가능한 파일을 복사 대신 심볼릭 링크로 설치한다. 목적은 src/를 고치면 재빌드 없이 반영되게 하는 것이다.

대상링크되나결과
Python 소스(ament_python)대체로 예수정 후 재빌드 없이 반영
launch 파일·YAML·RViz 설정 등 share/에 설치되는 데이터대체로 예 (확인 필요 — 설치 규칙에 따라 다름)수정 즉시 반영
C++ 소스아니오(당연히)재컴파일 필요
새로 추가한 파일아니오설치 규칙이 다시 실행돼야 하므로 재빌드 필요
setup.py의 엔트리포인트·data_files 변경아니오재빌드 필요
package.xml 의존성 변경아니오재빌드 필요

함정 셋:

  1. 파일 추가와 파일 수정은 다르다. 기존 launch 파일을 고치면 즉시 반영되지만, 새 launch 파일을 만들면 재빌드해야 보인다. 이 비대칭이 사람을 자주 오판하게 한다.
  2. 배포에 쓰면 안 된다. 심볼릭 링크는 원본 src/를 가리키므로, install/만 복사해 로봇에 배포하면 깨진 링크가 된다. 배포용 빌드는 --symlink-install 없이 한다(14.8).
  3. 혼용 시 잔재가 남는다. 링크로 설치했다가 일반 빌드로 바꾸면(또는 반대로) install/에 옛 링크·옛 파일이 섞일 수 있다. 방식을 바꿀 때는 install/·build/를 지우고 다시 빌드하는 편이 안전하다.

0.5.7 재빌드·재source가 필요한 시점

무엇을 바꿨나재빌드재source비고
C++ 소스필요불필요--packages-select로 해당 패키지만
Python 소스--symlink-install이면 불필요불필요아니면 재빌드
launch·YAML 등 데이터 파일 수정--symlink-install이면 불필요불필요—
파일 추가(어떤 종류든)필요불필요설치 규칙 재실행 필요
새 패키지 추가필요필요새 실행 파일 경로가 PATH에 들어가야
package.xml·CMakeLists.txt 수정필요경우에 따라의존성·설치 규칙 변경 시
메시지·서비스 정의(.msg·.srv·.action)필요대개 필요의존하는 패키지도 함께 재빌드(1.2)
패키지 삭제—셸을 새로 열 것install/에서도 지워야 한다(아래)
환경변수(ROS_DOMAIN_ID 등)불필요—단, 이미 기동된 프로세스에는 미적용(0.4)

0.5.8 오염 증상 다섯 가지와 복구

증상 ① 고쳤는데 옛 동작이 나온다

  • 원인 후보: 재빌드 안 함 / 다른 오버레이의 같은 패키지가 앞에 있음 / install/에 옛 사본이 남음.
  • 진단: ros2 pkg prefix <패키지> 로 실제 출처 확인 → 예상 경로인가.
  • 복구: 예상과 다르면 AMENT_PREFIX_PATH 순서를 고치거나, 앞선 경로의 옛 설치를 제거.

증상 ② 삭제한 패키지가 계속 보인다

  • 원인: src/에서만 지웠고 install/·build/에 남아 있다. colcon은 사라진 패키지를 자동으로 정리하지 않는다.
  • 복구: rm -rf install build log 후 전체 재빌드. (부분 정리는 실수하기 쉽다 — 시간이 아깝지 않다면 전체 재빌드가 안전하다.)

증상 ③ 새 셸에서 명령을 못 찾는다

  • 원인: source 안 함, 또는 setup.bash 대신 다른 워크스페이스를 source.
  • 진단: echo $AMENT_PREFIX_PATH, which ros2.

증상 ④ 링크·심볼 오류, ABI 불일치

  • 원인: 한 패키지는 옛 헤더로, 다른 패키지는 새 헤더로 빌드됨(메시지 정의 변경 후 부분 빌드가 대표적).
  • 복구: 의존 그래프 상위까지 재빌드 — colcon build --packages-above <바뀐패키지>.

증상 ⑤ 두 배포판이 섞였다

  • 원인: 한 셸에서 humble과 jazzy를 모두 source.
  • 결과: 예측 불가능한 링크 오류·런타임 크래시.
  • 복구: 셸을 닫고 새로 연다. 되돌리려 하지 말 것. 규칙: 한 셸에는 한 배포판.

0.5.9 colcon 실전 옵션 — 빌드를 통제하는 최소 세트

전체는 4.5에서 다루고, 여기서는 워크스페이스 운용에 직결되는 것만 정리한다.

# 패키지 하나만
colcon build --packages-select my_pkg

# 그 패키지와 그것에 의존하는 것들까지 (메시지 변경 시 필수)
colcon build --packages-above my_msgs

# 그 패키지가 의존하는 것들까지 (새 체크아웃 후 부분 빌드)
colcon build --packages-up-to my_app

# 릴리스 최적화 — 지정하지 않으면 최적화 없이 빌드될 수 있다
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release

# 개발 편의
colcon build --symlink-install

함정: CMAKE_BUILD_TYPE을 지정하지 않으면 최적화 플래그가 붙지 않을 수 있다. "우리 알고리즘이 ROS 2에서 10배 느리다"는 보고의 흔한 원인이다. 성능 측정 전에 반드시 빌드 타입을 확인하라(11.7·12.6). 디버깅 중이라면 RelWithDebInfo가 균형점이다.

src/ 아래 특정 디렉터리를 빌드에서 제외하려면 그 안에 빈 COLCON_IGNORE 파일을 둔다 — 남의 저장소 전체를 받았는데 일부만 필요할 때 유용하다.

0.5.10 워크스페이스 분리 전략

패키지가 늘어나면 하나의 워크스페이스에 전부 넣는 방식이 무너진다 — 전체 재빌드가 길어지고, 서드파티 코드의 빌드 실패가 우리 코드 개발을 막는다.

① 단일 워크스페이스

  • 구성: 모든 소스를 ~/ros2_ws/src에.
  • 맞는 경우: 패키지 수십 개 이하, 팀 소규모, 서드파티 소스 빌드가 거의 없음.
  • 대가: 전체 빌드 시간이 모두에게 부과된다.

② 2층 분리 — 서드파티 언더레이 + 자사 오버레이 (가장 흔한 선택)

  • 구성: ~/deps_ws(소스 빌드가 필요한 외부 패키지, 자주 안 바뀜) → ~/robot_ws(우리 코드).
  • 이득: 외부 의존성은 한 번 빌드하고 잊는다. 일상 빌드의 부담이 적다.
  • 대가: 층이 늘어난 만큼 source 순서·체인 관리가 필요하다.

③ 3층 이상 — 플랫폼·제품·실험 분리

  • 구성: 플랫폼 공통 라이브러리 → 제품별 코드 → 실험용 브랜치.
  • 맞는 경우: 여러 로봇 기종이 공통 플랫폼을 공유하는 조직.
  • 대가: 체인이 깊어질수록 오염 사고가 는다. 층마다 "어느 환경에서 빌드했는가"를 문서화·자동화해야 한다.

견해: ②가 대부분의 팀에 맞는 기본값이다. ③은 실제로 여러 제품 라인을 운영할 때만 값을 한다 — 그 전에는 복잡도만 늘어난다. 어느 쪽이든 빌드 환경을 스크립트·컨테이너로 고정**하는 것이 핵심이다(0.4.9·4.10).

0.5.12 함정 카탈로그

#함정증상대처
1워크스페이스 A를 source한 셸에서 B를 빌드B가 A를 체인으로 기억새 셸에서 빌드(0.5.4)
2경로만 만들고 ament 마커 없음Package not found — 경로는 있는데 못 찾는다share/ament_index/resource_index/packages/<패키지> 확인(0.5.5)
3고쳤는데 안 바뀐다다른 워크스페이스의 것이 쓰임ros2 pkg prefix <패키지> 로 실제 경로 확인(11.1.14.4)
4--symlink-install 을 모든 문제의 해결책으로 기대헤더·C++ 변경은 여전히 재빌드적용 범위 확인(0.5.6)
5CMAKE_BUILD_TYPE 미지정최적화 없이 느림--cmake-args -DCMAKE_BUILD_TYPE=Release(0.5.9)
6빌드만 하고 재 source 안 함새 실행 파일·메시지가 안 보임0.5.7의 판단표
7.bashrc 에 여러 워크스페이스 자동 source순서가 굳어져 오염 진단이 어려움명시적 source(0.4.6)
8빌드 중간 실패 후 그대로 사용일부 패키지만 설치된 상태install/ 정리 후 재빌드(0.5.8)
9언더레이 없이 오버레이를 빌드오버레이의 setup.bash가 언더레이 체인을 못 굽는다 — 이후 순서를 맞게 source해도 언더레이 전용 패키지가 안 보임빌드 전 언더레이부터 source(0.5.3.1)

2·3번이 한 쌍이다. 둘 다 "분명히 고쳤는데 반영이 안 된다"로 나타나는데, 원인이 정반대다 — 2번은 설치가 덜 된 것, 3번은 다른 것이 설치되어 있는 것이다. ros2 pkg prefix가 경로를 주면 3번, Package not found가 나오면 2번이다.

0.5.11 체크포인트

  • src·build·install·log 각각의 역할과, 지워도 되는 것을 구분할 수 있는가. 실행되는 코드는 어느 디렉터리에 있는가.
  • 오버레이 우선순위의 실체가 무엇인지(경로 변수의 순서) 설명하고, source 순서가 왜 중요한지 말할 수 있는가.
  • setup.bash와 local_setup.bash의 차이를 설명하고, 워크스페이스 A를 source한 채 B를 빌드하면 어떤 일이 생기는지 말할 수 있는가.
  • ros2 pkg prefix <패키지>가 진단에 유용한 이유를 설명할 수 있는가.
  • --symlink-install에서 파일 수정과 파일 추가의 차이를 설명할 수 있는가. 배포용 빌드에 이 옵션을 쓰면 안 되는 이유는?
  • 메시지 정의를 바꾼 뒤 어떤 범위를 재빌드해야 하는가. 왜 --packages-select만으로는 부족한가.
  • "삭제한 패키지가 계속 보인다"의 원인과 확실한 복구 절차를 말할 수 있는가.
  • 성능 측정 전에 반드시 확인해야 할 빌드 설정은 무엇인가.

다음 장 0.6은 이 워크스페이스 안에 첫 패키지를 만든다 — C++과 Python 패키지의 구조를 나란히 놓고, 파일 하나하나가 무엇을 하는지 해부한다.