Skip to main content
Summary

앞 장에서 우리는 자율주행 로봇이 목표 지점까지 이동하기 위해 무엇을 알아야 하는지 큰 그림을 살펴보았다. 로봇은 현재 위치를 알아야 하고, 목표 위치를 받아야 하며, 지도를 보고 경로를 만들고, 센서로 장애물을 확인하면서 속도 명령을 내보내야 한다. 이 모든 일을 한 프로그램 안에 몰아넣으면 처음에는 단순해 보일 수 있다. 하지만 로봇이 실제 복도에서 사람을 만나고, 의자 다리에 걸리고, 지도와 실제 환경이 조금씩 어긋나기 시작하면 이야기가 달라진다.

Nav2는 이 복잡한 일을 하나의 노드가 전부 떠안지 않는다. 대신 여러 개의 독립적인 서버가 각자 맡은 역할을 수행하고, 그 위에서 bt_navigator가 전체 흐름을 조율한다. 공식 문서에서도 Nav2를 여러 독립적인 모듈 서버를 Behavior Tree로 조율하는 구조로 설명하며, 각 서버는 경로 계산, 제어, 복구 행동 같은 내비게이션 작업을 담당한다1.

처음 Nav2를 실행하면 터미널에는 많은 노드 이름이 한꺼번에 나타난다. planner_server, controller_server, smoother_server, behavior_server, bt_navigator, map_server, amcl, lifecycle_manager 같은 이름들이 빠르게 지나가면 마치 공장 라인을 보는 것 같다. 컨베이어 벨트 하나에 모든 일을 올려놓은 것이 아니라, 지도 담당자, 위치 추정 담당자, 경로 설계자, 운전 담당자, 비상 대응 담당자가 각각 자기 자리에서 움직이는 구조다.

이 구조를 이해하면 Nav2 설정 파일을 대하는 태도가 달라진다. 예전에는 nav2_params.yaml을 복사해서 실행해 보고, 안 되면 다른 예제를 다시 복사했을지도 모른다. 이제는 문제를 이렇게 나누어 볼 수 있다. "경로가 이상한가? 그렇다면 planner와 global costmap 쪽을 보자." "경로는 보이는데 로봇이 움직이지 않는가? controller와 /cmd_vel 흐름을 보자." "로봇이 계속 회전하거나 후진만 하는가? Behavior Tree와 behavior server를 보자."

Nav2의 내부 구조를 아는 것은 단순한 이론 공부가 아니다. 로봇이 멈췄을 때 원인을 찾는 지도다.

Nav2 구조 이해하기

목표 하나가 들어오면 무슨 일이 벌어질까

RViz에서 Nav2 Goal 버튼을 누르고 지도 위 한 지점을 클릭했다고 해 보자. 화면에는 초록색 또는 파란색 경로가 나타나고, 로봇은 잠시 고민하듯 멈췄다가 움직이기 시작한다. 겉으로 보기에는 "목표를 보냈더니 로봇이 간다"라는 단순한 사건이다. 하지만 내부에서는 여러 서버가 순서대로 손을 맞춘다.

대략적인 흐름은 다음과 같다.

목표 Pose
  → bt_navigator
  → planner_server
  → smoother_server
  → controller_server
  → velocity_smoother 또는 collision_monitor
  → /cmd_vel
  → 모터 드라이버

bt_navigator는 목표를 받았다고 해서 직접 경로를 계산하지 않는다. 운전대를 직접 잡지도 않는다. 대신 Behavior Tree에 적힌 절차에 따라 "경로를 계산하라", "경로를 따라가라", "실패했으면 costmap을 지워 보라", "그래도 안 되면 회전하거나 후진하라" 같은 결정을 내린다. Nav2의 Behavior Tree는 XML 파일로 정의할 수 있으며, 예제 트리에서는 ComputePathToPose와 FollowPath 같은 노드를 조합해 주기적으로 경로를 다시 만들고 경로를 따라가도록 구성한다2.

이때 ComputePathToPose는 Behavior Tree 안의 이름이지만, 실제 경로 계산은 planner_server가 담당한다. FollowPath 역시 Behavior Tree 안에서는 하나의 행동처럼 보이지만, 실제 속도 명령 계산은 controller_server가 수행한다. Behavior Tree의 노드와 ROS 2의 노드는 이름이 같아 보여도 개념적으로 다르다. Behavior Tree의 노드는 행동 흐름을 표현하는 조각이고, ROS 2 노드는 실행 중인 프로세스 또는 컴포넌트다. 이 차이를 놓치면 Nav2 구조가 갑자기 복잡하게 느껴진다.

