앞에서 만든 Action Server는 Goal을 받고 Feedback을 발행한 뒤 성공 상태로 종료됩니다.
하지만 아직 중요한 기능이 하나 빠져 있습니다.
바로 Cancel 처리입니다.
현재 코드는 Client가 작업 취소 요청을 보내도 서버가 이를 확인하지 않습니다.
즉, 작업이 시작되면 중간에 멈출 수 없습니다.
실제 로봇 시스템에서는 이것이 꽤 위험한 구조입니다.
예를 들어 다음과 같은 상황을 생각해 볼 수 있습니다.
| 거북이가 목표 거리만큼 이동 중 | 사용자가 중간에 멈추고 싶을 수 있음 |
| 모바일 로봇이 목적지로 이동 중 | 장애물이 나타나면 즉시 중단해야 함 |
| 드론이 Waypoint로 이동 중 | GPS 오차, 배터리 부족, 비상 상황에서 취소 필요 |
| 로봇팔이 Pick & Place 중 | 충돌 위험이 있으면 동작 중단 필요 |
| 자동 도킹 중 | 도킹 실패 가능성이 보이면 취소 필요 |
Action은 단순히 오래 걸리는 작업을 실행하는 구조가 아닙니다.
진행 중인 작업을 감시하고, 필요하면 취소할 수 있는 구조까지 포함합니다.
그래서 ROS 2 Action을 제대로 이해하려면 Feedback뿐 아니라 Cancel 처리도 반드시 알아야 합니다.
1. Cancel 기능이 없는 기존 코드의 문제
기존 코드는 다음과 같은 구조입니다.
for n in range(10):
feedback_msg.remained_dist = float(10-n)
goal_handle.publish_feedback(feedback_msg)
time.sleep(0.5)
goal_handle.succeed()
이 코드는 10번 반복하면서 Feedback을 발행합니다.
하지만 반복 중간에 Client가 Cancel 요청을 보내도 서버는 아무 반응을 하지 않습니다.
왜냐하면 코드 안에서 다음 상태를 확인하지 않기 때문입니다.
goal_handle.is_cancel_requested
Action Server는 Cancel 요청이 들어왔는지 직접 확인해야 합니다.
Cancel 요청이 들어왔다고 해서 Python 코드가 자동으로 멈추지는 않습니다.
즉, 서버 코드 안에서 다음과 같은 처리가 필요합니다.
if goal_handle.is_cancel_requested:
goal_handle.canceled()
return result
이 구조를 넣어야 Client가 보낸 Cancel 요청을 서버가 정상적으로 받아들일 수 있습니다.
2. Cancel 처리가 추가된 Action Server 코드
아래는 Feedback 발행 코드에 Cancel 처리를 추가한 예제입니다.
import rclpy as rp
from rclpy.action import ActionServer, CancelResponse
from rclpy.node import Node
import time
from my_first_package_msgs.action import DistTurtle
class DistTurtleServer(Node):
def __init__(self):
super().__init__('dist_turtle_action_server')
self._action_server = ActionServer(
self,
DistTurtle,
'dist_turtle',
self.execute_callback,
cancel_callback=self.cancel_callback
)
def cancel_callback(self, goal_handle):
self.get_logger().info('Cancel request received')
return CancelResponse.ACCEPT
def execute_callback(self, goal_handle):
self.get_logger().info('Goal received')
feedback_msg = DistTurtle.Feedback()
result = DistTurtle.Result()
for n in range(10):
if goal_handle.is_cancel_requested:
self.get_logger().info('Goal canceled')
goal_handle.canceled()
result.pos_x = 0.0
result.pos_y = 0.0
result.pos_theta = 0.0
result.result_dist = float(10 - n)
return result
feedback_msg.remained_dist = float(10 - n)
goal_handle.publish_feedback(feedback_msg)
self.get_logger().info(
f'Feedback: remained_dist = {feedback_msg.remained_dist}'
)
time.sleep(0.5)
goal_handle.succeed()
result.pos_x = 0.0
result.pos_y = 0.0
result.pos_theta = 0.0
result.result_dist = 10.0
self.get_logger().info('Goal succeeded')
return result
def main(args=None):
rp.init(args=args)
dist_turtle_action_server = DistTurtleServer()
rp.spin(dist_turtle_action_server)
dist_turtle_action_server.destroy_node()
rp.shutdown()
if __name__ == '__main__':
main()
먼저 아래의 소스 코드를 for 문 아래에 추가합니다.
if goal_handle.is_cancel_requested:
self.get_logger().info('Goal canceled')
goal_handle.canceled()
result.pos_x = 0.0
result.pos_y = 0.0
result.pos_theta = 0.0
result.result_dist = float(10 - n)
return result

