[ROS2 Mastery 6편] Topic 통신을 활용한 Turtlesim 자율주행 구현 가이드
안녕하세요! 자율주행과 로봇 시스템 제어를 심도 있게 연구하시는 대학생 및 연구원 여러분. 쿼드(QUAD) 드론연구소의 자료를 바탕으로 진행되는 ‘ROS2 속성 마스터 과정’ 연재 블로그 6편에 오신 것을 환영합니다.
지난 4편과 5편에서는 ROS2 통신의 핵심인 발행자(Publisher)와 구독자(Subscriber) 노드를 각각 분리하여 학습했습니다. 4편에서는 거북이에게 맹목적으로 이동 명령(cmd_vel)을 내리는 개방형 루프(Open-loop) 제어를 해보았고, 5편에서는 거북이의 현재 위치 정보(pose)를 수동적으로 수신만 해보았습니다.
이번 6편은 기초 ROS2 통신 프로그래밍의 ‘꽃’이라고 할 수 있는 시간입니다. 바로 앞서 배운 Publisher와 Subscriber를 하나의 노드 안에 결합하여, 거북이가 화면의 벽(펜스)을 스스로 인식하고 부딪히기 전에 방향을 틀어 회피하는 ‘자율주행(Autonomous Driving)’ 알고리즘을 직접 구현해 볼 것입니다.
단순한 원격 조종을 넘어 로봇이 스스로 판단하여 움직이는 ‘폐루프 제어(Closed-loop Control)’의 기본 원리를 확실하게 다져보겠습니다.