비유하자면 bt_navigator는 영화감독에 가깝다. 감독은 배우가 직접 뛰는 속도를 계산하지 않는다. 대신 "이 장면에서는 주인공이 복도를 따라 달린다", "문이 막혀 있으면 돌아간다", "실패하면 다시 시도한다"라는 순서를 지시한다. 실제로 달리는 배우는 controller이고, 길을 그리는 스토리보드 담당자는 planner이며, 현장에서 위험 요소를 치우는 스태프가 behavior server다.

Planner Server: 큰길을 그리는 설계자

planner_server의 임무는 현재 위치에서 목표 위치까지 갈 수 있는 전역 경로, 즉 global path를 계산하는 것이다. 전역 경로는 로봇이 전체 지도에서 어느 방향으로 가야 하는지를 알려 주는 큰 선이다. 건물 안내 앱이 "엘리베이터 앞을 지나 오른쪽 복도로 이동하세요"라고 알려 주는 것과 비슷하다.

이때 planner_server는 보통 global costmap을 사용한다. global costmap은 저장된 지도나 SLAM으로 생성된 지도 위에 장애물과 안전 여유 공간을 비용 값으로 표현한 지도다. 벽은 지나갈 수 없는 높은 비용 영역이고, 벽 근처는 지나갈 수는 있지만 조심해야 하는 영역이다. planner는 이 비용을 보면서 가능한 한 안전하고 합리적인 경로를 찾는다.

중요한 점은 planner_server 자체가 하나의 고정된 알고리즘이 아니라는 것이다. Nav2에서는 플래너 알고리즘을 플러그인으로 선택한다. 예를 들어 NavFn, Smac Planner, Theta* 같은 플래너를 상황에 따라 사용할 수 있다. 설정 파일에서는 보통 다음과 같은 형태로 나타난다.

planner_server:
  ros__parameters:
    planner_plugins: ["GridBased"]
    GridBased:
      plugin: "nav2_navfn_planner/NavfnPlanner"
      tolerance: 0.5
      use_astar: false
      allow_unknown: true

여기서 GridBased는 이 설정에서 부르는 이름이고, nav2_navfn_planner/NavfnPlanner는 실제 로드할 플러그인 구현이다. 이 구분은 나중에 매우 중요해진다. Behavior Tree에서 특정 planner id를 지정하거나, 설정 파일에서 플러그인을 바꿀 때 이 이름들이 서로 맞아야 하기 때문이다.

전역 경로가 이상하게 벽을 뚫고 지나가거나, 지나갈 수 있는 복도를 돌아서 가거나, 아예 경로가 생성되지 않는다면 먼저 planner_server와 global costmap을 의심해 볼 수 있다. 지도 해상도, 로봇 footprint, inflation radius, unknown 영역 허용 여부 같은 설정이 전역 경로에 직접 영향을 준다.

Controller Server: 실제로 로봇을 움직이는 운전사

전역 경로는 "이 길로 가면 된다"는 계획이다. 하지만 실제 로봇은 계획선 위를 완벽하게 미끄러지듯 움직이지 않는다. 바퀴는 미끄러질 수 있고, 사람은 갑자기 앞을 지나갈 수 있으며, 로봇은 회전 반경이나 최대 속도 같은 물리적 제한을 가진다. 이 현실적인 움직임을 담당하는 서버가 controller_server다.

controller_server는 planner가 만든 경로를 받아 로봇이 지금 당장 낼 속도 명령을 계산한다. 보통 이 결과는 /cmd_vel 또는 그 앞단의 명령 토픽으로 이어진다. 여기서 속도 명령은 geometry_msgs/Twist 형식의 선속도와 각속도다. 쉽게 말해 "앞으로 0.2m/s로 가고, 왼쪽으로 0.4rad/s만큼 회전하라" 같은 명령이다.

controller는 global costmap보다 local costmap을 더 많이 본다. local costmap은 로봇 주변의 작은 창이다. 로봇이 움직이면 이 창도 따라 움직인다. global costmap이 전체 지도를 펼쳐 놓고 큰길을 찾는 지도라면, local costmap은 운전자의 전방 시야에 가깝다. 지금 앞에 사람이 있는지, 의자 다리가 새로 나타났는지, 문턱 근처가 위험한지 확인하는 영역이다.