goal_handle 다음 줄에 아래의 코드를 추가합니다.
self.get_logger().info(
f'Feedback: remained_dist = {feedback_msg.remained_dist}'
)

그리고 2행의 ActionServer 뒤에 CancelResponse를 추가합니다.

ActionServer 생성 부분에 cancel_callback 함수를 등록합니다.
cancel_callback=self.cancel_callback
3. 추가된 핵심 코드 분석
1) CancelResponse import
from rclpy.action import ActionServer, CancelResponse
기존에는 ActionServer만 import했습니다.
Cancel 요청을 받을지 거부할지 결정하려면 CancelResponse가 필요합니다.
CancelResponse는 Cancel 요청에 대해 서버가 어떤 응답을 할지 정합니다.
대표적으로 다음 값을 사용합니다.
CancelResponse.ACCEPT
Cancel 요청을 허용합니다.
CancelResponse.REJECT
Cancel 요청을 거부합니다.
이번 예제에서는 Cancel 요청을 받으면 항상 허용하도록 만들었습니다.
ActionServer에 cancel_callback 등록
self._action_server = ActionServer(
self,
DistTurtle,
'dist_turtle',
self.execute_callback,
cancel_callback=self.cancel_callback
)
기존 코드와 비교하면 마지막 줄이 추가되었습니다.
cancel_callback=self.cancel_callback
이 코드는 Client가 Cancel 요청을 보냈을 때 실행할 함수를 등록하는 부분입니다.
즉, Cancel 요청이 들어오면 self.cancel_callback() 함수가 호출됩니다.
2) cancel_callback 함수 추가
def cancel_callback(self, goal_handle):
self.get_logger().info('Cancel request received')
return CancelResponse.ACCEPT
이 함수는 Cancel 요청이 들어왔을 때 실행됩니다.
여기서 중요한 점은 이 함수가 실제 작업을 멈추는 함수는 아니라는 것입니다.
이 함수는 단지 다음 질문에 답하는 역할입니다.
이 Goal에 대한 Cancel 요청을 받아줄 것인가?
이번 예제에서는 다음과 같이 반환했습니다.
return CancelResponse.ACCEPT
즉, Cancel 요청을 허용합니다.
하지만 이것만으로 작업이 자동으로 멈추지는 않습니다.
실제 작업을 멈추는 코드는 execute_callback() 안에 있어야 합니다.
4. 실제 Cancel 상태 확인 코드
Cancel 처리에서 가장 중요한 부분은 다음 코드입니다.
if goal_handle.is_cancel_requested:
이 코드는 현재 실행 중인 Goal에 대해 Cancel 요청이 들어왔는지 확인합니다.
전체 구조는 다음과 같습니다.
for n in range(10):
if goal_handle.is_cancel_requested:
self.get_logger().info('Goal canceled')
goal_handle.canceled()
result.pos_x = 0.0
result.pos_y = 0.0
result.pos_theta = 0.0
result.result_dist = float(10 - n)
return result
feedback_msg.remained_dist = float(10 - n)
goal_handle.publish_feedback(feedback_msg)
time.sleep(0.5)
반복문이 실행되는 동안 매번 Cancel 요청이 들어왔는지 확인합니다.
Cancel 요청이 없으면 Feedback을 계속 발행합니다.
Cancel 요청이 있으면 다음 순서로 처리합니다.
- 로그 출력
- Goal 상태를 canceled로 변경
- Result 값 설정
- execute_callback 종료
5. goal_handle.canceled()의 의미
goal_handle.canceled()
이 코드는 현재 Goal의 최종 상태를 Canceled로 설정합니다.
Action의 최종 상태는 보통 다음 중 하나가 됩니다.
| SUCCEEDED | 작업 성공 |
| CANCELED | 작업 취소 |
| ABORTED | 작업 실패 |
기존 코드에서는 작업이 끝나면 다음 코드를 사용했습니다.
goal_handle.succeed()
이 경우 Client에는 다음과 같이 표시됩니다.
Goal finished with status: SUCCEEDED
Cancel 처리에서는 다음 코드를 사용합니다.
goal_handle.canceled()
이 경우 Client에는 다음과 같이 표시됩니다.
Goal finished with status: CANCELED
즉, Action Server가 작업을 정상적으로 취소 상태로 종료했다는 뜻입니다.
6. Cancel 시 Result를 반환해야 하는 이유
Cancel이 발생해도 execute_callback()은 반드시 Result 객체를 반환해야 합니다.
그래서 Cancel 처리 안에서도 다음 코드가 필요합니다.
result = DistTurtle.Result()
그리고 Cancel이 발생했을 때도 값을 넣고 반환합니다.
result.pos_x = 0.0
result.pos_y = 0.0
result.pos_theta = 0.0
result.result_dist = float(10 - n)
return result
이번 예제에서는 아직 실제 turtlesim 이동을 하지 않기 때문에 pos_x, pos_y, pos_theta는 모두 0.0으로 설정했습니다.
그리고 result_dist에는 현재까지 진행된 반복 횟수인 10 - n 값을 넣었습니다.
실제 이동 기능을 구현한 뒤에는 다음 값들을 넣는 것이 더 자연스럽습니다.
result.pos_x = 현재 x 위치
result.pos_y = 현재 y 위치
result.pos_theta = 현재 방향
result.result_dist = 실제 이동한 거리
즉, Cancel이 발생하더라도 “어디까지 진행했는지”를 Result로 알려줄 수 있습니다.
이게 Action의 장점입니다.
단순히 취소했다고 끝나는 것이 아니라, 취소 시점의 상태를 Client에게 전달할 수 있습니다.
7. Cancel 기능 실행하기
먼저 서버를 다시 빌드합니다.
cd ~/ros2_study
colcon build
source install/setup.bash
또는 alias를 설정해 두었다면 다음처럼 실행할 수 있습니다.
sl
그다음 Action Server를 실행합니다.
ros2 run my_first_package dist_turtle_action_server
다른 터미널에서 Goal을 보냅니다.
ros2 action send_goal --feedback /dist_turtle my_first_package_msgs/action/DistTurtle "{linear_x: 0, angular_z: 0, dist: 0}"
그러면 Feedback이 반복 출력됩니다.
Feedback:
remained_dist: 10.0
Feedback:
remained_dist: 9.0
Feedback:
remained_dist: 8.0
이 상태에서 다른 터미널을 하나 더 열고 Cancel 요청을 보냅니다.
ros2 action list
Action 이름을 확인합니다.
/dist_turtle
현재 실행 중인 Goal을 취소하려면 다음 명령을 사용합니다.
ros2 action cancel /dist_turtle
일반적인 ROS 2 CLI에서는 실행 중인 Action Goal을 직접 취소하는 다음 명령을 사용할 수 없는 경우가 많습니다.
따라서 Cancel 기능을 제대로 테스트하려면 Action Client 코드에서 직접 Cancel 요청을 보내는 방식으로 실습하는 것이 좋습니다.
최종 소스는 아래와 같습니다.
import rclpy as rp
from rclpy.action import ActionClient
from rclpy.node import Node
from my_first_package_msgs.action import DistTurtle
class DistTurtleCancelClient(Node):
def __init__(self):
super().__init__('dist_turtle_cancel_client')
self._action_client = ActionClient(
self,
DistTurtle,
'dist_turtle'
)
self.goal_handle = None
def send_goal(self):
goal_msg = DistTurtle.Goal()
goal_msg.linear_x = 1.0
goal_msg.angular_z = 0.0
goal_msg.dist = 10.0
self._action_client.wait_for_server()
self.get_logger().info('Sending goal')
self._send_goal_future = self._action_client.send_goal_async(
goal_msg,
feedback_callback=self.feedback_callback
)
self._send_goal_future.add_done_callback(
self.goal_response_callback
)
def goal_response_callback(self, future):
self.goal_handle = future.result()
if not self.goal_handle.accepted:
self.get_logger().info('Goal rejected')
return
self.get_logger().info('Goal accepted')
self._get_result_future = self.goal_handle.get_result_async()
self._get_result_future.add_done_callback(
self.get_result_callback
)
self.timer = self.create_timer(2.0, self.cancel_goal)
def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback
self.get_logger().info(
f'Received feedback: remained_dist = {feedback.remained_dist}'
)
def cancel_goal(self):
self.get_logger().info('Sending cancel request')
self.timer.cancel()
self._cancel_future = self.goal_handle.cancel_goal_async()
self._cancel_future.add_done_callback(
self.cancel_done_callback
)
def cancel_done_callback(self, future):
cancel_response = future.result()
if len(cancel_response.goals_canceling) > 0:
self.get_logger().info('Cancel request accepted')
else:
self.get_logger().info('Cancel request rejected')
def get_result_callback(self, future):
status = future.result().status
result = future.result().result
self.get_logger().info(f'Action finished with status: {status}')
self.get_logger().info(f'Result distance: {result.result_dist}')
rp.shutdown()
def main(args=None):
rp.init(args=args)
action_client = DistTurtleCancelClient()
action_client.send_goal()
rp.spin(action_client)
if __name__ == '__main__':
main()

