ROS2의 디버깅 모드 개요

ROS2에서 디버깅 모드는 프로그램 실행 중 발생하는 오류, 상태 변화, 성능 저하 등을 추적하고 분석하는 데 매우 유용하다. 이를 통해 ROS2 시스템의 다양한 요소들이 어떻게 상호작용하는지, 그리고 예상치 못한 문제가 발생했을 때 그 원인을 파악하는 데 중요한 역할을 한다.

디버깅 모드는 다양한 툴과 로그 시스템을 활용하여, 실시간으로 시스템 상태를 확인하고 문제를 해결하는 과정을 지원한다. 주요 툴로는 gdb, valgrind, ros2 topic, ros2 service, rqt_console, rqt_logger_level 등이 있다.

gdb를 통한 노드 디버깅

일반적으로 ROS2 노드는 gdb와 같은 디버깅 툴을 이용하여 디버깅할 수 있다. gdb는 프로그램의 실행을 제어하고, 중단점 설정, 변수 상태 확인, 코드 실행 흐름 추적 등을 할 수 있는 강력한 도구이다.

gdb 디버깅 예시

  1. ROS2 노드를 디버깅 모드로 실행하기 위해서는 먼저 컴파일 단계에서 디버깅 심볼을 포함시켜야 한다. 이를 위해 CMakeLists.txt 파일에서 아래와 같은 설정을 추가한다.
set(CMAKE_BUILD_TYPE Debug)
  1. 다음으로, 노드를 gdb를 사용해 실행한다.
gdb --args ros2 run <패키지명> <노드명>
  1. gdb 실행 후, 노드 실행을 시작하고, 중단점을 설정하여 특정 함수나 변수 상태를 확인할 수 있다.
(gdb) break <함수명>
(gdb) run
  1. 중단점에 도달하면, 특정 변수 값을 확인할 수 있다.
(gdb) print <변수명>

이러한 방식으로 gdb를 통해 노드가 실행되는 동안 발생하는 오류나 비정상적인 동작을 상세히 파악할 수 있다.

ros2 topic 명령을 통한 메시지 디버깅

ROS2의 주요 통신 방식인 토픽 메시지는 ros2 topic 명령을 통해 쉽게 디버깅할 수 있다. 이 명령어는 현재 실행 중인 노드에서 송수신되는 토픽 메시지를 실시간으로 확인하고, 메시지 형식을 파악하며, 메시지 전송 주기나 데이터의 유효성을 검증하는 데 사용된다.

ros2 topic 디버깅 예시

  1. 현재 실행 중인 모든 토픽 리스트를 확인하기 위해 다음 명령을 사용한다.
ros2 topic list
  1. 특정 토픽의 메시지를 실시간으로 확인하려면 다음 명령을 사용한다.
ros2 topic echo <토픽명>
  1. 메시지의 유형을 확인하려면 다음 명령을 사용한다.
ros2 topic type <토픽명>
  1. 메시지 주기 등을 확인하기 위해서는 아래 명령을 사용할 수 있다.
ros2 topic hz <토픽명>

이와 같은 명령을 사용하여 송수신되는 메시지의 상태를 실시간으로 확인하고, 예상치 못한 데이터가 송수신되는지 여부를 쉽게 파악할 수 있다.

ros2 service 명령을 통한 서비스 디버깅

ROS2 서비스는 요청-응답 구조로 이루어진 통신 방식이다. ros2 service 명령을 통해 서비스 호출, 서비스 목록 확인, 그리고 서비스 응답 상태를 확인할 수 있다. 이는 서비스가 정상적으로 동작하지 않을 때, 그 문제를 찾아내고 해결하는 데 유용하다.

ros2 service 디버깅 예시

  1. 현재 사용 가능한 모든 서비스 목록을 확인하려면 다음 명령을 사용한다.
ros2 service list
  1. 특정 서비스의 타입을 확인하려면 다음 명령을 사용한다.
ros2 service type <서비스명>
  1. 서비스의 요청과 응답 메시지를 확인하려면 ros2 service call 명령을 통해 직접 호출을 시도할 수 있다.