설정 파일에서는 controller도 플러그인으로 지정된다.

controller_server:
  ros__parameters:
    controller_frequency: 20.0
    progress_checker_plugins: ["progress_checker"]
    goal_checker_plugins: ["general_goal_checker"]
    controller_plugins: ["FollowPath"]

    FollowPath:
      plugin: "dwb_core::DWBLocalPlanner"
      max_vel_x: 0.26
      max_vel_theta: 1.0
      acc_lim_x: 2.5
      acc_lim_theta: 3.2

FollowPath는 Behavior Tree에서 경로 추종 행동을 부를 때 연결되는 이름으로 자주 등장한다. 실제 구현은 dwb_core::DWBLocalPlanner일 수도 있고, 다른 controller 플러그인일 수도 있다. 예를 들어 단순하고 부드러운 추종이 필요한 로봇에는 Regulated Pure Pursuit 계열 컨트롤러가 적합할 수 있고, 더 복잡한 동적 제약을 고려하고 싶다면 MPPI 계열 컨트롤러를 검토할 수 있다.

로봇이 경로는 잘 생성하는데 움직이지 않는다면 controller 쪽을 살펴보아야 한다. local costmap이 로봇 주변을 전부 장애물로 보고 있지는 않은지, cmd_vel이 실제로 발행되는지, 속도 제한이 너무 작지는 않은지, goal checker가 너무 엄격해서 목표 근처에서 떨고 있지는 않은지 확인해야 한다.

Smoother Server: 각진 길을 다듬는 손

planner_server가 만든 경로는 때때로 격자 지도 위에서 계산되기 때문에 각진 선처럼 보일 수 있다. 좁은 복도에서는 괜찮아 보이지만, 실제 로봇이 이 선을 그대로 따라가면 움직임이 딱딱해지고 불필요한 회전이 많아질 수 있다. 특히 큰 로봇이나 Ackermann 조향 방식의 로봇, 즉 자동차처럼 앞바퀴 조향으로 움직이는 로봇은 급격한 방향 전환이 어렵다.

smoother_server는 이런 경로를 더 부드럽고 실행 가능한 형태로 다듬는다. 말 그대로 경로의 모서리를 사포로 살짝 갈아내는 역할이다. Nav2는 경로를 더 연속적이고 부드럽거나 로봇 운동 특성에 맞게 만들기 위한 smoothing 기능을 제공한다3.

물론 모든 로봇에서 smoother가 반드시 극적인 차이를 만드는 것은 아니다. 작은 차동 구동 로봇은 제자리 회전이 가능하기 때문에 다소 각진 경로도 따라갈 수 있다. 하지만 로봇이 커지거나, 속도가 빨라지거나, 회전 반경 제약이 강해질수록 smoother의 중요성은 커진다.

설정에서는 대략 다음과 같은 형태를 볼 수 있다.

smoother_server:
  ros__parameters:
    smoother_plugins: ["simple_smoother"]
    simple_smoother:
      plugin: "nav2_smoother::SimpleSmoother"

여기서도 구조는 같다. 서버는 틀을 제공하고, 실제 알고리즘은 플러그인으로 로드된다. 이 반복되는 패턴이 Nav2의 핵심이다.

Behavior Server: 막혔을 때의 비상 행동

로봇이 항상 계획대로 움직이면 navigation은 쉬운 문제가 된다. 하지만 실제 현장은 친절하지 않다. 로봇 앞에 박스가 놓일 수 있고, 좁은 문 앞에서 몸을 틀지 못할 수 있고, local costmap에 순간적인 노이즈가 들어와 길이 막힌 것처럼 보일 수도 있다. 이때 필요한 것이 복구 행동, 즉 recovery behavior다.

Nav2의 behavior_server는 회전, 후진, 대기, 특정 방향으로 이동 같은 행동을 실행한다. 예를 들어 로봇이 길을 잃은 듯 멈춰 있을 때 제자리 회전을 하며 주변 LiDAR 데이터를 새로 얻을 수 있다. 너무 벽에 가까이 붙었다면 조금 후진해서 local costmap에서 빠져나올 수 있다. 사람이 앞을 지나가는 상황이라면 무작정 움직이기보다 잠시 기다리는 편이 낫다.

설정은 보통 다음처럼 보인다.

