자율주행은 한 번의 명령이 아니라 하나의 흐름이다
이제 우리는 꽤 먼 곳까지 왔다. ROS2의 노드와 토픽을 배웠고, TF로 좌표계를 연결했으며, URDF와 RViz로 로봇을 눈앞에 세웠다. Gazebo에서 움직여 보기도 했고, LiDAR와 오도메트리로 SLAM 지도를 만들고 저장하는 과정도 살펴보았다.
그렇다면 이제 질문은 하나다.
로봇에게 "저쪽 방 입구까지 가"라고 말하면, 내부에서는 무슨 일이 벌어질까?
사람에게는 간단한 부탁처럼 들린다. 하지만 로봇에게는 전혀 간단하지 않다. 로봇은 먼저 자신이 어디에 있는지 알아야 한다. 목표가 어디인지도 알아야 한다. 벽과 통로가 어디에 있는지 알아야 하고, 방금 앞에 나타난 사람이나 의자도 피해야 한다. 그리고 이 모든 판단을 바탕으로 바퀴를 얼마나 빠르게, 어느 방향으로 굴릴지 결정해야 한다.
자율주행 로봇의 전체 구조는 이 판단의 연쇄다. 어떤 노드 하나가 마법처럼 모든 일을 처리하는 것이 아니다. 여러 노드가 각자 맡은 일을 하고, 토픽과 액션과 TF를 통해 정보를 주고받으며, 마지막에는 /cmd_vel이라는 속도 명령으로 모인다.
이 장에서는 Nav2를 본격적으로 배우기 전에 큰 그림을 잡는다. 세부 파라미터나 알고리즘 이름보다 중요한 것은 "무엇이 무엇에게 데이터를 넘기고, 그 결과 로봇이 어떻게 움직이는가"이다.