8. Cancel 처리 흐름 정리
Cancel 처리 흐름은 다음과 같습니다.
Action Client
|
| Goal 전송
v
Action Server
|
| execute_callback 실행
| Feedback 반복 발행
|
Action Client
|
| Cancel 요청
v
Action Server
|
| cancel_callback 실행
| CancelResponse.ACCEPT 반환
|
| execute_callback 내부에서
| goal_handle.is_cancel_requested 확인
|
| goal_handle.canceled()
| Result 반환
v
Action Client
|
| Goal finished with status: CANCELED
여기서 가장 중요한 포인트는 두 가지입니다.
첫 번째는 cancel_callback입니다.
cancel_callback=self.cancel_callback
이 함수는 Cancel 요청을 받을지 말지 결정합니다.
두 번째는 execute_callback() 내부의 Cancel 확인입니다.
if goal_handle.is_cancel_requested:
이 코드는 실제 작업 중단 여부를 확인합니다.
둘 중 하나만 있으면 부족합니다.
cancel_callback만 있고 is_cancel_requested 확인이 없으면, Cancel 요청은 허용되지만 실제 작업은 계속 실행될 수 있습니다.
반대로 is_cancel_requested 확인만 있고 cancel_callback이 제대로 등록되지 않으면 Cancel 요청 자체가 기대한 방식으로 처리되지 않을 수 있습니다.
따라서 실무에서는 보통 다음 두 가지를 함께 사용합니다.
cancel_callback=self.cancel_callback
if goal_handle.is_cancel_requested:
goal_handle.canceled()
return result
9. 실습 시 자주 발생하는 오류
a. Cancel 요청을 보냈는데 작업이 계속 실행되는 경우
대부분 execute_callback() 안에 다음 코드가 없는 경우입니다.
if goal_handle.is_cancel_requested:
Cancel 요청은 서버에 들어왔지만, 작업 루프에서 이를 확인하지 않으면 작업은 계속 진행됩니다.
즉, 장시간 실행되는 반복문 안에는 반드시 Cancel 확인 코드를 넣어야 합니다.
b. goal_handle.canceled()를 호출하지 않은 경우
Cancel 요청을 확인했더라도 다음 코드를 호출하지 않으면 최종 상태가 명확하게 CANCELED로 처리되지 않습니다.
goal_handle.canceled()
Cancel을 정상 처리하려면 다음 구조를 지켜야 합니다.
if goal_handle.is_cancel_requested:
goal_handle.canceled()
return result
c. Result를 반환하지 않은 경우
execute_callback()은 항상 Result 객체를 반환해야 합니다.
아래처럼 return만 하면 안 됩니다.
return
대신 반드시 Action 정의에 맞는 Result 객체를 반환해야 합니다.
result = DistTurtle.Result()
return result
d. Cancel 확인 주기가 너무 긴 경우
예를 들어 다음과 같은 코드가 있다고 가정합니다.
time.sleep(10.0)
이 경우 Cancel 요청이 들어와도 최대 10초 동안 반응하지 않을 수 있습니다.
그래서 실제 로봇 제어 코드에서는 긴 sleep을 피하고, 짧은 주기로 반복하면서 Cancel 상태를 자주 확인하는 것이 좋습니다.
예를 들면 다음 구조가 더 안전합니다.
for n in range(100):
if goal_handle.is_cancel_requested:
goal_handle.canceled()
return result
# 제어 명령 발행
# Feedback 발행
time.sleep(0.1)
이렇게 하면 0.1초마다 Cancel 요청을 확인할 수 있습니다.
10. 실제 로봇 제어에서 Cancel이 중요한 이유
실제 로봇에서는 Cancel 처리가 단순 편의 기능이 아닙니다.
안전 기능에 가깝습니다.
예를 들어 모바일 로봇이 목표 지점으로 이동 중일 때 장애물이 갑자기 나타났다면, 기존 Goal을 취소하고 정지 명령을 내려야 합니다.
드론이 Waypoint로 이동 중 GPS 상태가 불안정해졌다면, 현재 이동 Goal을 취소하고 Hover 또는 RTL로 전환해야 합니다.
로봇팔이 물체를 집는 도중 충돌 가능성이 감지되면, Pick 동작을 취소하고 안전 위치로 이동해야 합니다.
즉, Action에서 Cancel은 다음 역할을 합니다.
| 작업 중단 | 진행 중인 Goal을 멈춤 |
| 안전 확보 | 위험 상황에서 즉시 제어 흐름 변경 |
| 상태 반환 | 취소 시점의 위치, 거리, 진행률 반환 가능 |
| 다음 작업 준비 | 기존 Goal을 정리하고 새 Goal 실행 가능 |
| 사용자 제어권 확보 | 사용자가 장시간 작업을 중간에 멈출 수 있음 |
특히 드론, 모바일 로봇, 로봇팔처럼 실제 물리적인 움직임이 있는 시스템에서는 Cancel 처리를 반드시 넣는 것이 좋습니다.
11. 정리
이번 단계에서는 ROS 2 Action Server에 Cancel 기능을 추가했습니다.
핵심 코드는 세 부분입니다.
from rclpy.action import ActionServer, CancelResponse
cancel_callback=self.cancel_callback
if goal_handle.is_cancel_requested:
goal_handle.canceled()
return result
cancel_callback()은 Cancel 요청을 허용할지 결정합니다.
goal_handle.is_cancel_requested는 실행 중인 작업 안에서 Cancel 요청이 들어왔는지 확인합니다.
goal_handle.canceled()는 현재 Goal의 최종 상태를 CANCELED로 설정합니다.
이번 예제는 아직 실제 turtlesim 이동 제어를 하지 않지만, Action Server의 중요한 구조인 Goal, Feedback, Result, Cancel 흐름을 모두 포함하게 되었습니다.
다음 단계에서는 이 구조에 /turtle1/cmd_vel 발행과 /turtle1/pose 구독을 추가하면 됩니다.
그렇게 하면 사용자가 입력한 거리만큼 거북이를 이동시키고, 이동 중 남은 거리를 Feedback으로 보내며, 중간에 Cancel 요청이 들어오면 거북이를 정지시키는 실제 Action Server를 만들 수 있습니다.
'강좌 > ROS2' 카테고리의 다른 글
| 6일차 강의 (0) | 2026.05.24 |
|---|---|
| ROS 2 디버깅과 관찰 도구: 로그, rqt_console, rqt_graph, rqt_plot 실습 정리 #2 (0) | 2026.05.24 |
| ROS 2 단위, 좌표, 시간, 파일 시스템, 빌드 시스템, 패키지 구조 정리 #1 (0) | 2026.05.23 |
| 3일차 강의 (0) | 2026.05.23 |
| 2일차 강의 (0) | 2026.05.23 |