자율주행 알고리즘 설계: 로봇의 인지, 판단, 제어
로봇 공학에서 자율주행 시스템을 설계할 때 가장 기본이 되는 피드백 루프(Feedback Loop)는 다음과 같습니다.
- 인지 (Sense): 센서를 통해 현재 로봇의 상태와 주변 환경 정보를 수집합니다.
- 판단 (Compute/Plan): 수집된 데이터를 바탕으로 다음 행동을 계산합니다.
- 제어 (Act): 계산된 명령을 모터(액추에이터)에 전달하여 로봇을 움직입니다.
이 원리를 우리의 거북이 시뮬레이터(Turtlesim)에 그대로 적용해 보겠습니다.
- 인지 (Sense): 거북이의 실시간 x, y 좌표를 알려주는
/turtle1/pose토픽을 구독(Subscribe)하여 현재 위치를 파악합니다. - 판단 (Compute): 거북이가 움직이는 흰색 펜스(벽)의 한계 좌표를 파악합니다. Turtlesim의 화면은 x, y 축 모두 약
0.0에서11.0사이의 좌표계를 가집니다. 따라서 거북이의 위치가x < 2.0이거나x > 9.0, 혹은y < 2.0이거나y > 9.0과 같이 벽에 일정 수준 근접했는지 조건문으로 검사합니다. - 제어 (Act): 벽에 근접했다면 회전 명령(angular)을 내리고, 안전한 구역이라면 직진 명령(linear)을 내리는 메시지를 생성하여
/turtle1/cmd_vel토픽으로 발행(Publish)합니다.
이제 목표와 알고리즘이 명확해졌습니다. 본격적인 코딩에 들어가 보겠습니다.
자율주행 노드 코드 작성: turtle_controller.py
우리가 계속해서 작업하고 있는 my_robot_controller 패키지의 소스 코드 디렉토리로 이동하여 새로운 파이썬 소스 파일을 생성하고, 시스템이 실행할 수 있도록 권한을 부여해 줍니다.
# 노드 소스 코드가 있는 디렉토리로 이동
cd ~/ros2_ws/src/my_robot_controller/my_robot_controller
# 파일 생성 및 실행 권한(x) 부여
touch turtle_controller.py
chmod +x turtle_controller.py
VS Code와 같은 에디터를 열어 turtle_controller.py 파일에 다음 코드를 작성합니다. 여러분의 이해를 돕기 위해 상세한 주석을 포함하였습니다.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist # 로봇을 제어할 메시지 타입
from turtlesim.msg import Pose # 로봇의 위치를 수신할 메시지 타입
class TurtleControllerNode(Node):
def __init__(self):
super().__init__('turtle_controller')
# 1. 제어 명령을 내릴 Publisher 생성
self.cmd_vel_publisher_ = self.create_publisher(
Twist,
'/turtle1/cmd_vel',
10
)
# 2. 위치 정보를 수신할 Subscriber 생성 (수신 시 pose_callback 실행)
self.pose_subscriber_ = self.create_subscription(
Pose,
'/turtle1/pose',
self.pose_callback,
10
)
self.get_logger().info("거북이 자율주행 컨트롤러가 시작되었습니다! (충돌 회피 모드)")
# 3. 콜백 함수: 위치 정보가 수신될 때마다 자동 실행되는 판단 및 제어 로직
def pose_callback(self, pose: Pose):
cmd = Twist()
# Turtlesim 화면의 벽(펜스) 범위는 대략 0 ~ 11.0 입니다.
# 거북이가 벽에 근접했는지(x 또는 y 좌표가 너무 작거나 크지 않은지) 검사합니다.
if pose.x > 9.0 or pose.x < 2.0 or pose.y > 9.0 or pose.y < 2.0:
# 벽에 가까워지면 직진 속도를 줄이고 강하게 회전합니다.
cmd.linear.x = 1.0
cmd.angular.z = 1.5
self.get_logger().info("경고: 벽 접근 감지! 방향을 전환합니다.")
else:
# 안전한 중앙 구역에서는 빠르게 직진합니다.
cmd.linear.x = 5.0
cmd.angular.z = 0.0
# 결정된 행동(Twist 메시지)을 모터(cmd_vel 토픽)로 발행(Publish)합니다.
self.cmd_vel_publisher_.publish(cmd)
def main(args=None):
rclpy.init(args=args)
node = TurtleControllerNode()
try:
# 노드를 실행 상태로 유지하며 콜백 함수를 대기시킵니다.
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
💡[연구원을 위한 코드 심층 분석]
이번 노드는 ROS2 프로그래밍의 ‘이벤트 구동(Event-Driven)’ 방식의 정수를 보여줍니다. 이전 발행자 예제에서는 Timer를 이용해 능동적으로 주기마다 데이터를 발행했지만, 이번 자율주행 컨트롤러는 별도의 타이머가 없습니다.
대신 구독자(Subscriber)가 데이터를 수신하는 순간 발생하는 콜백 이벤트(pose_callback)에 의존합니다. /turtle1/pose 토픽은 약 60Hz(초당 60번)의 빠른 속도로 수신되는데, 데이터가 들어올 때마다 콜백 함수가 깨어나 거북이의 위치(pose.x, pose.y)를 판단하고, 회피 기동 또는 직진 기동 값을 즉시 발행(publish(cmd))하는 피드백 루프를 형성하고 있습니다. 이처럼 센서 입력과 제어 출력이 긴밀하게 연결되는 구조가 실제 자율주행 소프트웨어의 핵심 뼈대입니다.
설정 파일(setup.py) 업데이트 및 진입점 등록
노드 코드 작성을 완료했다면, 시스템이 이 스크립트를 인식하고 실행할 수 있도록 setup.py에 실행 진입점(Entry point)을 등록해야 합니다.
패키지의 최상단 경로(~/ros2_ws/src/my_robot_controller)에 있는 setup.py 파일을 열고, entry_points 영역에 우리가 방금 만든 자율주행 노드를 추가해 줍니다.
entry_points={
'console_scripts': [
'test_node = my_robot_controller.my_first_node:main',
'draw_circle = my_robot_controller.draw_circle:main',
'pose_subscriber = my_robot_controller.pose_subscriber:main',
# 방금 작성한 자율주행 노드의 진입점을 추가합니다.
'turtle_controller = my_robot_controller.turtle_controller:main'
],
},
패키지 빌드 및 자율주행 테스트
모든 준비가 끝났습니다! 이제 수정한 패키지를 빌드하고 대망의 자율주행 테스트를 진행해 보겠습니다.
터미널을 열고 작업 공간의 루트 디렉토리로 이동하여 빌드를 수행합니다.
# 작업 공간 루트로 이동
cd ~/ros2_ws
# 패키지 빌드
colcon build --symlink-install
에러 없이 빌드가 완료되었다면, 두 개의 터미널을 열어 시스템을 구동합니다.
💡[실행 단계]
- 첫 번째 터미널 (시뮬레이터 실행):
- 두 번째 터미널 (자율주행 컨트롤러 실행): 새 터미널을 열고 우리가 수정한 오버레이 환경을 소싱한 뒤, 자율주행 노드를 실행합니다.
성공입니다! 컨트롤러 노드가 실행되자마자 거북이가 앞으로 빠르게 전진합니다. 그리고 화면 가장자리의 보이지 않는 경계선(x < 2.0 또는 x > 9.0 등)에 닿을 때마다, 우리가 심어둔 로직에 따라 거북이가 안전 구역을 향해 스스로 회전하며 펜스 밖으로 튕겨 나가지 않고 영구적으로 자율주행을 수행하는 것을 볼 수 있습니다.
RQT Graph로 폐루프(Closed-loop) 통신망 시각화 점검
현재 동작하고 있는 이 훌륭한 시스템의 내부 통신 구조는 어떻게 이루어져 있을까요? 세 번째 터미널을 열고 앞서 배운 rqt_graph 도구를 실행해 봅니다.
rqt_graph
그래프를 확인해 보면 감탄이 절로 나옵니다!
/turtlesim노드에서 출발한 실시간 센서 데이터가/turtle1/pose토픽을 타고/turtle_controller노드로 흘러 들어갑니다. (인지)/turtle_controller노드는 이를 판단하여 제어 명령을 생성한 뒤,/turtle1/cmd_vel토픽을 통해 다시/turtlesim노드로 피드백 명령을 되돌려 보냅니다. (제어)
이처럼 데이터가 순환하는 완벽한 폐루프 제어(Closed-loop control) 시스템이 구축된 것을 시각적으로 명확히 확인할 수 있습니다. 로봇 연구를 진행하실 때 rqt_graph 상에서 이러한 순환 고리가 제대로 형성되어 있는지를 확인하는 것은 매우 중요한 디버깅 습관입니다.
마무리하며
이번 6편에서는 우리가 앞서 개별적으로 배웠던 ‘토픽 발행(Publish)’과 ‘토픽 구독(Subscribe)’을 융합하여, 로봇의 인지-판단-제어 프로세스를 구현하는 자율주행 노드를 성공적으로 완성해 보았습니다.
벽을 회피하는 이 간단해 보이는 코드 안에는 현대 자율주행 자동차와 로봇 팔을 움직이는 핵심적인 피드백 루프 원리가 고스란히 담겨 있습니다. 여기서 조금만 더 알고리즘을 응용하면, 라이다(LiDAR) 센서 데이터를 수신하여 실제 장애물을 회피하거나, 카메라 영상을 분석하여 차선을 따라가는 복잡한 로봇 소프트웨어로 발전하게 됩니다.
다음 [ROS2 Mastery 7편]에서는 일방향 또는 양방향 스트리밍 방식인 Topic 통신을 넘어서, 클라이언트가 요청(Request)할 때만 서버가 처리하여 응답(Response)을 돌려주는 “ROS2 Service 프로그래밍”에 대해 깊이 있게 알아보겠습니다. 거북이가 움직이는 경로의 펜 색상을 실시간으로 바꿔보는 재미있는 서비스 데모가 기다리고 있으니 다음 연재도 많은 기대 부탁드립니다!
연구원 및 대학생 여러분의 열정적인 자율주행 연구를 항상 응원합니다. 감사합니다!
YouTube Tutorial

작성자: 에이든(Aiden), 쿼드(QUAD) 드론연구소 마케팅팀
기고일: 2026.05.17