behavior_server:
  ros__parameters:
    behavior_plugins: ["spin", "backup", "wait"]

    spin:
      plugin: "nav2_behaviors/Spin"
    backup:
      plugin: "nav2_behaviors/BackUp"
    wait:
      plugin: "nav2_behaviors/Wait"

여기서 주의할 점이 있다. behavior_server는 복구 행동을 실제로 실행하는 서버다. 반면 bt_navigator는 언제 어떤 복구 행동을 호출할지 결정하는 쪽이다. 이름이 비슷해서 둘을 혼동하기 쉽지만, 역할은 분명히 다르다.

behavior_server를 소방서라고 생각해 보자. 소방서에는 구조대, 구급차, 사다리차가 준비되어 있다. 하지만 어느 상황에 어떤 팀을 보낼지는 상황실이 판단한다. Nav2에서 상황실에 해당하는 것이 bt_navigator이고, 실제 출동 가능한 행동 팀이 behavior_server다.

bt_navigator: Behavior Tree로 전체 흐름을 지휘한다

Nav2 구조에서 가장 흥미로운 요소는 bt_navigator다. bt_navigator는 Behavior Tree를 실행하면서 내비게이션의 흐름을 결정한다. Behavior Tree는 로봇 행동을 트리 구조로 표현하는 방식이다. 각 노드는 성공, 실패, 실행 중 같은 상태를 반환하고, 부모 노드는 그 결과를 보고 다음 행동을 결정한다.

간단한 Behavior Tree는 다음과 같이 생각할 수 있다.

<root main_tree_to_execute="MainTree">
  <BehaviorTree ID="MainTree">
    <Sequence name="Navigate">
      <ComputePathToPose goal="{goal}" path="{path}"/>
      <FollowPath path="{path}"/>
    </Sequence>
  </BehaviorTree>
</root>

이 트리는 "경로를 계산한 뒤, 그 경로를 따라가라"는 뜻이다. Sequence는 자식 노드를 순서대로 실행한다. 첫 번째 행동이 성공해야 두 번째 행동으로 넘어간다. 만약 경로 계산이 실패하면 경로 추종은 시작하지 않는다. 공식 Behavior Tree 예제에서도 ComputePathToPose와 FollowPath를 조합해 기본적인 내비게이션 흐름을 구성할 수 있음을 보여 준다4.

실제 Nav2 기본 트리는 이보다 훨씬 풍부하다. 주기적으로 경로를 다시 계산하고, 목표가 바뀌었는지 확인하고, 경로가 아직 유효한지 검사하고, 실패하면 costmap을 지우거나 회전·대기·후진 같은 복구 행동을 시도한다. Nav2의 기본 navigate_to_pose_w_replanning_and_recovery.xml은 주기적 재계획과 복구 행동을 포함하는 대표적인 트리로 설명된다5.

여기서 중요한 설계 철학이 드러난다. Nav2는 "목표까지 가기"를 하나의 딱딱한 절차로 고정하지 않는다. 상황에 따라 다시 계획하고, 실패하면 복구하고, 목표가 바뀌면 반응한다. Behavior Tree는 이런 유연한 흐름을 XML로 바꿔 끼울 수 있게 해 준다.

예를 들어 서비스 로봇이 사무실에서 커피를 배달한다고 해 보자. 일반 이동만 필요하다면 기본 트리로 충분할 수 있다. 하지만 엘리베이터를 타야 한다면 "엘리베이터 호출", "문 열림 대기", "탑승", "층 선택", "하차" 같은 행동이 추가되어야 한다. 물류 로봇이라면 "도킹 스테이션에서 출발", "선반 앞 정렬", "적재 완료 신호 대기" 같은 행동이 필요할 수 있다. Behavior Tree를 사용하면 이런 고수준 행동을 navigation 흐름과 함께 구성할 수 있다.

Nav2의 nav2_behavior_tree 모듈은 BehaviorTree.CPP 라이브러리를 사용해 Behavior Tree 처리를 수행하며, ROS 2 액션과 서비스를 Behavior Tree 노드로 통합하기 위한 기능을 제공한다6. 그래서 ComputePathToPose, FollowPath, Spin, BackUp 같은 트리 노드는 내부적으로 각 서버의 액션 인터페이스를 호출할 수 있다.

액션 서버들이 서로 이어지는 방식

