TIKI-MINI로 시작하는 ROS2 실무
목차

1. 이론

1.4서비스 (Service)

토픽만으로는 부족한 경우

토픽은 보내고 잊어버리는 방식입니다. 퍼블리셔는 누가 받았는지, 잘 처리했는지 알 수 없습니다. 센서 값처럼 계속 흐르는 데이터에는 알맞지만 다음과 같은 일에는 불편합니다.

  • "로봇을 지금 멈춰 줘" 처럼 한 번 시키고 결과를 받아야 하는 일
  • "두 수를 더해 줘", "현재 지도를 줘" 처럼 질문하고 답을 받아야 하는 일

이럴 때 **서비스(Service)**를 씁니다. 서비스는 요청(Request)을 보내면 응답(Response)이 한 번 돌아오는 통신입니다.

add_clientadd_server/add_two_ints

둥근 상자 = 노드 · 사각형 = 토픽 · 점선 사각형 = 서비스(점선 화살표: 클라이언트 → 서비스 → 서버)

  • 서비스를 제공하는 노드를 서버(Server), 호출하는 노드를 클라이언트(Client) 라고 합니다.
  • 서버는 서비스 이름 하나에 하나만 두는 것이 원칙입니다. 클라이언트는 여러 개여도 됩니다.
  • 토픽과 마찬가지로 이름과 타입이 맞아야 연결됩니다.
구분토픽서비스
방향퍼블리셔에서 서브스크라이버로 한 방향클라이언트가 요청, 서버가 응답 (양방향)
보내는 시점주기적으로 계속필요할 때 한 번
결과 확인알 수 없음응답으로 성공 여부와 값을 받음
알맞은 예센서 값, 속도 명령, 로봇 상태모드 전환, 설정 조회, 한 번 실행하는 동작

서비스는 요청 하나에 응답 하나가 오는 통신입니다. 아래 애니메이션에서 클라이언트가 요청을 보내면 서버가 처리하고 응답을 돌려줍니다.

클라이언트가 서버에 요청을 보내고 응답을 받는 서비스 애니메이션

서비스 타입과 .srv 파일

서비스 타입은 .srv 파일로 정의합니다. 요청 부분과 응답 부분을 --- 줄로 나눕니다.

# example_interfaces/srv/AddTwoInts.srv
int64 a
int64 b
---
int64 sum

위쪽이 요청(a, b), 아래쪽이 응답(sum)입니다. 타입 이름은 패키지/srv/이름 형식을 따라 example_interfaces/srv/AddTwoInts가 됩니다. Python에서는 from example_interfaces.srv import AddTwoInts로 가져오고 요청과 응답 클래스로는 각각 AddTwoInts.Request, AddTwoInts.Response를 씁니다.

이 장에서 쓰는 서비스 타입은 다음과 같습니다.

타입요청응답용도
example_interfaces/srv/AddTwoIntsint64 a, int64 bint64 sum예제용 덧셈
std_srvs/srv/Trigger(없음)bool success, string message인자 없이 동작을 실행하고 결과를 받음
std_srvs/srv/SetBoolbool databool success, string message켜기/끄기
std_srvs/srv/Empty(없음)(없음)신호만 전달

해보기 1: 서버와 클라이언트

AddTwoInts 서버와 클라이언트를 한 프로그램에 담았습니다. 실제 ROS2에서는 터미널 두 개에서 따로 실행하지만 이 실행기는 한 파일만 실행하므로 노드 두 개를 executor에 함께 올립니다.

LIVE

1) 서비스 서버 + 클라이언트: AddTwoInts

코드 읽는 법

  • 서버: create_service(타입, 이름, 콜백). 콜백은 (request, response)를 받고 response를 return 합니다.
  • 클라이언트: create_client(타입, 이름). call_async(request)는 기다리지 않고 Future를 돌려줍니다.
  • wait_for_service(timeout_sec=...): 서버가 나타날 때까지 기다립니다. 없으면 False입니다.
  • spin_until_future_complete(future): 응답이 올 때까지 spin 합니다. 그 뒤 future.result()로 응답 객체를 꺼냅니다.

결과 읽는 법

  • 서비스 탭: 요청과 응답이 번호(#1)로 짝지어 보입니다. (ros2 service call의 결과와 같은 정보입니다)
  • 노드 그래프 탭: 클라이언트 → 서비스 → 서버가 점선으로 이어집니다.

직접 바꿔 보기

  • [(3, 4), (10, 25)]에 쌍을 더 추가해 보세요.
  • request.a = 1.5 처럼 정수 필드에 실수를 넣으면 어떻게 될까요? (int64 필드에는 int만 넣을 수 있습니다)
  • 서버 콜백의 return response를 지우고 실행해 보세요. 오류 메시지를 읽어 보세요.

해보기 2: 응답이 오지 않는 클라이언트 고치기

서비스에서 가장 흔한 문제는 오류 없이 응답이 안 오는 것입니다. 아래 코드는 클라이언트가 응답을 받지 못합니다. 실행해서 증상을 보고 버그 두 개를 찾아 고쳐 보세요.

LIVE

2) 연습: 응답이 오지 않아요

진단 순서

  1. 출력 탭의 메시지를 읽습니다. 처음에는 서버를 찾지 못했다고 나옵니다.
  2. 고친 뒤에도 응답이 없다면 서비스 탭을 보세요. 요청은 갔는데 응답이 없는지 확인합니다.
  3. 아래를 점검합니다.
    • 클라이언트와 서버의 서비스 이름이 정확히 같은가?
    • 서비스 타입이 같은가?
    • 서버 노드가 spin 중인가? (노드를 만들기만 하면 콜백은 호출되지 않습니다)