목표 지점까지 가려면 무엇이 필요한가
로봇이 목표 지점까지 이동하려면 최소한 네 가지 정보가 필요하다.
첫째, 현재 위치가 필요하다. 로봇이 지금 어디에 있는지 모르면 목표까지의 방향도 알 수 없다. 지도 위에서 현재 위치를 추정하는 역할은 보통 AMCL 같은 위치 추정 노드가 맡는다. 오도메트리만으로도 짧은 시간의 움직임은 추정할 수 있지만, 바퀴 미끄러짐과 센서 오차 때문에 시간이 지나면 위치가 조금씩 틀어진다. 그래서 저장된 지도와 LiDAR 데이터를 비교해 "지도 위의 나는 여기쯤 있다"라고 계속 보정한다.
둘째, 목표 위치가 필요하다. 목표는 보통 RViz에서 클릭하거나, 프로그램이 액션 요청으로 보낸다. 이 목표는 단순한 점 하나가 아니다. 대부분 x, y 위치와 함께 로봇이 도착했을 때 바라볼 방향까지 포함한다. 예를 들어 복도 끝으로 이동하더라도, 문을 바라보고 멈출지, 복도 방향을 따라 멈출지에 따라 최종 자세가 달라진다.
셋째, 지도가 필요하다. 지도는 로봇이 세상을 이해하는 배경이다. 벽이 어디 있는지, 통로가 어디로 이어지는지, 갈 수 없는 공간이 어디인지 알려 준다. 앞 장에서 저장한 정적 지도는 바로 이 역할을 한다. 지도 없이도 센서만으로 주변을 피하며 움직일 수는 있지만, "반대편 방까지 가는 길"처럼 넓은 범위의 이동을 안정적으로 계획하기는 어렵다.
넷째, 실시간 센서 데이터가 필요하다. 지도는 과거의 기록이다. 어제 없던 박스가 오늘 복도 한가운데 놓여 있을 수도 있고, 사람이 갑자기 로봇 앞을 지나갈 수도 있다. LiDAR, 카메라, 깊이 센서 같은 실시간 센서는 이런 변화를 알려 준다.
이 네 가지가 함께 있어야 자율주행이 가능해진다. 하나라도 빠지면 로봇은 금방 이상한 행동을 한다. 현재 위치를 모르면 길을 잃고, 목표 위치를 모르면 출발할 이유가 없으며, 지도를 모르면 멀리 돌아가는 길을 찾기 어렵고, 실시간 센서가 없으면 갑자기 나타난 장애물에 부딪힐 수 있다.
현재 위치: 로봇은 지도 위의 자신을 찾아야 한다
자율주행의 첫 번째 질문은 "나는 어디에 있는가?"이다. 이 질문에 답하기 위해 ROS2 로봇은 보통 TF 트리를 사용한다.
우리가 앞에서 반복해서 본 핵심 구조는 다음과 같다.
map → odom → base_linkbase_link는 로봇 몸체의 기준 좌표계다. 쉽게 말해 로봇의 중심이다. odom은 오도메트리 기준 좌표계다. 바퀴 회전량이나 관성 센서 정보를 바탕으로 로봇이 출발 지점에서 얼마나 움직였는지 추정한다. map은 저장된 지도 기준 좌표계다. 로봇이 실제 환경의 지도 위에서 어디에 있는지를 표현한다.
오도메트리는 짧은 시간 동안 부드럽게 변한다. 로봇이 앞으로 10cm 움직이면 odom → base_link 변환도 자연스럽게 바뀐다. 하지만 오도메트리는 조금씩 틀어진다. 반면 지도 기반 위치 추정은 주변 벽과 장애물 모양을 보고 누적 오차를 보정한다. 이때 보정의 결과가 보통 map → odom 변환으로 표현된다.
이 구조가 중요한 이유는 자율주행의 모든 판단이 결국 좌표계를 기준으로 이루어지기 때문이다. 목표가 map 기준으로 주어졌는데 로봇 위치가 odom 기준으로만 있다면, 둘을 바로 비교할 수 없다. LiDAR 데이터가 laser_frame 기준으로 들어오는데 로봇 몸체와의 관계를 모르면, 장애물이 로봇의 앞인지 옆인지 판단할 수 없다.
그래서 자율주행이 잘 안 될 때는 멋진 알고리즘 이름보다 먼저 TF를 확인해야 한다. 로봇이 지도 위에서 순간이동하는 것처럼 보이거나, 목표를 찍었는데 엉뚱한 방향으로 가려 한다면 좌표계 연결이 잘못되었을 가능성이 높다.
목표 위치: 로봇에게 도착해야 할 곳을 알려 주기
현재 위치를 알았다면 다음은 목표 위치다. 자율주행 로봇에게 목표는 단순한 문자열 명령이 아니다. "주방으로 가"라는 사람의 말도 결국 로봇 내부에서는 지도 좌표로 바뀌어야 한다.
ROS2에서 이동 목표는 보통 위치와 방향을 포함한 포즈로 표현된다. 포즈는 위치와 자세를 합친 말이다. 2D 이동 로봇이라면 대체로 x, y, yaw가 중요하다. 여기서 yaw는 평면에서 로봇이 바라보는 회전 방향이다.
RViz에서 목표 지점을 찍는 장면을 떠올려 보자. 마우스로 지도를 클릭하고 드래그하면 화살표가 생긴다. 화살표의 시작점은 도착 위치이고, 화살표 방향은 도착했을 때 로봇이 바라볼 방향이다. 이 정보가 내비게이션 시스템으로 전달되면, 로봇은 현재 위치에서 목표 위치까지 갈 수 있는 경로를 찾기 시작한다.
여기서 중요한 점은 목표 좌표의 기준 프레임이다. 일반적으로 목표는 map 프레임 기준으로 주어진다. 저장된 지도 위에서 "여기까지 가라"고 말하는 것이기 때문이다. 만약 목표가 다른 프레임 기준으로 잘못 들어가면, 로봇은 전혀 엉뚱한 장소를 목표로 해석할 수 있다.
자율주행에서 좌표계는 주소 체계와 같다. 서울의 주소와 부산의 주소를 같은 숫자만 보고 비교할 수 없는 것처럼, 서로 다른 프레임의 좌표를 그냥 섞으면 안 된다.
지도: 로봇이 세상을 읽는 배경
지도는 자율주행의 무대다. 앞 장에서 우리는 SLAM으로 만든 지도를 저장하고 다시 불러오는 방법을 살펴보았다. 그 지도는 보통 점유 격자 지도 형태로 사용된다. 점유 격자 지도는 공간을 작은 칸으로 나누고, 각 칸이 비어 있는지, 막혀 있는지, 알 수 없는지를 표현한다.
사람이 보는 지도는 도로 이름과 건물 모양이 중요하지만, 이동 로봇이 보는 지도는 조금 다르다. 로봇에게 중요한 질문은 훨씬 직접적이다.
"이 칸을 지나갈 수 있는가?"
벽이 있는 칸은 지나갈 수 없다. 빈 복도는 지나갈 수 있다. 아직 관측하지 못한 영역은 상황에 따라 조심해야 한다. 이 단순한 표현만으로도 로봇은 꽤 많은 판단을 할 수 있다.
하지만 정적 지도만으로는 충분하지 않다. 지도에 표시된 벽은 변하지 않겠지만, 환경에는 늘 변화가 있다. 의자, 박스, 사람, 다른 로봇은 지도에 없을 수 있다. 그래서 내비게이션 시스템은 저장된 지도와 실시간 센서 데이터를 함께 사용한다.
이 둘을 결합해 "지금 이 순간 로봇이 지나가도 안전한 공간"을 표현하는 자료 구조가 costmap이다.
Costmap: 위험을 숫자로 바꾼 지도
Costmap은 이름 그대로 비용 지도다. 여기서 비용은 돈이 아니라 위험도에 가깝다. 로봇이 어떤 칸을 지나갈 때 얼마나 위험한지 숫자로 표현한다.
빈 공간은 비용이 낮다. 벽이나 장애물이 있는 곳은 비용이 매우 높다. 장애물 바로 옆도 완전히 안전하다고 보기 어렵기 때문에 비용을 높게 줄 수 있다. 로봇은 이 비용을 보며 되도록 안전한 길을 선택한다.
Costmap은 보통 여러 정보가 겹쳐져 만들어진다.
저장된 지도에서 가져온 벽과 고정 장애물
LiDAR나 깊이 카메라가 감지한 실시간 장애물
로봇 반지름과 안전거리를 고려해 장애물 주변에 부여한 위험 영역
예를 들어 지도에는 복도가 깨끗하게 비어 있다고 하자. 그런데 LiDAR가 로봇 앞 1m 지점에서 박스를 감지했다. 그러면 costmap은 그 위치를 장애물로 표시한다. 또한 박스 바로 옆을 스치듯 지나가는 것도 위험하므로 주변 칸의 비용도 함께 높인다. 이처럼 장애물 주변으로 위험 영역을 부풀리는 과정을 inflation이라고 부른다.
Inflation은 로봇을 겁쟁이로 만드는 장치가 아니라, 현실적인 안전장치다. 지도상으로는 로봇 중심점이 벽을 피해서 지나갈 수 있어 보여도, 실제 로봇은 크기가 있다. 중심점만 보고 경로를 만들면 로봇 몸체가 벽이나 가구에 부딪힐 수 있다. 그래서 장애물 주변에 여유 공간을 둔다.
Costmap을 이해하면 자율주행 문제를 훨씬 현실적으로 볼 수 있다. 로봇이 벽에서 너무 멀리 떨어져 다니면 inflation 반경이 너무 클 수 있다. 반대로 좁은 틈을 무리하게 지나가려 한다면 안전거리 설정이 부족할 수 있다. 로봇이 갑자기 멈춘다면 센서가 가짜 장애물을 보고 costmap에 높은 비용을 찍고 있을 수도 있다.
전역 계획: 어느 길로 갈 것인가
현재 위치와 목표 위치, 그리고 지도가 준비되면 로봇은 큰 길을 찾아야 한다. 이 역할을 맡는 것이 global planner다. 한국어로는 전역 경로 계획기라고 부를 수 있다.
Global planner의 질문은 이렇다.
"지도 전체를 봤을 때, 현재 위치에서 목표 위치까지 어떤 경로로 가는 것이 좋은가?"
전역 계획은 넓은 시야를 가진다. 방에서 나와 복도를 지나고, 코너를 돌아 목표 방으로 들어가는 전체 흐름을 만든다. 이때 global costmap을 사용한다. Global costmap은 비교적 넓은 영역, 때로는 지도 전체를 대상으로 장애물과 비용 정보를 표현한다.
전역 경로는 보통 선의 형태로 표현된다. RViz에서 자율주행 목표를 찍었을 때 지도 위에 길게 그려지는 선이 바로 이 전역 경로다. 이 선은 로봇이 따라가야 할 추천 경로다.
하지만 전역 경로가 곧바로 바퀴 명령은 아니다. 전역 경로는 "대략 이 길로 가라"는 계획에 가깝다. 실제 로봇은 매 순간 속도 제한, 회전 반경, 근처 장애물, 미끄러짐, 센서 노이즈를 고려해야 한다. 그래서 다음 단계가 필요하다.
지역 제어: 지금 당장 어떻게 움직일 것인가
Global planner가 큰 길을 만든다면, local controller는 지금 당장 로봇이 어떻게 움직여야 하는지 결정한다. 한국어로는 지역 제어기 또는 로컬 컨트롤러라고 부를 수 있다.
두 역할의 차이를 사람의 운전에 비유해 보자. 내비게이션 앱이 "500m 뒤 우회전"이라고 알려 주는 것은 전역 계획에 가깝다. 하지만 운전자가 지금 핸들을 얼마나 돌리고, 브레이크를 밟을지, 앞차와 거리를 얼마나 둘지는 지역 제어에 가깝다.
로봇도 마찬가지다. Global planner가 만든 경로를 따라가되, local controller는 가까운 주변 상황을 계속 확인한다. 앞에 갑자기 사람이 나타나면 속도를 줄이거나 멈춰야 한다. 경로에서 조금 벗어났다면 다시 경로 쪽으로 부드럽게 돌아와야 한다. 회전해야 하는 구간에서는 직진 속도를 낮추고 각속도를 높여야 한다.
Local controller는 보통 local costmap을 사용한다. Local costmap은 로봇 주변의 작은 영역을 빠르게 갱신한다. 전역 지도 전체를 매 순간 새로 계산하는 대신, 로봇 주변 몇 미터 안의 장애물과 비용을 집중적으로 본다.
이 단계에서 최종적으로 만들어지는 출력이 속도 명령이다. ROS2 이동 로봇에서는 이 속도 명령이 흔히 /cmd_vel 토픽으로 발행된다.
/cmd_vel 메시지는 보통 선속도와 각속도를 담는다. 선속도는 앞으로 또는 뒤로 얼마나 빠르게 움직일지를 나타내고, 각속도는 얼마나 빠르게 회전할지를 나타낸다. 차동 구동 로봇이라면 linear.x와 angular.z가 핵심이다.
쉽게 말해 local controller는 매 순간 이런 결정을 내린다.
"지금은 초속 0.2m로 앞으로 가고, 초당 0.5rad 정도 왼쪽으로 돌자."
이 결정이 수십 밀리초 또는 수백 밀리초마다 반복되면서 로봇은 경로를 따라 움직인다.
/cmd_vel: 판단이 움직임으로 바뀌는 마지막 문
자율주행 파이프라인의 끝에는 /cmd_vel이 있다. 앞에서 아무리 멋진 지도를 만들고, 현재 위치를 정확히 추정하고, 경로를 아름답게 그려도, 결국 로봇을 움직이는 직접 명령은 속도 명령이다.
/cmd_vel은 대개 geometry_msgs/msg/Twist 타입을 사용한다. 이 메시지에는 선형 속도와 회전 속도가 들어 있다. 2D 바퀴 로봇에서는 대부분 다음 두 값이 중요하다.
linear.x 앞으로 가는 속도
angular.z 제자리 또는 주행 중 회전하는 속도이 토픽을 구독하는 쪽은 로봇 구성에 따라 달라진다. 시뮬레이션에서는 Gazebo 플러그인이 /cmd_vel을 받아 가상 바퀴를 움직일 수 있다. 실제 로봇에서는 모터 제어 노드나 하위 제어기가 이 명령을 받아 왼쪽 바퀴와 오른쪽 바퀴의 회전 속도로 변환한다. ros2_control을 사용하는 시스템이라면 컨트롤러가 이 흐름의 중간에 들어간다.
흐름을 단순화하면 다음과 같다.
local controller
↓ /cmd_vel
모터 제어 노드 또는 시뮬레이션 플러그인
↓
바퀴 회전
↓
로봇 이동
↓
오도메트리와 센서 데이터 변화여기서 흥미로운 점은 끝이 다시 시작으로 이어진다는 것이다. 로봇이 움직이면 바퀴 엔코더 값이 바뀌고, 오도메트리가 갱신된다. LiDAR가 보는 벽의 위치도 달라진다. 위치 추정은 새 센서 데이터를 보고 현재 위치를 다시 계산한다. planner와 controller는 갱신된 정보를 바탕으로 다음 명령을 만든다.
자율주행은 직선 파이프라인처럼 보이지만, 실제로는 계속 도는 순환 루프다.
Sense-Plan-Act로 보는 자율주행
로봇 시스템을 이해할 때 자주 사용하는 구조가 Sense-Plan-Act다. 말 그대로 감지하고, 계획하고, 행동한다는 뜻이다.
Sense는 세상을 관측하는 단계다. LiDAR, 카메라, IMU, 엔코더 같은 센서가 데이터를 만든다. ROS2에서는 이 데이터가 /scan, /image, /imu, /odom, /tf 같은 토픽으로 흘러간다.
Plan은 판단하고 계획하는 단계다. 현재 위치를 추정하고, 지도와 costmap을 만들고, 목표까지의 경로를 계산한다. 이 단계에는 map server, localization, costmap, global planner 같은 구성 요소가 들어간다.
Act는 실제로 움직이는 단계다. Local controller가 속도 명령을 만들고, 모터 제어기나 시뮬레이션 플러그인이 그 명령을 바퀴 움직임으로 바꾼다.
ROS2 토픽 그래프로 표현하면 대략 다음과 같은 흐름이다.
[LiDAR] ── /scan ──┐
├── [localization] ── TF(map→odom→base_link)
[Encoder] ─ /odom ─┘
[Map Server] ── /map ──┐
├── [Costmap]
[LiDAR] ───── /scan ───┘
[Current Pose] + [Goal Pose] + [Global Costmap]
↓
[Global Planner] ── /plan
↓
[Local Controller] + [Local Costmap]
↓
/cmd_vel
↓
[Motor Controller or Gazebo Plugin]
↓
Robot Motion이 그림에서 가장 중요한 것은 데이터가 한 방향으로만 끝나지 않는다는 점이다. 로봇이 움직이면 다시 센서 데이터가 바뀌고, 위치 추정이 바뀌고, costmap이 바뀌고, 제어 명령도 바뀐다. 로봇은 한 번 계산한 길을 맹목적으로 따라가지 않는다. 계속 보고, 다시 판단하고, 조금씩 수정한다.
정적 지도와 실시간 장애물은 어떻게 함께 쓰이는가
자율주행을 처음 배울 때 헷갈리는 부분 중 하나가 지도와 센서의 관계다. "지도에 벽이 있는데 LiDAR가 또 벽을 보면 중복 아닌가?" 또는 "지도만 있으면 센서가 없어도 되는 것 아닌가?"라는 질문이 자연스럽게 나온다.
정답은 둘 다 필요하다는 것이다.
정적 지도는 장기 기억에 가깝다. 건물 구조, 벽, 복도, 방의 위치처럼 잘 변하지 않는 정보를 담는다. 로봇은 이 정보를 이용해 멀리 있는 목표까지의 큰 경로를 만든다.
실시간 센서는 단기 기억에 가깝다. 지금 내 앞에 무엇이 있는지를 알려 준다. 지도에는 없지만 현재 나타난 장애물, 열린 문과 닫힌 문, 사람의 움직임 같은 정보는 센서가 담당한다.
Costmap은 이 장기 기억과 단기 기억을 합친다. 저장된 지도에서 벽을 가져오고, 센서에서 현재 장애물을 가져온다. 그리고 로봇 크기와 안전거리를 고려해 위험 영역을 계산한다. 그 결과 planner와 controller는 단순히 "벽이 어디 있는가"가 아니라 "어디가 안전하고 어디가 위험한가"를 볼 수 있다.
예를 들어 로봇이 복도를 따라 이동 중이라고 하자. 정적 지도만 보면 복도는 비어 있다. 그래서 global planner는 복도 중앙을 따라가는 경로를 만든다. 그런데 주행 도중 LiDAR가 앞쪽의 박스를 감지한다. Local costmap은 박스 위치를 장애물로 표시하고, local controller는 속도를 줄이거나 옆으로 피하는 명령을 만든다. 박스가 너무 커서 지나갈 수 없다면 시스템은 경로를 다시 계획하거나, 멈춘 뒤 복구 행동을 시도할 수 있다.
이것이 자율주행의 현실적인 모습이다. 지도는 길을 알려 주고, 센서는 지금의 위험을 알려 주며, costmap은 둘을 하나의 판단 재료로 합친다.
전역 경로와 지역 제어가 나뉘는 이유
처음에는 "그냥 목표까지 한 번에 속도 명령을 만들면 되지 않을까?"라고 생각할 수 있다. 작은 예제에서는 가능해 보인다. 현재 위치와 목표 위치를 비교해서 목표 방향으로 회전하고, 앞으로 가고, 다시 회전하면 된다.
하지만 실제 환경은 그렇게 단순하지 않다. 복도에는 모퉁이가 있고, 방에는 책상이 있으며, 사람은 예측하기 어렵게 움직인다. 로봇은 넓은 범위의 길 찾기와 가까운 범위의 순간 대응을 동시에 해야 한다.
그래서 global planner와 local controller가 역할을 나눈다.
Global planner는 전체적인 길을 본다. 지도 위에서 어느 통로를 지나야 하는지, 막다른 길을 피하려면 어디로 돌아가야 하는지 판단한다. 계산 범위가 넓기 때문에 너무 자주 복잡하게 계산하면 부담이 커질 수 있다.
Local controller는 가까운 미래를 본다. 다음 몇 초 동안 어떤 속도로 움직이면 경로를 잘 따라가면서 장애물을 피할 수 있는지 판단한다. 계산 범위는 좁지만 빠르게 반복되어야 한다.
이 역할 분담 덕분에 시스템이 유연해진다. 큰 길은 global planner가 잡고, 작은 흔들림과 갑작스러운 장애물은 local controller가 처리한다. 로봇이 목표까지 가는 모습은 마치 오케스트라와 비슷하다. 지도, 위치 추정, planner, controller, costmap, 센서 노드가 각자 다른 악기를 연주하지만, 최종적으로는 하나의 움직임을 만들어 낸다.
자율주행 토픽을 추적하는 방법
이제 자율주행 시스템을 볼 때 어떤 순서로 추적해야 하는지 정리해 보자. 로봇이 목표까지 가지 못할 때 무작정 파라미터를 바꾸기 시작하면 길을 잃기 쉽다. 대신 데이터 흐름을 따라가면 문제가 어디쯤 있는지 좁힐 수 있다.
먼저 센서 데이터가 들어오는지 확인한다. /scan이 발행되는지, /odom이 갱신되는지, /tf가 끊기지 않는지 본다. 센서가 없으면 localization도 costmap도 정상적으로 동작하기 어렵다.
그다음 현재 위치를 확인한다. RViz에서 로봇이 지도 위의 올바른 위치에 있는지 본다. 실제 로봇은 가만히 있는데 지도 위에서 흔들리거나, 회전 방향이 맞지 않는다면 위치 추정이나 TF에 문제가 있을 수 있다.
다음은 지도를 확인한다. /map이 정상적으로 발행되는지, RViz의 Fixed Frame이 map으로 설정되어 있는지, 저장한 지도와 실제 환경이 크게 다르지 않은지 살핀다.
그다음 costmap을 본다. 벽 주변에 위험 영역이 적절히 표시되는지, 로봇 앞 장애물이 local costmap에 나타나는지 확인한다. 로봇이 가지 못하는 이유가 실제 장애물 때문인지, 센서 노이즈 때문인지, inflation 설정 때문인지 여기서 드러나는 경우가 많다.
마지막으로 계획과 제어를 확인한다. 전역 경로가 만들어지는지, local controller가 /cmd_vel을 발행하는지, 그리고 그 /cmd_vel을 실제 구동계가 받고 있는지 본다.
문제를 추적하는 흐름은 다음처럼 생각할 수 있다.
센서가 보이는가?
↓
현재 위치가 맞는가?
↓
지도와 TF가 연결되어 있는가?
↓
costmap에 장애물이 올바르게 표시되는가?
↓
전역 경로가 생성되는가?
↓
/cmd_vel이 발행되는가?
↓
모터 또는 시뮬레이션이 그 명령을 실행하는가?이 순서를 익혀 두면 Nav2를 다룰 때 큰 도움이 된다. Nav2는 많은 기능을 제공하지만, 결국 이 흐름 위에 서 있다.
로봇이 멈췄을 때 생각해야 할 질문
자율주행 로봇은 종종 멈춘다. 초보자에게는 이것이 답답하게 느껴진다. 목표를 찍었는데 움직이지 않는다. 움직이다가 갑자기 멈춘다. 경로는 보이는데 바퀴가 돌지 않는다. 반대로 /cmd_vel은 나오는데 로봇이 제자리에서 떨기만 한다.
이때 중요한 것은 "Nav2가 안 된다"라고 뭉뚱그려 생각하지 않는 것이다. 자율주행은 여러 단계의 연쇄이므로, 멈춤의 원인도 단계별로 다르다.
현재 위치가 불안정하면 planner는 출발점을 믿을 수 없다. 목표 위치가 장애물 안에 있으면 경로를 만들기 어렵다. 지도와 실제 환경이 다르면 로봇은 벽을 통과하려 하거나, 빈 공간을 막힌 곳으로 생각할 수 있다. Costmap이 너무 보수적이면 갈 수 있는 통로도 막힌 것처럼 보인다. Controller 파라미터가 로봇의 실제 속도 한계와 맞지 않으면 명령은 나오지만 움직임이 불안정해진다. /cmd_vel이 발행되어도 모터 제어 노드가 다른 토픽을 듣고 있으면 바퀴는 돌지 않는다.
따라서 자율주행 디버깅은 추리 소설에 가깝다. 범인은 하나일 수도 있고, 여러 명일 수도 있다. 단서는 토픽, TF, costmap, 로그, RViz 화면에 흩어져 있다. 좋은 로봇 개발자는 이 단서를 차례로 연결해 원인을 좁혀 간다.
하나의 목표 이동을 처음부터 끝까지 따라가기
이제 로봇에게 목표를 주는 순간부터 실제로 움직이기까지의 과정을 하나의 이야기로 따라가 보자.
로봇은 이미 저장된 지도를 불러온 상태다. Map server는 /map을 발행하고 있다. AMCL 같은 위치 추정 노드는 LiDAR 데이터와 지도를 비교하면서 map → odom 변환을 갱신한다. 바퀴 엔코더 또는 시뮬레이션 플러그인은 /odom을 발행하고, TF를 통해 odom → base_link가 이어진다.
사용자가 RViz에서 목표 지점을 클릭한다. 목표는 map 기준의 포즈로 내비게이션 시스템에 전달된다. 시스템은 현재 로봇 위치와 목표 위치를 비교하고, global costmap을 참고해 갈 수 있는 길을 찾는다. Global planner는 벽과 위험 영역을 피해 전역 경로를 만든다.
이 전역 경로는 local controller에게 전달된다. Local controller는 로봇 주변의 local costmap을 보며 경로를 따라가기 위한 속도 명령을 계산한다. 근처에 장애물이 없으면 부드럽게 전진한다. 장애물이 나타나면 속도를 줄이거나 회전하며 피하려 한다.
계산된 속도 명령은 /cmd_vel로 발행된다. 실제 로봇에서는 모터 제어기가 이 명령을 받아 바퀴 속도로 바꾼다. 시뮬레이션에서는 Gazebo 플러그인이 가상 로봇의 바퀴를 굴린다. 로봇이 움직이면 오도메트리와 센서 데이터가 바뀐다. 위치 추정은 다시 현재 위치를 갱신하고, controller는 다시 다음 속도 명령을 만든다.
이 과정이 목표에 도착할 때까지 반복된다. 도착하면 시스템은 목표 위치와 현재 위치의 차이가 허용 범위 안에 들어왔는지 확인한다. 위치뿐 아니라 방향도 충분히 맞았는지 판단할 수 있다. 조건을 만족하면 목표 이동은 성공으로 끝난다.
겉으로는 로봇이 그냥 움직이는 것처럼 보이지만, 내부에서는 위치 추정, 지도, costmap, 계획, 제어, 구동이 끊임없이 대화하고 있다.
Nav2를 배우기 전에 가져야 할 큰 그림
다음 장부터 우리는 Nav2의 구조를 본격적으로 살펴본다. Nav2에는 planner server, controller server, behavior tree navigator, costmap, recovery behavior 같은 구성 요소가 등장한다. 이름만 보면 복잡해 보이지만, 이 장의 큰 그림을 기억하면 훨씬 쉽게 이해할 수 있다.
Nav2는 결국 자율주행 파이프라인을 ROS2 방식으로 잘 나누어 구현한 시스템이다. 각 구성 요소는 독립된 노드나 플러그인으로 동작하고, 토픽, 서비스, 액션, TF를 통해 협력한다.
큰 흐름은 다음과 같다.
현재 위치를 추정한다.
목표 위치를 받는다.
지도와 센서로 costmap을 만든다.
전역 경로를 계획한다.
지역 제어로 속도 명령을 만든다.
/cmd_vel로 로봇을 움직인다.
움직인 결과를 다시 센서와 위치 추정에 반영한다.이 흐름을 이해하면 Nav2 설정 파일을 볼 때도 덜 낯설다. global_costmap은 넓은 지도 기반 판단에 필요하고, local_costmap은 주변 장애물 대응에 필요하다. planner는 길을 만들고, controller는 속도를 만든다. robot_base_frame, global_frame, odom_frame 같은 프레임 이름은 TF 구조와 연결된다. 속도 제한 파라미터는 실제 로봇의 움직임과 연결된다.
자율주행 로봇은 단순히 목표를 향해 직진하는 기계가 아니다. 자기 위치를 찾고, 세상을 해석하고, 위험을 계산하고, 길을 계획하고, 순간적으로 제어하며, 다시 결과를 관측하는 순환 시스템이다.
이제 우리는 이 순환 시스템의 지도를 손에 넣었다. 다음 장에서는 이 지도를 들고 Nav2 내부로 들어간다. Planner, Controller, Behavior Tree, Costmap이 각각 어떤 방에서 어떤 일을 하는지 하나씩 문을 열어 볼 차례다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.