9장에서 액션을 배울 때 오래 걸리는 작업에는 goal, feedback, result가 필요하다고 했다. Nav2는 이 액션 구조를 적극적으로 사용한다. 목표 지점까지 이동하는 일은 즉시 끝나지 않는다. 경로 계산도 시간이 걸릴 수 있고, 경로 추종은 몇 초에서 몇 분까지 이어질 수 있으며, 중간에 취소되거나 새 목표로 교체될 수도 있다.

planner_server는 경로 계산 액션을 제공하고, controller_server는 경로 추종 액션을 제공한다. behavior_server의 각 행동도 액션으로 호출될 수 있다. bt_navigator는 이 액션들을 직접 수행하는 대신 액션 클라이언트처럼 호출하고 결과를 기다린다.

이 구조 덕분에 Nav2는 다음과 같은 상황을 자연스럽게 처리할 수 있다.

  • 사용자가 이동 중에 새 목표를 보낸다.

  • controller가 경로를 따라가다가 더 이상 진행하지 못한다고 보고한다.

  • planner가 현재 costmap에서는 목표까지 갈 경로가 없다고 실패를 반환한다.

  • 복구 행동이 성공하면 다시 경로 계산부터 시도한다.

  • 사용자가 목표를 취소하면 진행 중인 행동을 중단한다.

서비스였다면 이런 흐름을 표현하기 어렵다. 서비스는 짧은 요청과 응답에 적합하다. 반면 내비게이션은 긴 작업이고, 중간 피드백과 취소와 실패 처리가 중요하다. 그래서 Nav2의 주요 기능 서버들이 액션 서버로 동작한다는 점은 구조적으로 매우 자연스럽다.

Lifecycle Node: 준비되지 않은 로봇은 출발하지 않는다

Nav2를 실행할 때 자주 보게 되는 로그가 있다.

Configuring planner_server
Activating planner_server
Configuring controller_server
Activating controller_server

처음에는 단순한 실행 메시지처럼 보이지만, 여기에는 중요한 개념이 숨어 있다. Nav2의 주요 서버들은 ROS 2 Lifecycle Node로 동작한다. Lifecycle Node는 노드의 상태를 명시적으로 관리하는 방식이다. 일반 노드가 실행되면 곧바로 일을 시작하는 것과 달리, lifecycle 노드는 대략 다음 단계들을 거친다.

unconfigured → inactive → active

configure 단계에서는 파라미터를 읽고, 플러그인을 로드하고, 필요한 자원을 준비한다. inactive 상태에서는 준비는 되었지만 아직 실제 작업을 수행하지 않는다. activate가 되어야 토픽 발행, 액션 처리, costmap 업데이트 같은 핵심 기능이 본격적으로 동작한다.

이 과정을 관리하는 노드가 nav2_lifecycle_manager다. nav2_lifecycle_manager는 map server, AMCL, planner, controller, behavior server, bt navigator 같은 구성 요소를 정해진 순서로 configure하고 activate한다. 순서가 중요한 이유는 간단하다. 지도도 없고 위치 추정도 준비되지 않았는데 planner가 경로를 계산하려고 하면 안 된다. controller가 local costmap을 사용하려면 TF와 센서 입력이 준비되어 있어야 한다.

실제 launch 구성에서는 localization 쪽과 navigation 쪽 lifecycle manager가 나뉘는 경우도 많다.

localization lifecycle manager
  → map_server
  → amcl

navigation lifecycle manager
  → controller_server
  → smoother_server
  → planner_server
  → behavior_server
  → bt_navigator
  → waypoint_follower
  → velocity_smoother

이 목록은 프로젝트마다 조금 달라질 수 있다. 중요한 것은 "Nav2 서버들이 그냥 동시에 켜지는 것이 아니라, lifecycle 상태 전이를 통해 준비되고 활성화된다"는 사실이다.

만약 RViz에서 목표를 보내도 아무 일도 일어나지 않는다면 가장 먼저 lifecycle 상태를 확인해야 한다.

ros2 lifecycle get /planner_server
ros2 lifecycle get /controller_server
ros2 lifecycle get /bt_navigator

active가 아니라면 아직 출발 준비가 끝나지 않은 것이다. 이때는 파라미터 오류, 플러그인 로드 실패, TF 미연결, map 파일 경로 오류 같은 문제가 앞단에서 발생했을 가능성이 높다.

Costmap: Planner와 Controller가 바라보는 세계