ros2 service call <서비스명> <서비스타입> "{data}"

예를 들어, add_two_ints라는 서비스를 호출할 때, 다음과 같이 호출할 수 있다.

ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 2, b: 3}"

이 명령어는 서비스가 제대로 작동하고 있는지, 요청에 대한 응답이 올바르게 이루어지는지 확인할 수 있는 방법을 제공한다.

rqt_console 및 rqt_logger_level을 이용한 로그 분석

rqt_consolerqt_logger_level은 ROS2 노드에서 발생하는 로그 메시지를 실시간으로 확인하고, 로그 레벨을 조정하여 디버깅할 수 있는 GUI 툴이다. 이를 통해 시스템 전반에서 발생하는 이벤트나 오류를 직관적으로 확인할 수 있다.

rqt_console 활용 방법

  1. rqt_console는 터미널에서 다음 명령을 통해 실행할 수 있다.
rqt_console
  1. 실행된 GUI에서 발생하는 로그 메시지를 실시간으로 확인할 수 있다. 로그 레벨을 필터링하여 특정 중요도 이상의 메시지만 볼 수 있다.

rqt_logger_level을 통한 로그 레벨 변경

  1. rqt_logger_level은 특정 노드의 로그 레벨을 동적으로 변경할 수 있다. 이를 통해 디버깅이 필요한 상황에서는 로그 레벨을 세밀하게 조정하여 더 많은 디버깅 정보를 출력할 수 있다.

  2. 터미널에서 다음 명령을 사용해 rqt_logger_level을 실행한다.

rqt_logger_level
  1. 실행 후, 로그 레벨을 변경하고자 하는 노드를 선택한 뒤, 해당 노드의 로그 레벨을 조정할 수 있다. 일반적으로 DEBUG, INFO, WARN, ERROR, FATAL의 레벨을 지원한다.

이와 같은 GUI 도구를 사용하면, 명령어 기반의 디버깅 도구보다 직관적으로 문제를 파악하고 로그 레벨을 쉽게 조정할 수 있어, 디버깅이 수월해진다.

Valgrind를 통한 메모리 누수 디버깅

메모리 관리가 중요한 시스템에서는 Valgrind를 통해 메모리 누수를 추적하고, 성능 병목을 찾아낼 수 있다. Valgrind는 C++로 작성된 ROS2 노드를 디버깅할 때 특히 유용하다.

Valgrind 디버깅 예시

  1. Valgrind를 설치한 후, ROS2 노드를 다음 명령으로 실행할 수 있다.
valgrind --leak-check=yes ros2 run <패키지명> <노드명>
  1. 이 명령을 실행하면, 노드 실행 중 발생하는 메모리 누수 정보가 출력된다. 이를 통해 특정 코드가 메모리를 적절히 해제하지 않았는지 여부를 확인할 수 있다.
==12345== 32 bytes in 1 blocks are definitely lost in loss record 56 of 78
  1. 이러한 메모리 누수 로그를 기반으로 문제를 추적하여 코드를 수정할 수 있다.

메모리 관리 문제는 성능 저하나 시스템 불안정성으로 이어질 수 있으므로, Valgrind를 활용한 디버깅은 매우 중요하다.

rcl_logging을 통한 커스텀 로그 분석

rcl_logging은 ROS2에서 로그를 생성하고 관리하는 기능을 제공한다. 개발자는 커스텀 로그 메시지를 추가하여 특정한 조건에서만 로그를 기록하거나, 성능 이슈가 발생했을 때 로그를 활용하여 문제를 진단할 수 있다. 로그 레벨은 DEBUG, INFO, WARN, ERROR, FATAL의 다섯 가지로 구분된다.

rcl_logging 사용 방법

  1. 로그를 추가하고 싶은 위치에 RCLCPP 매크로를 사용하여 로그 메시지를 삽입할 수 있다. 예를 들어, 특정 변수가 올바르게 설정되었는지 확인하기 위한 INFO 로그를 추가할 수 있다.
