제0부 개관과 출발
0.2 ROS 1에서 ROS 2로 — 설계 결정과 그 대가
0.2.1 이 장에서 배우는 것 / 선수 지식
ROS 2의 복잡함은 대부분 의도된 것이다. ROS 1에서 겪은 구체적 실패 — 마스터가 종료되면 전부 종료되고, 무선에서 데이터가 무너지고, 여러 로봇을 한 네트워크에 놓을 수 없고, 윈도우에서 실행되지 않고, 실시간 루프를 넣을 수 없었던 — 를 고치려는 결정들의 누적이 지금의 구조다. 그리고 모든 결정에는 대가가 있었다.
이 장은 그 결정 목록을 하나씩 열고, 무엇을 얻고 무엇을 잃었는지 정직하게 적는다. ROS 1을 모르는 독자에게는 "왜 이렇게 복잡한가"에 대한 답으로, ROS 1 경험자에게는 "왜 내 습관이 통하지 않는가"에 대한 답으로 읽힌다.
배우는 것:
- ROS 1의 구조 — 마스터·XMLRPC 협상·TCPROS 연결이 실제로 어떻게 동작했나(대조를 위한 최소 요약).
- 열 개의 핵심 설계 변경 — 마스터 제거, DDS 채택, QoS, 파라미터 소유권, 액션 1급화, 컴포넌트, 시간 추상화, 빌드 시스템, launch, 라이프사이클. 각각의 동기·이득·대가.
- 개념 대응표 — ROS 1 용어를 ROS 2 용어로 옮기는 표와, 옮겨지지 않는 것들.
- ROS 2에서 새로 생긴 실패 양상 — ROS 1에는 없던 종류의 버그(QoS 침묵 실패, 디스커버리 폭주, 오버레이 오염).
선수 지식: 0.1(3중 정체·계층 스택). ROS 1 경험은 없어도 된다.
정직: ROS 1은 2025년 5월 Noetic의 EOL로 공식 지원이 끝났다(마지막 배포판 Noetic Ninjemys, Ubuntu 20.04 기준). 이 장은 역사 서술이 아니라 설계 근거 서술이 목적이므로 ROS 1의 기능을 전수하지 않는다. ROS 1 시스템과의 실제 연동은 15.3(ros1_bridge)에서 다룬다. 설계 동기에 관한 서술은 ROS 2 설계 문서(design.ros2.org)와 공개 논의에 근거하지만, 일부 인과 해석은 저자의 견해다.
0.2.2 ROS 1은 어떻게 동작했나 — 최소 요약
ROS 1의 통신은 중앙 마스터(roscore)가 중개하되, 데이터는 직접 흐르는 구조였다.
핵심 성질 셋:
- 마스터는 이름 서버였다. 데이터는 마스터를 거치지 않지만, 연결을 맺으려면 마스터가 실행 중이어야 했다. 마스터가 종료되면 기존 연결은 유지되지만 새 연결은 불가능했고, 실질적으로 시스템 재시작이 필요했다.
- 연결은 TCP 스트림이었다. TCPROS는 신뢰성이 있었고 순서가 보장됐으며, 무엇보다 "연결되면 별도 설정 없이 동작했다" — 구독자는 발행자의 데이터를 확실히 받았다. 유실이 걱정되면 큐를 늘리면 됐다.
- 설정 표면이 작았다.
ROS_MASTER_URI,ROS_IP·ROS_HOSTNAME정도가 네트워크 설정의 전부였다. 이것이 ROS 1의 가장 큰 실용적 미덕이었다 — 그리고 ROS 2가 가장 크게 잃은 것이기도 하다.
0.2.3 결정 ① — 중앙 마스터 제거
동기: 단일 장애점(SPOF) 제거, 노드의 시작 순서 자유, 다중 로봇 시스템, 그리고 마스터 없는 실제 분산 시스템.
변경: ROS 2에는 마스터가 없다. 노드는 부팅과 동시에 미들웨어의 자동 디스커버리로 서로를 찾는다(DDS의 SPDP·SEDP, 3.3). 같은 ROS_DOMAIN_ID를 쓰고 서로 네트워크로 닿을 수 있으면, 아무 순서로 실행해도 결국 연결된다.
얻은 것:
- 노드 하나가 종료돼도 나머지는 계속 실행된다. 재시작한 노드는 자동으로 다시 연결된다.
- 시작 순서 의존이 사라진다(구독자를 먼저 실행해도 된다).
- 여러 로봇이 도메인 분리만으로 한 네트워크에 공존한다(14.9).
대가(정직하게):
- "진실의 원천"이 사라졌다. ROS 1에서는 마스터에 물으면 그래프의 전모를 알 수 있었다. ROS 2에서
ros2 node list가 보여주는 것은 "내 노드가 지금까지 발견한 것" 이며, 발견은 시간이 걸리고 네트워크 구성에 따라 비대칭적일 수 있다. 두 대의 컴퓨터에서ros2 topic list결과가 다른 상황이 정상적으로 발생한다. - 디스커버리가 비용이다. 참가자 수 $N$ 에 대해 디스커버리 트래픽·메모리는 대략 $O(N^2)$ 로 증가한다(엔티티마다 상대를 알아야 하므로). 노드가 수십 개를 넘어가면 이것이 실제 병목이 된다(3.3·3.4).
- 네트워크에 훨씬 민감해졌다. 멀티캐스트가 막힌 네트워크, 컨테이너 브리지, 무선 AP의 멀티캐스트 처리, VPN — ROS 1에서는 문제되지 않던 것들이 ROS 2에서는 "노드가 서로 안 보인다"로 나타난다(3.9).
- 의도치 않은 연결이 생긴다. 같은 서브넷·같은 도메인이면 옆 사람의 노드와 연결된다. 연구실에서 두 사람이 동시에 개발하다 서로의
/cmd_vel을 받는 사고는 ROS 2 특유의 것이다(0.4의 도메인 위생).
0.2.4 결정 ② — DDS·RTPS 채택
동기: 통신 계층을 직접 만들지 않고, 이미 검증된 산업 표준을 쓴다. DDS(OMG 표준)는 항공·방위·산업 자동화에서 실적이 있었고, QoS·보안·디스커버리·실시간을 이미 갖고 있었다.
변경: ROS 2의 기본 전송은 DDS이며, 그 위에 rmw 추상 계층을 둬서 벤더를 교체할 수 있게 했다(3.5). Jazzy의 기본은 rmw_fastrtps_cpp(eProsima Fast DDS)다.
얻은 것:
- 신뢰성·내구성·데드라인·라이브라인스 같은 QoS 어휘를 추가 비용 없이 얻었다(1.5).
- 보안(DDS-Security)의 표준 경로가 생겼다(13.3).
- 공유 메모리 전송·zero-copy 같은 고성능 경로가 벤더 구현으로 제공된다(2.7·3.7).
- ROS가 전송 계층을 유지보수할 필요가 없어졌다.
대가:
- 복잡도의 대부분이 여기서 온다. DDS는 큰 표준이고, 벤더마다 설정 방식(Fast DDS는 XML, Cyclone은 XML+환경변수)과 기본값이 다르다. "ROS 2 문제"의 상당수는 실제로는 "DDS 설정 문제"다.
- 추상화가 새는(leaky) 지점이 많다. 대용량 데이터, 멀티캐스트, MTU·단편화, 디스커버리 규모 — 모두 DDS 계층의 지식을 요구한다. 제3부 전체가 이 대가를 다룬다.
- 의존성이 커졌다. DDS 구현은 수십만 줄 규모의 C++ 라이브러리다.
- 벤더 간 동작 차이가 실재한다. "Cyclone에서는 되는데 Fast DDS에서는 안 된다"(또는 그 반대)는 흔한 보고다(3.6).
0.2.5 결정 ③ — QoS 도입
동기: ROS 1의 통신은 사실상 한 종류였다 — 신뢰성 있는 TCP 스트림. 그러나 로봇의 데이터는 성격이 극단적으로 다르다. 라이다 스캔은 최신값만 중요하고 한두 개 빠져도 되지만, 비상정지 명령은 반드시 도착해야 하고, 지도는 늦게 들어온 구독자도 받아야 한다.
변경: 발행자·구독자 각각이 QoS 프로파일(신뢰성·내구성·history·depth·deadline·liveliness·lifespan)을 선언하고, 호환될 때만 연결된다(1.5).
얻은 것:
- 무선·손실 네트워크에서
BEST_EFFORT로 지연 폭증을 피할 수 있다. TRANSIENT_LOCAL로 "늦게 온 구독자도 마지막 값을 받는" 래치(latched) 동작을 표준적으로 얻는다(지도·로봇 기술·정적 TF).- 데드라인·라이브라인스로 계약 위반을 이벤트로 감지할 수 있다.
대가 — 이것이 ROS 2 입문의 가장 큰 함정이다:
- 불일치는 조용히 실패한다. 발행자가
BEST_EFFORT, 구독자가RELIABLE이면 두 엔티티는 연결되지 않는다.ros2 topic list에는 토픽이 보이고,ros2 node info에도 둘 다 보이는데, 데이터만 안 온다. ROS 1에서는 존재하지 않던 실패 양상이다. - 진단 도구조차 QoS에 종속된다. CLI 도구도 결국 자기 QoS로 구독한다. 다만 Jazzy의
ros2 topic echo는 기본이best_available이라 발행자에 맞춰 자동으로 낮춰 매칭된다(실행 확인) — 그래서 이 증상은 CLI에서는 대체로 사라졌지만, 직접 작성한 구독 노드에는 그대로 남아 있다. 오히려 "CLI로는 보이는데 내 노드로는 안 들어온다"는 형태로 나타난다(0.7·1.5.3.8·1.6). - 설계 부담이 응용으로 넘어왔다. 어떤 토픽에 어떤 QoS를 줄지는 이제 아키텍처 결정이며, 잘못 정하면 성능·신뢰성이 무너진다(1.6).
0.2.6 결정 ④ — 파라미터 서버 제거, 노드가 파라미터를 소유
동기: ROS 1의 중앙 파라미터 서버는 타입이 느슨했고, 언제 읽히는지 불분명했으며(노드가 시작 시 한 번 읽고 마는 경우가 많았다), 변경 알림이 없었다.
변경: 파라미터는 노드에 속한다. 노드가 declare_parameter로 선언하고, 타입·범위 제약을 붙이며, 외부에서는 서비스(/노드/set_parameters)로 읽고 쓴다. 변경은 콜백으로 통지된다(1.9).
얻은 것: 타입 안전, 선언 시점 명확, 변경 콜백, 노드별 격리, 기술자(descriptor)로 범위·읽기전용 지정.
대가:
- 전역 설정 공유 패턴이 사라졌다. ROS 1에서 여러 노드가
/robot_description이나 공통 튜닝값을 파라미터 서버에서 함께 읽던 패턴은 ROS 2에서 성립하지 않는다. 대신 launch에서 각 노드에 주입하거나(14.3), 토픽으로 공유한다(robot_description은TRANSIENT_LOCAL토픽으로 배포하는 방식이 표준이 됐다, 5.5). - 노드가 종료되면 파라미터도 사라진다. 런타임에 바꾼 값은 재시작하면 초기값으로 돌아간다. 지속성이 필요하면 파일로 덤프·복원해야 한다(
ros2 param dump). - 선언하지 않은 파라미터는 기본적으로 설정할 수 없다(
allow_undeclared_parameters를 켜지 않는 한). ROS 1의 자유로운 설정 습관이 여기서 막힌다.
0.2.7 결정 ⑤~⑦ — 액션·컴포넌트·시간
⑤ 액션의 1급화. ROS 1의 actionlib은 여러 토픽(goal·cancel·status·feedback·result)을 규약으로 묶은 라이브러리였고, 그래서 도구가 액션을 이해하지 못했다. ROS 2는 액션을 rcl_action으로 코어에 넣었고, 내부적으로 서비스 3개 + 토픽 2개로 구현하되 CLI·도구가 액션을 1급으로 인식한다(ros2 action list·send_goal). 대가: 상태기계가 복잡해졌고, 목표 취소·중단의 의미론을 정확히 이해해야 한다(1.8).
⑥ nodelet → 컴포넌트. ROS 1의 nodelet은 "복사 없이 통신하려면 nodelet으로 다시 짜야 하는" 별도 API였다. ROS 2는 같은 노드 코드를 프로세스 안에 여러 개 담을 수 있게 했고(컴포넌트, 1.13), intra-process 통신으로 복사를 없앨 수 있다(2.7). 대가: 컴포넌트 컨테이너·ComposableNode launch 구성이라는 새 개념이 생겼고, 프로세스 격리를 잃는다(한 노드가 종료되면 컨테이너 전체가 종료된다).
⑦ 시간 추상화. ROS 1의 /clock과 use_sim_time은 ROS 2에서도 유지되지만, rclcpp::Clock이 ROS 시간·시스템 시간·steady 시간을 명시적으로 구분한다(1.10). 대가: 어떤 시계를 쓸지 코드에서 결정해야 하고, 잘못 고르면 시뮬레이션에서 타이머가 멈추거나 실기에서 시간이 불연속적으로 변한다.
0.2.8 결정 ⑧~⑨ — 빌드 시스템과 launch
⑧ catkin → ament + colcon.
ROS 1의 catkin_make는 워크스페이스 전체를 하나의 CMake 프로젝트로 묶었고, devel 공간 덕에 빌드 후 바로 실행할 수 있었다. ROS 2는 패키지마다 독립 빌드(ament)하고, 그 순서를 colcon이 의존성 그래프로 조율한다(4.1·4.5).
- 얻은 것: 패키지 격리(한 패키지의 CMake 오염이 다른 패키지로 새지 않는다), 다중 빌드 타입(CMake·Python·외부 프로젝트) 혼합, 병렬 빌드, 다중 플랫폼.
- 대가:
devel공간이 없어 install 공간이 필수가 됐다(--symlink-install로 완화, 0.5). 빌드가 느려질 수 있고,colcon은 ROS에 국한되지 않는 범용 도구라 ROS 특유의 편의가 얇다. 무엇보다 환경 설정(오버레이) 문제가 새로운 주요 실패 원인이 됐다.
⑨ launch: XML에서 Python으로.
ROS 1의 launch는 XML 선언이었다. ROS 2는 Python launch를 1급으로 두고 XML·YAML도 지원한다(14.3·14.4).
- 얻은 것: 조건·반복·계산이 가능하고, 프로세스 생명주기 이벤트에 반응할 수 있으며(노드가 종료되면 무엇을 할지), 라이프사이클 전이를 오케스트레이션할 수 있다.
- 대가: 훨씬 어렵다. substitution(
LaunchConfiguration)은 "값이 아니라 나중에 평가될 식"이라는 개념을 요구하고, 이것이 Python 변수처럼 보여서 초심자를 계속 넘어뜨린다. 정적 분석도 어렵다 — 무엇이 뜰지 실행 전에 알기 어렵다.
0.2.13 결정 ⑩ — 라이프사이클(관리형 노드)
번호에 대하여: 이 절은 나중에 추가되었다. 서두(0.2.1)와 종합 도해(0.2.11)가 라이프사이클을 핵심 결정으로 꼽으면서 본문에는 항목이 없던 것을 메운다. 규약(CLD-03)에 따라 번호는 끝에 붙이고, 읽는 흐름을 위해 결정 목록 뒤·대응표 앞에 놓았다.
동기: ROS 1에는 노드의 상태라는 개념이 없었다. 프로세스가 실행 중이면 "실행되고 있는 것"이고, 초기화가 끝났는지·자원을 확보했는지·지금 명령을 받아도 되는지를 밖에서 알 방법이 없었다. 그래서 실무에서는 "센서가 준비될 때까지 5초 기다린다" 같은 sleep이 시스템 곳곳에 박혔고, 그 값은 느린 날 깨졌다.
변경: ROS 2는 관리형 노드(managed node) 를 표준화했다. 노드가 unconfigured → inactive → active → finalized의 상태를 갖고, 전이(configure·activate·deactivate·cleanup·shutdown)가 서비스로 노출된다(1.12). 밖에서 상태를 묻고, 순서대로 전이시키고, 실패를 관측할 수 있다.
얻은 것:
- 부팅 순서를 명시적으로 조율할 수 있다 — sleep 대신 "configure가 성공하면 다음"(14.2.3).
- 자원 획득 시점이 계약이 된다 —
on_configure에서 장치를 열고on_activate에서 발행을 시작한다는 규약이 생겼다(6.2.6의 하드웨어 컴포넌트가 그대로 이 모델이다). - 비활성 상태가 1급 개념이 된다 — 실행 중이지만 명령을 받지 않는 상태를 표현할 수 있어, 모드 전환(14.2.7)이 안전해진다.
- 진단·오케스트레이션 도구가 노드 상태를 표준 방식으로 읽는다.
대가:
- 선택적이라 절반만 쓰인다. 라이프사이클은 강제가 아니다. 시스템에 관리형 노드와 일반 노드가 섞이면 오케스트레이션이 두 가지 방식을 모두 다뤄야 한다 — 14.2.4가 "핵심 경로만 생애주기로, 나머지는 준비 확인으로"라는 절충을 권하는 이유다.
- 코드가 늘어난다. 초기화를
on_configure·on_activate로 분할하야 하고, 각 전이의 실패 처리를 써야 한다. - 상태를 누가 옮기는가라는 문제가 새로 생긴다. 노드는 스스로 활성화되지 않으므로 조율자가 필요하고(14.2.9), 그것을 잊으면 "노드는 기동됐는데 아무 일도 안 한다"가 된다.
- 라이프사이클은 도구이지 오케스트레이터가 아니다(0.1.8의 마지막 행). 상태 기계를 제공할 뿐, 시스템 모드·장애 대응·부팅 정책은 여전히 응용이 설계해야 한다.
견해: 이 결정은 "얻은 것"보다 "가능해진 것"으로 평가해야 한다. 라이프사이클 자체가 문제를 풀어 주지는 않는다. 다만 부팅·모드·장애를 명시적으로 설계할 수 있는 어휘를 준다 — ROS 1에서는 그 어휘가 없어 각 팀이 제각기 발명했다. 표준 어휘가 생겼다는 것이 실질적 이득이며, 그래서
ros2_control(6.1)·Nav2(8.1)처럼 상태가 중요한 스택이 전부 이 모델을 채택했다.
0.2.9 개념 대응표 — 그리고 옮겨지지 않는 것
| ROS 1 | ROS 2 | 주의 |
|---|---|---|
roscore / master | (없음) — 분산 디스커버리 | 진실의 원천이 사라짐(0.2.3) |
roscpp / rospy | rclcpp / rclpy (위에 rcl) | API가 다르다. 포팅은 재작성에 가깝다 |
| TCPROS / UDPROS | DDS·RTPS (또는 Zenoh) | 전송 선택이 QoS·벤더 설정으로 이동 |
rosrun / roslaunch | ros2 run / ros2 launch | launch 언어가 Python 우선 |
rostopic 등 | ros2 topic 등 통합 CLI | ros2 <명령> <동사> 구조(0.7) |
| 파라미터 서버 | 노드 소유 파라미터 | 전역 공유 없음(0.2.6) |
actionlib | 코어 액션(rcl_action) | 도구가 1급 인식(1.8) |
| nodelet | 컴포넌트(rclcpp_components) | 같은 노드 코드 재사용(1.13) |
catkin | ament + colcon | devel 공간 없음(4.1) |
.msg/.srv | 동일 + .action, IDL 기반 | 타입 해시 도입(1.3) |
tf → tf2 | tf2(동일 계보) | API·헤더가 정리됨(5.1) |
rosbag (.bag) | rosbag2 (Jazzy 기본 MCAP) | 포맷 비호환, 변환 필요(11.2) |
rviz | rviz2 | 플러그인 재작성 필요(11.4) |
ros_control | ros2_control | 아키텍처 유사, API 상이(6.1) |
move_base | Nav2 | 행동 트리 기반으로 재설계(8.1) |
| MoveIt | MoveIt 2 | 상당 부분 재작성(9.1) |
옮겨지지 않는 것(대응물이 없거나 성격이 다름):
- "마스터에 물으면 다 안다" — 없다. 그래프 관측은 근사적이다(0.7).
- "연결되면 반드시 받는다" — QoS 계약으로 대체됐다.
rostopic echo의 무조건적 동작 — QoS 호환에 종속된다.- 글로벌 파라미터 — launch 주입·토픽 배포로 대체.
rosdep은 남아 있으나 의미가 확장됐다(4.6).
0.2.10 ROS 2에서 새로 생긴 실패 양상
ROS 1 경험자가 ROS 2에서 처음 만나는, ROS 1에는 존재하지 않던 버그 종류들이다. 각각 이 책의 어디서 다루는지 함께 적는다.
| 새 실패 양상 | 증상 | 원인 층 | 상세 |
|---|---|---|---|
| QoS 침묵 불일치 | 토픽은 보이는데 데이터가 안 옴, 오류 메시지 없음 | rmw 계약 | 1.5·1.6 |
| 디스커버리 미도달 | 같은 네트워크인데 노드가 서로 안 보임 | DDS·네트워크 | 3.3·3.9 |
| 디스커버리 폭주 | 노드 수십 개 이상에서 CPU·대역폭 급증, 시작이 느려짐 | DDS | 3.3·3.4 |
| 도메인 누출 | 옆자리 개발자의 노드와 연결됨 | 환경 | 0.4 |
| 오버레이 오염 | 빌드했는데 옛 코드가 돎, 삭제한 패키지가 계속 보임 | 빌드 환경 | 0.5 |
| executor 기아 | 콜백 하나가 길어 다른 콜백이 기아 상태, 타이머가 밀림 | rclcpp | 2.2~2.4 |
| 서비스 데드락 | 콜백 안에서 서비스를 동기 호출해 영구 정지 | rclcpp | 1.7 |
| 시간 소스 혼선 | 시뮬에서 타이머가 안 돎, use_sim_time 누락 | rcl·rclcpp | 1.10 |
| 대용량 토픽 붕괴 | 이미지·포인트클라우드에서 지연·유실 폭증 | DDS·직렬화 | 7.8·12.6 |
| Jazzy 콜백 순서 변화 | Iron 이전과 콜백 실행 순서가 달라짐 | rclcpp executor | 0.3·2.3 |
이 표는 이 책의 문제 기반 목차로 써도 좋다. 11.1(현장 디버깅 플레이북)이 이 표를 절차로 확장한다.
0.2.11 종합 — 무엇을 위해 이 복잡도를 감수하나
견해: 이 교환은 시스템 규모가 커질수록 이득이다. 노드 다섯 개로 구성된 실험 로봇에서는 ROS 2가 ROS 1보다 확실히 번거롭다. 그러나 노드 수십 개, 컴퓨터 여러 대, 로봇 여러 대, 무선 링크, 보안 요구, 실시간 제어가 섞이는 순간 ROS 1로는 애초에 도달할 수 없는 지점들이 있다. 이 책은 그 규모에서 일하는 사람**을 독자로 놓았고(BK-02), 따라서 대가를 감추지 않고 다루는 쪽을 택했다.
실무적 처방은 단순하다 — 대가가 나타나는 지점을 미리 알고, 그 지점마다 관측 수단을 갖춰라. QoS는 이벤트 콜백으로 감시하고(1.6), 디스커버리는 규모를 측정하고(3.3), 실행 지연은 트레이싱으로 보고(11.6), 환경은 스크립트로 고정한다(0.5·14.8).
0.2.12 체크포인트
- ROS 1의 마스터가 한 일을 한 문장으로 말하고, ROS 2가 그것을 무엇으로 대체했는지 설명할 수 있는가.
- 마스터 제거가 가져온 네 가지 대가를 댈 수 있는가(진실의 원천 상실, $O(N^2)$ 디스커버리, 네트워크 민감성, 의도치 않은 연결).
- QoS 도입이 해결한 문제와 새로 만든 문제를 각각 두 개씩 말할 수 있는가.
- ROS 1의 파라미터 서버 패턴(여러 노드가 공통 설정을 읽기)을 ROS 2에서 재현하려면 어떤 선택지가 있는가 — launch 주입,
TRANSIENT_LOCAL토픽, 전용 설정 노드. 각각의 트레이드오프는? - nodelet과 ROS 2 컴포넌트의 차이를 "코드 재작성 필요 여부" 관점에서 설명할 수 있는가.
- 0.2.10 표의 열 가지 실패 양상 중, 지금 담당 시스템에서 이미 겪은 것과 아직 안 겪었지만 규모가 커지면 겪을 것을 구분해 보라.
- 라이프사이클이 ROS 1에 없던 어휘를 준다는 말의 뜻은 무엇인가. 그것이 "문제를 푼다"와 어떻게 다른가(0.2.13).
다음 장 0.3은 이 ROS 2가 Jazzy Jalisco라는 특정 배포판에서 어떤 모습인지 — 지원 기간·플랫폼·이 배포판에서 바뀐 것과 사라진 API — 를 확정한다.