Nav2의 많은 문제는 costmap을 보면 실마리가 잡힌다. costmap은 로봇이 세상을 이해하기 위해 사용하는 비용 지도다. 장애물은 높은 비용, 빈 공간은 낮은 비용, 장애물 주변의 안전 여유 공간은 중간 비용으로 표현된다.

Nav2에서 특히 중요한 것은 global costmap과 local costmap의 구분이다. global costmap은 전체 경로 계획에 사용되고, local costmap은 실제 주행 중 장애물 회피와 속도 명령 계산에 사용된다. planner_server는 global costmap을 보고 큰 경로를 만들고, controller_server는 local costmap을 보고 지금 당장 어떻게 움직일지 결정한다.

이 구분을 모르면 이상한 상황에서 헤매게 된다. 예를 들어 RViz에서 전역 경로는 멋지게 보이는데 로봇이 출발하지 못할 수 있다. 이때 global costmap만 보면 문제가 없어 보인다. 하지만 local costmap을 켜 보면 로봇 주변이 장애물 비용으로 꽉 차 있을 수 있다. LiDAR frame이 잘못되어 장애물이 로봇 몸체 위에 찍히거나, footprint가 실제보다 너무 크게 설정되었거나, inflation radius가 좁은 복도를 막아 버렸을 수 있다.

반대로 로봇 주변은 깨끗하고 수동 조종도 잘 되는데 목표까지의 경로가 생성되지 않을 수 있다. 이때는 local costmap보다 global costmap, static layer, map server, AMCL 위치, map → odom → base_link TF 연결을 먼저 보아야 한다.

Costmap은 다음 장에서 더 자세히 다룬다. 지금은 Nav2 구조 안에서 costmap이 어디에 붙어 있는지만 분명히 기억하자. planner의 눈은 global costmap이고, controller의 눈은 local costmap이다.

Nav2는 강력하지만 마법은 아니다. 입력이 어긋나면 아무리 좋은 planner와 controller를 써도 제대로 움직일 수 없다. 공식 Nav2 문서에서도 Nav2의 기대 입력으로 REP-105에 맞는 TF 변환, static costmap layer를 사용할 경우 map source, Behavior Tree XML 파일, 관련 센서 데이터 소스를 언급한다7.

실제 프로젝트에서는 다음 연결이 일관되어야 한다.

  • map_server 또는 SLAM이 /map을 제공해야 한다.

  • AMCL, SLAM, 외부 위치 추정 시스템 중 하나가 로봇의 위치를 추정해야 한다.

  • map → odom → base_link로 이어지는 TF 트리가 끊기지 않아야 한다.

  • LiDAR, 카메라, depth sensor 같은 센서 데이터의 frame_id가 TF 트리 안에 있어야 한다.

  • global costmap과 local costmap의 frame, topic, layer 설정이 실제 로봇 구성과 맞아야 한다.

  • controller가 만든 속도 명령이 실제 모터 드라이버가 구독하는 토픽까지 이어져야 한다.

여기서 하나라도 어긋나면 증상은 다양하게 나타난다. 지도는 보이는데 로봇 모델이 엉뚱한 곳에 있을 수 있다. 로봇 위치는 맞는데 경로가 생성되지 않을 수 있다. 경로는 생성되는데 /cmd_vel이 나오지 않을 수 있다. /cmd_vel은 나오는데 실제 바퀴가 움직이지 않을 수도 있다.

그래서 Nav2 디버깅은 "Nav2가 안 된다"에서 시작하면 안 된다. 대신 다음처럼 질문을 쪼개야 한다.

지도는 있는가?
현재 위치는 추정되는가?
TF는 이어져 있는가?
global costmap은 정상인가?
전역 경로는 생성되는가?
local costmap은 정상인가?
속도 명령은 발행되는가?
모터 드라이버가 그 명령을 받는가?

이 질문들은 앞 장들에서 배운 ROS 2 기본기와 그대로 연결된다. ros2 topic list, ros2 topic echo, ros2 node info, ros2 action list, tf2_echo, RViz의 TF와 Costmap 표시가 모두 다시 등장한다. ROS 2를 따로 배우고 Nav2를 따로 배우는 것이 아니다. Nav2는 지금까지 배운 도구들이 한꺼번에 모인 종합 무대다.

pluginlib: Nav2를 바꿔 끼울 수 있게 만드는 힘

