제0부 개관과 출발
0.4 설치와 환경 — apt·소스·Docker·환경변수
0.4.1 이 장에서 배우는 것 / 선수 지식
ROS 2에서 사람들이 잃는 시간의 상당 부분은 알고리즘이 아니라 환경에서 나온다. "어제까지 되던 것이 오늘 안 된다", "동료 컴퓨터에서는 되는데 내 것에서는 안 된다", "노드는 기동되는데 서로 안 보인다" — 이 셋의 원인 대부분이 설치 방식과 환경변수다.
이 장은 설치를 레시피가 아니라 구조로 다룬다. 어떤 경로로 설치하면 무엇이 어디에 놓이고, 셸의 어떤 변수가 무엇을 결정하며, 그 변수들이 어떻게 서로를 오염시키는지.
배우는 것:
- 세 가지 설치 경로(apt 바이너리·소스 빌드·컨테이너)의 트레이드오프와 "누구에게 맞나".
- 패키지 변형(
ros_core·ros_base·desktop·desktop_full등, REP 2001)의 차이와 로봇 탑재 시 선택. - 환경변수 전수 — 통신(도메인·디스커버리·RMW), 빌드·탐색 경로, 로깅, 벤더 설정, 보안. 각각이 어느 계층에서 읽히는지.
ROS_DOMAIN_ID의 실체 — 도메인이 실제로는 UDP 포트 계산이라는 사실과, 거기서 나오는 상한·충돌 규칙.- 컨테이너에서의 ROS 2 — 네트워크 모드·공유 메모리·GUI가 만드는 특유의 함정.
- 환경 위생 규칙 — 팀 단위로 재현 가능한 환경을 만드는 방법.
선수 지식: 0.3(Jazzy의 플랫폼·툴체인). 리눅스 셸·apt·Docker의 기본 사용법.
정직: 설치 절차는 ROS 2 문서가 배포판 수명 중에도 바꾸는 영역이다. 실제로 apt 저장소 등록 방식은 Jazzy 출시 이후 한 차례 변경된 이력이 있다(키·소스 목록을 직접 내려받는 방식 → 저장소 등록용
.deb패키지 설치 방식)(확인 필요 — 현재 공식 문서의 절차를 반드시 확인할 것). 이 장은 절차의 뼈대와 각 단계가 무엇을 하는지를 설명하는 데 무게를 두며, 명령 문자열 자체는 공식 문서를 정본으로 삼기 바란다. 아래의 apt 저장소 등록·설치 명령은 실행 검증되지 않았다 — 이후 회차의 검증은 이미 ROS 2가 설치된 컨테이너 이미지(ros:jazzy-*)에서 이루어졌으므로, 설치 절차 자체는 검증 대상이 아니었다(목차 3). 예외 —ROS_AUTOMATIC_DISCOVERY_RANGE의 기본값·오타 시 동작은 93차에 실행 확인했다(0.4.12).
0.4.2 설치 경로 세 가지 — 무엇을 고를 것인가
① apt 바이너리 설치 (/opt/ros/jazzy)
- 동작: 빌드팜(4.9)에서 만든 데비안 패키지를 설치한다. 시스템 경로에 놓이며
apt upgrade로 갱신된다. - 맞는 경우: Ubuntu 24.04(또는 지원 플랫폼)를 쓰는 대부분의 개발·배포. 특히 로봇에 탑재하는 런타임.
- 한계: 패치를 넣을 수 없다(코어를 고치려면 해당 패키지만 소스로 오버레이해야 한다, 0.5). 지원되지 않는 OS에서는 불가. 패키지가 갱신되면 의도치 않게 동작이 바뀔 수 있다 — 필드 로봇은 버전 고정 전략이 필요하다(14.8).
② 소스 빌드
- 동작:
vcstool로 수십 개 저장소를 받아colcon으로 전부 빌드한다(4.7). - 맞는 경우: 지원되지 않는 플랫폼·아키텍처, 코어에 패치가 필요할 때, 코어 개발·디버깅(심볼과 소스가 손에 있어야 할 때), 커스텀 RMW·특수 컴파일 옵션.
- 한계: 첫 빌드가 길고(수십 분~수 시간, 하드웨어 의존), 디스크·메모리를 많이 쓰며, 재현성을 직접 관리해야 한다(어떤 커밋을 받았는지
.repos파일로 고정해야 한다).
③ 컨테이너(Docker 등)
- 동작:
ros:jazzy계열 공식 이미지 또는 자체 이미지를 쓴다. - 맞는 경우: CI(4.10), 여러 배포판 병행, 팀 간 환경 통일, 호스트 OS가 24.04가 아닐 때, 배포·롤백이 중요한 운영(14.8).
- 한계: 네트워크·공유 메모리·장치 접근에서 ROS 2 특유의 함정이 생긴다(0.4.7). GUI 도구(rviz2)는 추가 설정이 필요하다.
세 경로는 배타적이지 않다. 흔한 실무 구성은 "컨테이너 안에 apt로 설치하고, 우리 패키지만 소스로 오버레이"다.
0.4.3 apt 설치의 구조 — 각 단계가 무엇을 하나
절차를 외우기보다 각 단계의 목적을 이해하면 실패했을 때 진단할 수 있다.
| 단계 | 왜 하나 | 빠뜨리면 |
|---|---|---|
| ① UTF-8 로케일 | ROS 2 도구가 UTF-8을 전제 | 인코딩 관련 오류·깨진 출력 |
| ② universe | ROS 2 의존 패키지 일부가 universe에 있다 | 의존성 해결 실패 |
| ③ 저장소·키 | apt가 ROS 2 패키지를 신뢰·발견하게 | ros-jazzy-* 패키지를 못 찾음 |
| ⑤ 변형 선택 | 필요한 만큼만 설치 | 과다 설치(디스크·부팅 시간)·과소 설치(패키지 없음) |
⑥ ros-dev-tools | colcon·rosdep·vcstool 등 빌드 도구 | 빌드 자체가 불가 |
| ⑦ source | 환경변수 주입 | ros2 명령을 못 찾음 |
패키지 변형(REP 2001): 무엇을 설치할지는 로봇의 역할에 따라 다르다.
| 변형 | 포함 | 쓰임 |
|---|---|---|
ros-jazzy-ros-core | 통신 코어·기본 인터페이스·CLI | 최소 런타임 |
ros-jazzy-ros-base | ros_core + tf2·URDF·라이프사이클 등 | 로봇 탑재 런타임의 표준 선택 |
ros-jazzy-desktop | ros_base + rviz2·데모·튜토리얼 | 개발용 워크스테이션 |
ros-jazzy-desktop-full | desktop + 인지·시뮬 관련 확장 (확인 필요 — Jazzy에서의 정확한 구성) | 풀 개발 환경 |
ros-dev-tools | colcon·rosdep·vcstool 등 (ROS 패키지 아님) | 빌드하는 모든 곳 |
견해:** 로봇에 탑재하는 이미지는
ros-base+ 실제로 쓰는 패키지만 명시적으로 넣는 편이 낫다.desktop을 탑재하면 rviz2·Qt·OpenGL 의존성이 함께 설치돼 이미지가 커지고, 부팅·보안 관리 표면이 넓어진다.
0.4.4 환경변수 전수 — 무엇이 어느 계층에서 읽히나
ROS 2의 동작을 바꾸는 환경변수는 읽히는 계층이 다르다. 이 구분을 알면 "어떤 변수를 언제 바꿔야 하는지"가 정리된다.
통신·디스커버리 (rmw·DDS 계층에서 읽힘):
| 변수 | 의미 | 함정 |
|---|---|---|
ROS_DOMAIN_ID | 논리적 통신 도메인(기본 0) | 같은 네트워크의 다른 사람과 충돌하는 주요 원인. 상한이 있다(0.4.5) |
RMW_IMPLEMENTATION | 사용할 rmw 구현 | 모든 노드가 같아야 한다. 섞이면 서로 안 보인다(3.5) |
ROS_AUTOMATIC_DISCOVERY_RANGE | 디스커버리 범위(OFF·LOCALHOST·SUBNET·SYSTEM_DEFAULT) | Iron에서 도입. 기본값은 SUBNET(Jazzy, 실행 확인 — 0.4.12) |
ROS_STATIC_PEERS | 정적으로 지정한 피어 목록 | 멀티캐스트가 막힌 망에서 유용(3.9) |
ROS_LOCALHOST_ONLY | 로컬호스트로 제한 | 비추천(deprecated) — 위 두 변수로 대체됨 (확인 필요 — Jazzy에서의 잔존 동작) |
FASTRTPS_DEFAULT_PROFILES_FILE | Fast DDS XML 프로파일 경로 | 벤더 전용. 경로가 틀려도 조용히 무시될 수 있다(3.7) |
CYCLONEDDS_URI | Cyclone DDS 설정(파일 또는 인라인 XML) | 벤더 전용(3.8) |
탐색·빌드 경로 (ament·CMake·Python이 읽음):
| 변수 | 의미 |
|---|---|
AMENT_PREFIX_PATH | 설치된 ROS 패키지 검색 경로. 오버레이 우선순위의 실체(0.5) |
CMAKE_PREFIX_PATH | CMake의 find_package 탐색 경로 |
LD_LIBRARY_PATH | 공유 라이브러리 탐색 |
PYTHONPATH | Python 모듈 탐색 |
ROS_DISTRO | 현재 배포판 이름. 도구·스크립트가 참조 |
ROS_PACKAGE_PATH | ROS 1 잔재. ROS 2에서는 대부분 무의미 |
COLCON_PREFIX_PATH | colcon이 기록하는 설치 접두 경로 |
로깅 (rcutils·rcl 계층):
| 변수 | 의미 |
|---|---|
RCUTILS_CONSOLE_OUTPUT_FORMAT | 콘솔 로그 포맷 토큰({severity}·{name}·{time}·Jazzy 신규 {date_time_with_ms} 등) |
RCUTILS_COLORIZED_OUTPUT | 색 출력 강제·해제 |
RCUTILS_LOGGING_BUFFERED_STREAM | 로그 버퍼링 |
ROS_LOG_DIR·ROS_HOME | 로그·설정 파일 위치 |
보안 (SROS2, 13.2): ROS_SECURITY_ENABLE·ROS_SECURITY_STRATEGY·ROS_SECURITY_KEYSTORE·ROS_SECURITY_ENCLAVE_OVERRIDE.
함정: 환경변수는 프로세스 시작 시점에 읽힌다. 셸에서
export ROS_DOMAIN_ID=7을 해도 이미 실행 중인 노드에는 적용되지 않는다. launch로 실행한 노드는 launch 프로세스의 환경을 물려받으므로, launch 파일 안에서 환경을 설정하는 편이 안전하다(14.3).
0.4.5 ROS_DOMAIN_ID의 실체 — 도메인은 사실 포트 번호다
ROS_DOMAIN_ID는 "논리적 그룹"처럼 소개되지만, DDS(RTPS) 수준에서는 UDP 포트 번호 계산의 입력이다. RTPS 표준의 포트 산식은 다음과 같다.
$$ \text{port} = PB + DG \cdot d + \text{offset} $$
기본값은 $PB=7400$(port base), $DG=250$(domain gain), 참가자 번호 게인 $PG=2$ 이며, offset은 용도별 상수($d_0=0$: 멀티캐스트 디스커버리, $d_1=10$: 유니캐스트 디스커버리, $d_2=1$·$d_3=11$: 사용자 트래픽). 예컨대 도메인 $d=0$ 의 멀티캐스트 디스커버리 포트는 7400, $d=1$ 은 7650이다.
여기서 실무적으로 세 가지가 따라 나온다.
- 도메인 ID에는 상한이 있다. 도메인 번호가 커지면 계산된 포트가 OS의 임시 포트 범위(ephemeral port range)와 충돌한다. 리눅스 기본 설정에서 0~101 범위는 안전하며, 그보다 큰 값은 OS 설정에 따라 충돌할 수 있다 (확인 필요 —
/proc/sys/net/ipv4/ip_local_port_range값에 의존). - 도메인이 다르면 물리적으로 다른 포트를 쓴다. 그래서 도메인 분리는 확실한 격리 수단이다 — 다만 보안 수단은 아니다. 상대가 도메인 번호를 알면 그만이다(13.1).
- 같은 도메인 + 같은 서브넷 = 자동 연결. 연구실·사무실에서 동료와 같은 기본값(0)을 쓰면 서로의 노드가 섞인다. 개발자·로봇마다 도메인 번호를 배정하는 것이 팀 위생의 기본이다(14.9).
0.4.6 셸 환경의 함정 — .bashrc 자동 source
거의 모든 튜토리얼이 이렇게 안내한다.
echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc
편리하지만, 규모가 커지면 문제의 원천이 된다.
- 여러 배포판·워크스페이스를 오갈 수 없다. 새 터미널마다 특정 환경이 강제된다.
- 오버레이 오염을 눈치채기 어렵다.
.bashrc에 워크스페이스 setup까지 넣으면, 그 워크스페이스를 지운 뒤에도 경로가 남아 비정상적인 동작을 한다(0.5). - CI·systemd·컨테이너에서는
.bashrc가 실행되지 않는다. 로컬에서만 되는 환경이 만들어진다 — 배포 시점에 드러난다(14.8).
대안(견해): 자동 source 대신 명시적 진입 스크립트를 쓴다.
# ~/bin/rosjazzy — 필요할 때만 진입
#!/usr/bin/env bash
set -euo pipefail
source /opt/ros/jazzy/setup.bash
export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-42}" # 팀에서 배정받은 번호
export RMW_IMPLEMENTATION="${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp}"
exec "${SHELL:-/bin/bash}"
프로젝트 워크스페이스라면 여기에 source install/setup.bash를 덧붙인다. 이렇게 하면 환경이 파일로 명시되므로 팀·CI·로봇이 같은 정의를 공유할 수 있고, 문제가 생겼을 때 "무엇이 source됐나"를 코드로 답할 수 있다.
0.4.7 컨테이너에서의 ROS 2 — 특유의 함정
컨테이너는 환경 재현에 강력하지만, ROS 2의 통신 모델과 부딪히는 지점이 있다.
| 항목 | 문제 | 대처 |
|---|---|---|
| 네트워크 모드 | 같은 Docker 네트워크 안의 컨테이너끼리는 기본 bridge로도 동작한다(3.9.12 실측). 막히는 것은 다른 네트워크·다른 호스트이고, 호스트↔컨테이너는 인터페이스 지정 문제다 | 컨테이너를 한 네트워크에 모으면 그대로 된다. 호스트를 넘어야 할 때 --network=host, 운영에서는 Discovery Server(3.4)나 정적 피어 |
| 공유 메모리 | DDS의 공유 메모리 전송은 /dev/shm을 쓴다. 컨테이너 기본 /dev/shm(64 MB)이 부족하면 성능이 떨어지거나 실패 | --shm-size 상향, 필요 시 --ipc=host |
| PID·프로세스 격리 | 같은 호스트의 다른 컨테이너와 공유 메모리 전송이 안 될 수 있다 | --ipc=host 또는 네트워크 전송으로 강제 |
| 장치 접근 | 카메라·시리얼·CAN·GPU | --device, /dev 마운트, GPU 런타임 |
| GUI(rviz2) | X11·Wayland 접근 | DISPLAY 전달 + X 소켓 마운트, 또는 원격 시각화 |
| 시간 | 컨테이너는 호스트 시계를 공유하지만, 여러 호스트 간에는 동기화 필요 | NTP·PTP(7.7) |
# 개발용 최소 실행 예 (확인 필요 — 환경에 따라 조정)
docker run -it --rm \
--network=host --ipc=host \
--shm-size=1g \
-e ROS_DOMAIN_ID=42 \
-e RMW_IMPLEMENTATION=rmw_fastrtps_cpp \
ros:jazzy-ros-base
함정:
--network=host는 컨테이너 격리의 상당 부분을 포기하는 선택이다. 개발에서는 합리적이지만, 운영에서 그대로 쓰면 도메인 누출·포트 충돌·보안 표면 확대를 감수하게 된다. 운영 배포 구조는 14.8에서 따로 다룬다.
0.4.8 설치 검증 — 무엇을 확인하면 "된 것"인가
설치 직후에 확인해야 할 것은 "명령이 실행되는가"가 아니라 "두 프로세스가 실제로 통신하는가" 다.
# 터미널 A
source /opt/ros/jazzy/setup.bash
ros2 run demo_nodes_cpp talker
# 터미널 B (같은 ROS_DOMAIN_ID여야 한다)
source /opt/ros/jazzy/setup.bash
ros2 run demo_nodes_py listener
talker가 발행하고 listener가 받으면 C++·Python 양쪽 클라이언트 라이브러리, 디스커버리, 직렬화, 전송이 모두 정상이라는 뜻이다. 여기서 막히면 다음 순서로 좁힌다.
ros2 node list에 상대가 보이면 디스커버리는 되는 것이고, 그다음 의심 대상은 QoS(1.5)다. 보이지 않으면 네트워크·도메인 문제다(3.9). 이 두 갈래를 구분하는 것이 ROS 2 디버깅의 첫 분기점이며, 11.1의 플레이북에서 다시 쓴다.
0.4.9 환경 위생 — 팀 단위 규칙
경험적으로 사고를 크게 줄이는 규칙들이다(견해 — 근거는 위 절들의 함정 목록).
- 도메인 번호를 배정하라. 개발자·로봇·테스트 벤치마다 고유 번호. 문서화하고 환경 스크립트에 명시해 둔다.
- RMW를 고정하라. 팀 전체가 같은
RMW_IMPLEMENTATION을 쓴다. 벤더를 바꾸는 것은 실험이지 기본값이 아니다. - 환경을 파일로 만들어라.
.bashrc가 아니라 진입 스크립트·.env·컨테이너 이미지로. "내 컴퓨터에서만 되는 상태"를 만들지 않는다. - 로봇에는 개발 도구를 설치하지 마라.
ros-base+ 필요한 것만. 빌드는 별도 환경에서(14.8). - 버전을 고정하라. 필드 로봇에서
apt upgrade가 자동으로 실행되면, 어느 날 조용히 동작이 바뀐다. 패키지 고정·이미지 태그·롤백 경로를 준비한다. - 환경 점검을 부팅 절차에 넣어라. 도메인·RMW·시계·네트워크 인터페이스를 시작 시 로그로 남기면, 현장 장애 분석이 몇 시간 빨라진다(11.1).
0.4.11 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | .bashrc 에서 무조건 source | 워크스페이스 오염·진단 곤란 | 0.4.6의 규칙 |
| 2 | ROS_DOMAIN_ID 를 격리 수단으로 신뢰 | 값이 크면 포트가 겹칠 수 있다 | 범위 확인(0.4.5·1.1.7) |
| 3 | ROS_DOMAIN_ID 를 한쪽 셸에서만 설정 | 노드가 서로 안 보임 | echo $ROS_DOMAIN_ID 를 양쪽에서(11.1.14.2) |
| 4 | "컨테이너라서 디스커버리가 안 된다"고 단정 | --network host 부터 켜게 됨 | 같은 Docker 네트워크면 별도 설정 없이 된다(3.9.12) |
| 5 | 호스트에서 컨테이너 노드가 안 보임 | 차단이 아니라 가입 인터페이스 문제 | 3.9.12.3 |
| 6 | apt 와 소스 빌드를 같은 경로에 섞음 | 어느 것이 쓰이는지 불명 | 워크스페이스 분리(0.5.10) |
| 7 | 설치 후 검증 생략 | 나중에 잘못된 층을 의심 | 0.4.8의 확인 절차 |
| 8 | RMW_IMPLEMENTATION 을 일부 노드에만 설정 | 조용히 통신 안 됨 | 전 노드 통일(3.5.5) |
| 9 | ROS_AUTOMATIC_DISCOVERY_RANGE에 오타 | 오류 없이 LOCALHOST로 격하 — 다른 로봇과 안 보임 | 값 재확인, 경고 로그 확인(0.4.12) |
3·4·5번이 "안 보인다"의 대부분이다. 순서를 정해 두면 빠르다 — ① 도메인 확인 → ② 같은 컨테이너·같은 네트워크인지 → ③
ros2 doctor hello의 두 표(11.1.15).
0.4.10 체크포인트
- 설치 세 경로(apt·소스·컨테이너)의 트레이드오프를 말하고, 지금 프로젝트에 맞는 조합을 근거와 함께 정할 수 있는가.
ros-base와desktop의 차이를 설명하고, 로봇 탑재 이미지에 무엇을 넣을지 결정할 수 있는가.ROS_DOMAIN_ID가 내부적으로 무엇을 결정하는지(포트 산식) 설명하고, 안전한 값 범위와 그 이유를 말할 수 있는가.- 통신 관련 환경변수 넷(
ROS_DOMAIN_ID·RMW_IMPLEMENTATION·ROS_AUTOMATIC_DISCOVERY_RANGE·ROS_STATIC_PEERS)의 역할을 구분할 수 있는가. .bashrc자동 source가 CI·systemd·컨테이너에서 왜 문제를 일으키는지 설명할 수 있는가.- 컨테이너에서 노드가 서로 안 보일 때 확인할 항목 세 가지(네트워크 모드·도메인·멀티캐스트)를 순서대로 댈 수 있는가.
talker/listener가 통하지 않을 때, "디스커버리 문제"와 "QoS 문제"를 어떤 한 가지 관측으로 가르는가.ROS_AUTOMATIC_DISCOVERY_RANGE를 안 주면 기본값이 무엇이 되는가, 그리고 오타를 내면 무슨 일이 생기는가(0.4.12).
0.4.12 ROS_AUTOMATIC_DISCOVERY_RANGE의 기본값 — 실측
실행 확인(2026-08-18, ros:jazzy-ros-base, rcl 헤더·gcc 직접 컴파일) — rcl/discovery_options.h의 주석은 "환경변수를 안 주면 RCL_DEFAULT_DISCOVERY_RANGE를, 그 전처리 정의가 없으면 SUBNET을 쓴다"고 적어 두었다(주석은 동시에 "미래 버전에서는 기본값이 LOCALHOST로 바뀔 예정"이라고도 적는다). 문서화된 문장을 그대로 믿지 않고, rcl_get_automatic_discovery_range()를 직접 호출하는 프로그램을 빌드해 실행했다.
rmw_discovery_options_t opts = rmw_get_zero_initialized_discovery_options();
rcl_get_automatic_discovery_range(&opts);
printf("%s\n", rcl_automatic_discovery_range_to_string(opts.automatic_discovery_range));
| 조건 | 결과 |
|---|---|
ROS_AUTOMATIC_DISCOVERY_RANGE 미설정 | RMW_AUTOMATIC_DISCOVERY_RANGE_SUBNET — 주석 그대로, Jazzy는 아직 LOCALHOST로 전환하지 않았다 |
LOCALHOST로 설정 | RMW_AUTOMATIC_DISCOVERY_RANGE_LOCALHOST — 정상 반영 |
오타(BOGUS)로 설정 | [WARN] [rcl]: Invalid value 'BOGUS' … assuming localhost only → LOCALHOST로 조용히 격하(오류 아님, 경고 1줄) |
세 번째 행이 함정이다. 값 자체가 틀리면 OFF(디스커버리 정지)나 에러가 아니라 가장 좁은 범위인 LOCALHOST로 자동 격하된다. 오타 하나로 "내 컴퓨터 안에서는 되는데 다른 로봇과는 안 보인다"가 되고, 원인은 경고 로그 한 줄(대개 stdout 소음에 묻힌다)뿐이다.
확인하지 못한 것 — 이 기본값이 rmw 벤더(Cyclone DDS·zenoh)에 따라서도 동일한지는 확인하지 않았다(확인 필요). rcl 계층에서 결정되므로 벤더 무관일 가능성이 높지만, 실측하지는 않았다.
다음 장 0.5는 설치 위에 구성되는 워크스페이스와 오버레이를 다룬다 — AMENT_PREFIX_PATH가 실제로 어떻게 해석되며, 왜 "빌드했는데 옛 코드가 실행되는" 일이 생기는지.