실제 ROS2에서는 ros2 service list로 이름을, ros2 service type 이름으로 타입을 확인할 수 있습니다.

정답 확인: 두 버그는 무엇이고 어떻게 고치나요?
  1. 서비스 이름 오타: 클라이언트가 'add_two_int'(s 없음)로 찾고 있었습니다. 서버의 'add_two_ints'와 똑같이 맞춥니다. 이름이 다르면 wait_for_service가 False를 돌려줍니다.
  2. 서버 노드가 spin되지 않음: executor에 클라이언트만 넣어서 서버의 콜백이 호출되지 않았습니다. executor.add_node(server)를 추가합니다. 서비스 호출은 성공적으로 전송되지만 처리해 주는 쪽이 없으니 응답이 영원히 오지 않습니다.

고친 코드:

self.cli = self.create_client(AddTwoInts, 'add_two_ints')   # 이름 수정
...
executor.add_node(client)
executor.add_node(server)                                    # 서버도 spin

해보기 3: 서비스로 로봇 주행 켜고 끄기

Trigger 서비스로 로봇의 주행 상태를 전환해 봅시다. driver 노드는 /toggle_drive 서비스를 받을 때마다 주행을 시작하거나 정지합니다. operator 노드는 3초마다 이 서비스를 호출합니다. 로봇 캔버스에서 3초, 9초에 출발하고 6초, 12초에 멈추는 것을 확인하세요.

LIVE

3) Trigger 서비스로 주행 전환

토픽과 서비스를 함께 쓰는 모습

  • 속도 명령(/cmd_vel)은 계속 흐르는 데이터이므로 토픽입니다.
  • 주행을 켜고 끄는 일은 한 번의 요청이므로 서비스입니다. 응답의 message로 결과를 알 수 있습니다.

직접 바꿔 보기

  • Trigger를 SetBool로 바꿔 보세요 (from std_srvs.srv import SetBool). 요청에 request.data가 생기므로 "켜기/끄기"를 직접 지정할 수 있습니다. 서버는 self.running = request.data로 바꾸고 클라이언트는 req = SetBool.Request(); req.data = True로 호출합니다.
  • 타이머 주기를 1.5초로 바꾸면 어떻게 되나요?
  • executor.add_node(driver) 줄을 지우고 실행하면 로봇이 움직일까요? 서비스 탭을 확인하세요.

실제 ROS2에서는 (참고)

개발환경을 구축한 뒤에는 터미널에서 서비스를 직접 확인하고 호출할 수 있습니다.

ros2 service list                   # 현재 서비스 목록
ros2 service list -t                # 타입까지 함께 표시
ros2 service type /add_two_ints     # example_interfaces/srv/AddTwoInts
ros2 interface show example_interfaces/srv/AddTwoInts   # .srv 구조 보기
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 2, b: 3}"

데모 서버로 연습하려면 터미널 한 곳에서 ros2 run demo_nodes_cpp add_two_ints_server를 실행한 뒤 다른 터미널에서 위 ros2 service call을 실행합니다. 서비스 호출 한 번은 ros2 topic pub --once처럼 한 번만 요청을 보내고 응답을 출력합니다.

한눈에 정리

  1. 서비스는 요청 한 번에 응답 한 번을 주고받는 통신이고 계속 흐르는 데이터는 토픽을 쓴다.
  2. .srv 파일은 ---로 요청과 응답을 나눈다. 서버 콜백은 (request, response)를 받아 response를 return 한다.
  3. 클라이언트는 call_async()로 future를 받고 spin_until_future_complete() 또는 add_done_callback()으로 응답을 처리한다.
  4. 응답이 안 오면 서비스 이름, 타입, 서버 노드의 spin 여부 순으로 점검한다.
  5. 콜백 안에서 서비스를 동기식으로 기다리면 데드락에 빠질 수 있다.

확인 문제

Q1. 로봇의 현재 배터리 잔량을 0.5초마다 다른 노드들에 알려 주려고 합니다. 토픽과 서비스 중 어느 쪽이 알맞나요?

토픽입니다. 값이 계속 바뀌고 여러 노드가 받아 볼 수 있어야 하므로 퍼블리셔가 주기적으로 발행하면 됩니다. 서비스는 "지금 잔량 알려 줘"처럼 필요할 때 한 번 물어보는 용도에 맞습니다.

Q2. 클라이언트에서 call_async 후 spin_until_future_complete가 끝나지 않습니다. 오류 메시지도 없습니다. 확인할 것 세 가지는?
  1. 서비스 이름이 서버와 정확히 같은지 (ros2 service list)
  2. 서비스 타입이 같은지 (ros2 service type)
  3. 서버 노드가 실행 중이고 spin되고 있는지 (같은 프로그램이라면 executor에 서버 노드를 추가했는지)
Q3. 서비스 서버 콜백 마지막에 return response를 빼먹으면 어떻게 되나요?

클라이언트에 올바른 응답을 돌려줄 수 없어 오류가 납니다. 서버 콜백은 반드시 채워 넣은 response 객체를 return 해야 합니다. 이 에뮬레이터도 이 경우 안내 메시지와 함께 오류를 냅니다.

다음 장

1.5장에서는 코드를 고치지 않고 노드 설정을 바꾸는 파라미터를 다룹니다. 그다음 1.6장에서 서비스로는 부족한 오래 걸리는 작업을 목표, 피드백, 결과로 주고받는 액션을 배웁니다.