RCLCPP_INFO(this->get_logger(), "현재 변수 값: %d", 변수명);
  1. 로그 레벨을 동적으로 변경하기 위해서는 rclcpp::Logger 객체를 활용할 수 있다. 이를 통해 특정 노드에 대한 로그 레벨을 실행 중에 변경할 수 있다.
auto logger = rclcpp::get_logger("example_logger");
logger.set_level(rclcpp::Logger::Level::Debug);
  1. 추가된 로그는 노드가 실행되는 동안 rqt_console 혹은 터미널에서 실시간으로 확인할 수 있다. 예를 들어, DEBUG 레벨의 로그를 추가하면 개발 과정에서 필요한 디버깅 정보를 상세히 확인할 수 있다.

디버깅 모드에서 발생할 수 있는 문제와 해결 방안

디버깅 모드를 활성화한 상태에서 발생할 수 있는 문제를 해결하기 위해서는 다음과 같은 전략을 사용할 수 있다.

  1. 중단점이 도달하지 않는 문제
    gdb로 디버깅하는 과정에서 중단점이 도달하지 않는 경우, 프로그램 흐름을 다시 확인하고, 중단점을 설정한 함수나 코드가 호출되는지 검토해야 한다. 또한, 최적화 옵션이 켜져 있을 경우, 최적화에 의해 코드가 재배치되어 중단점이 정상적으로 동작하지 않을 수 있다. 이를 해결하려면 디버깅 심볼이 포함된 빌드 옵션으로 다시 빌드해야 한다.

  2. 메모리 누수 문제
    Valgrind로 발견된 메모리 누수는 종종 동적 메모리를 할당한 후 해제하지 않아서 발생한다. 이러한 문제는 객체의 생명주기와 메모리 해제 과정을 면밀히 검토함으로써 해결할 수 있다. 또한, C++의 스마트 포인터(std::shared_ptr, std::unique_ptr)를 적극적으로 활용하는 것이 메모리 관리를 수월하게 한다.

  3. 로그 레벨이 기대대로 출력되지 않는 문제
    로그 레벨이 낮게 설정되어 있을 경우, INFO 이하의 로그가 출력되지 않을 수 있다. 이때는 rclcpp::Logger 객체를 통해 실행 중에 로그 레벨을 조정할 수 있으며, rqt_logger_level을 사용해 해당 노드의 로그 레벨을 상위 수준으로 변경하는 것이 필요하다.

  4. 서비스 호출 시 응답이 없는 문제
    서비스가 비동기 방식으로 동작하는 경우, 응답이 늦게 오거나 아예 오지 않을 수 있다. 이 문제는 서비스 콜백 함수의 처리 시간이 너무 길거나, 네트워크 지연이 발생할 때 종종 나타난다. 이를 해결하기 위해서는 콜백 함수 내의 연산을 최적화하거나, 별도의 쓰레드를 사용해 비동기 응답을 처리하는 방식으로 개선할 수 있다.

디버깅 사례: 서비스의 비동기 호출 문제 해결

다음은 비동기 서비스 호출에서 발생한 응답 지연 문제를 해결한 사례이다. 비동기 호출 중 콜백 함수가 너무 많은 작업을 처리하면서 서비스 응답이 늦어졌을 때, 이를 해결하기 위해 콜백을 멀티스레딩 방식으로 분리하였다.

문제 상황

서비스의 요청을 처리하는 콜백 함수가 복잡한 연산을 포함하고 있어, 서비스 응답이 지연되는 문제가 발생하였다. 아래는 비동기 서비스 호출에서 응답 지연을 발생시키는 예시이다.

void handle_request(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
                    std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
    // 복잡한 연산을 수행
    long_processing_task();

    response->sum = request->a + request->b;
}

문제 해결

해결책으로, std::thread를 사용하여 연산을 비동기적으로 처리하고, 서비스 응답 시간을 개선하였다.

void handle_request(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
                    std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
    std::thread([request, response]() {
        long_processing_task();
        response->sum = request->a + request->b;
    }).detach();
}

이와 같은 방식으로 연산을 별도의 쓰레드에서 처리함으로써, 서비스 응답 지연 문제를 해결할 수 있었다.