Nav2를 처음 배울 때는 기본 설정을 그대로 사용하는 것이 좋다. TurtleBot3 예제라면 기본 planner와 controller만으로도 목표 지점까지 이동하는 경험을 할 수 있다. 하지만 실제 로봇 프로젝트로 가면 곧 이런 질문을 만나게 된다.

"우리 로봇은 제자리 회전이 안 되는데 이 planner를 써도 될까?"

"좁은 복도에서 너무 벽에 붙는데 controller를 바꿔야 할까?"

"동적 장애물이 많은 환경에서는 더 부드럽게 회피하고 싶다."

"대형 로봇이라 회전 반경을 고려한 경로가 필요하다."

Nav2는 이런 차이를 플러그인 구조로 해결한다. planner, controller, behavior, smoother, costmap layer, goal checker, progress checker 등 많은 구성 요소가 pluginlib 기반 플러그인으로 교체될 수 있다. 공식 Nav2 개요에서도 controller, planner, recovery 등에 여러 플러그인을 사용할 수 있으며, 이를 통해 상황에 맞는 navigation behavior를 구성할 수 있다고 설명한다8.

플러그인은 "프로그램 전체를 고치지 않고 특정 기능 구현만 바꿔 끼우는 방식"이라고 이해하면 된다. 서버는 공통된 인터페이스를 제공하고, 실제 알고리즘은 플러그인이 맡는다. 그래서 같은 planner_server라도 어떤 플러그인을 로드하느냐에 따라 동작 방식이 달라진다.

차동 구동 로봇은 왼쪽 바퀴와 오른쪽 바퀴 속도 차이로 움직이며 제자리 회전이 가능하다. TurtleBot 계열이 여기에 가깝다. Holonomic 로봇은 메카넘 휠이나 옴니휠을 사용해 옆으로도 움직일 수 있다. Ackermann 로봇은 자동차처럼 조향각과 회전 반경의 제약이 강하다. 이 세 로봇에 같은 controller와 같은 속도 제한을 적용하면 좋은 결과를 기대하기 어렵다.

Nav2가 다양한 이동 로봇을 지원할 수 있는 이유는 이처럼 서버의 역할과 알고리즘 구현을 분리했기 때문이다. Nav2 문서는 holonomic, non-holonomic, differential-drive, legged, ackermann 같은 다양한 로봇 유형에 대해 적절한 설정을 통해 속도 명령을 제공할 수 있다고 설명한다9.

즉 Nav2를 잘 쓴다는 것은 단순히 "Nav2를 켠다"가 아니다. 내 로봇의 구동 방식, 센서 구성, 환경 조건에 맞는 플러그인과 파라미터를 고르는 일이다.

설정 파일을 읽는 새로운 눈

이제 nav2_params.yaml의 일부를 다시 보자. 예전에는 길고 복잡한 숫자 목록처럼 보였을 것이다.

bt_navigator:
  ros__parameters:
    default_nav_to_pose_bt_xml: "navigate_to_pose_w_replanning_and_recovery.xml"

planner_server:
  ros__parameters:
    planner_plugins: ["GridBased"]
    GridBased:
      plugin: "nav2_navfn_planner/NavfnPlanner"

controller_server:
  ros__parameters:
    controller_plugins: ["FollowPath"]
    FollowPath:
      plugin: "dwb_core::DWBLocalPlanner"

global_costmap:
  global_costmap:
    ros__parameters:
      global_frame: map
      robot_base_frame: base_link
      plugins: ["static_layer", "obstacle_layer", "inflation_layer"]

local_costmap:
  local_costmap:
    ros__parameters:
      global_frame: odom
      robot_base_frame: base_link
      rolling_window: true
      plugins: ["obstacle_layer", "inflation_layer"]

이제 이 파일은 조금 다르게 보인다. bt_navigator는 어떤 Behavior Tree XML을 실행할지 정한다. planner_server는 어떤 전역 planner 플러그인을 쓸지 정한다. controller_server는 어떤 경로 추종 controller를 쓸지 정한다. global costmap은 map 프레임을 기준으로 전체 지도 위의 비용을 만들고, local costmap은 보통 odom 프레임을 기준으로 로봇 주변의 rolling window를 만든다.

설정 파일은 주문서다. "이 로봇은 이런 지도와 이런 센서를 쓰고, 이런 방식으로 경로를 만들며, 이런 controller로 따라가고, 실패하면 이런 행동을 하라"라고 적어 둔 주문서다. Nav2 서버들은 이 주문서를 읽고 각자의 역할을 수행한다.

문제가 생겼을 때 어디를 볼 것인가

Nav2를 공부하는 가장 현실적인 이유는 문제를 빨리 좁히기 위해서다. 로봇은 언젠가 반드시 멈춘다. 중요한 것은 멈추지 않는 로봇을 상상하는 것이 아니라, 멈췄을 때 어디를 볼지 아는 것이다.

경로가 아예 생성되지 않는다면 planner_server, global costmap, map, AMCL 위치, TF를 확인한다. 목표가 장애물 안에 찍혔거나, 현재 위치가 지도 밖으로 추정되었거나, map → odom 변환이 없을 수 있다.

경로는 생성되는데 로봇이 움직이지 않는다면 controller_server, local costmap, velocity command 체인을 확인한다. /cmd_vel이 발행되는지, velocity smoother나 collision monitor가 명령을 막고 있지는 않은지, 모터 드라이버가 다른 토픽을 구독하고 있지는 않은지 살펴본다.

로봇이 조금 움직이다가 계속 복구 행동만 반복한다면 Behavior Tree와 behavior server 로그를 본다. progress checker가 너무 엄격한지, local costmap이 자주 막히는지, controller가 경로를 따라가지 못한다고 실패를 반환하는지 확인한다.

Nav2가 시작조차 되지 않는다면 lifecycle 상태와 launch 로그를 먼저 본다. 플러그인 이름이 틀렸거나, YAML 들여쓰기가 잘못되었거나, map 파일 경로가 틀렸거나, 특정 서버가 configure 단계에서 실패했을 수 있다.

이런 식으로 문제를 나누면 Nav2는 더 이상 검은 상자가 아니다. 여러 서버와 데이터 흐름으로 이루어진 투명한 시스템이 된다.

마지막으로 Nav2를 하나의 장면으로 상상해 보자. 로봇은 건물 1층 복도에 서 있다. 사용자가 RViz에서 회의실 앞을 목표로 찍는다.

bt_navigator가 목표를 받는다. Behavior Tree는 먼저 planner를 호출하라고 말한다. planner_server는 global costmap을 펼쳐 놓고 현재 위치에서 회의실 앞까지 이어지는 큰 경로를 만든다. 필요하다면 smoother_server가 그 경로를 더 부드럽게 다듬는다. 이제 controller_server가 local costmap을 보면서 경로를 따라가기 위한 속도 명령을 계속 계산한다. 복도 중간에 사람이 나타나면 local costmap에 장애물이 반영되고 controller는 속도를 줄이거나 돌아가려 한다. 진행이 막히면 Behavior Tree는 실패를 감지하고, costmap을 지우거나 behavior_server의 회전·후진·대기 행동을 호출한다. 그래도 안 되면 다시 계획하고, 목표가 바뀌면 새 목표에 맞춰 흐름을 바꾼다.

이 모든 구성 요소는 lifecycle manager에 의해 준비되고 활성화된다. 그리고 그 아래에는 지도, 위치 추정, TF, 센서, costmap, 속도 명령이 촘촘하게 연결되어 있다.

Nav2는 단순한 "목표 이동 패키지"가 아니다. 여러 서버, 액션, Behavior Tree, Lifecycle Node, 플러그인, costmap이 결합된 내비게이션 프레임워크다. 이 구조를 이해하면 이제부터 우리는 Nav2를 복사해서 실행하는 사용자가 아니라, 원인을 추적하고 구조를 조정하는 개발자로 들어서게 된다.

다음 장에서는 이 구조의 핵심 감각기관인 costmap을 더 깊게 들여다본다. 로봇이 무엇을 장애물로 보고, 얼마나 멀리 피하려 하며, 왜 좁은 문 앞에서 망설이는지 costmap을 통해 해석해 볼 것이다.

참고

1Nav2 공식 문서 Overview

2Nav2 Behavior Trees

3Nav2 공식 문서 Overview

4Nav2 Behavior Trees

5Detailed Behavior Tree Walkthrough

6nav2_behavior_tree 패키지 설명

7Nav2 공식 문서 Overview

8Nav2 공식 문서 Overview

9Nav2 공식 문서 Overview

책 목록

ROS2로 완성하는 자율주행 로봇: 노드·TF·SLAM·Nav2 실전 설계

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기