1. 이론
1.4서비스 (Service)
토픽만으로는 부족한 경우
토픽은 보내고 잊어버리는 방식입니다. 퍼블리셔는 누가 받았는지, 잘 처리했는지 알 수 없습니다. 센서 값처럼 계속 흐르는 데이터에는 알맞지만 다음과 같은 일에는 불편합니다.
- "로봇을 지금 멈춰 줘" 처럼 한 번 시키고 결과를 받아야 하는 일
- "두 수를 더해 줘", "현재 지도를 줘" 처럼 질문하고 답을 받아야 하는 일
이럴 때 **서비스(Service)**를 씁니다. 서비스는 요청(Request)을 보내면 응답(Response)이 한 번 돌아오는 통신입니다.
둥근 상자 = 노드 · 사각형 = 토픽 · 점선 사각형 = 서비스(점선 화살표: 클라이언트 → 서비스 → 서버)
- 서비스를 제공하는 노드를 서버(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/AddTwoInts | int64 a, int64 b | int64 sum | 예제용 덧셈 |
std_srvs/srv/Trigger | (없음) | bool success, string message | 인자 없이 동작을 실행하고 결과를 받음 |
std_srvs/srv/SetBool | bool data | bool success, string message | 켜기/끄기 |
std_srvs/srv/Empty | (없음) | (없음) | 신호만 전달 |
해보기 1: 서버와 클라이언트
AddTwoInts 서버와 클라이언트를 한 프로그램에 담았습니다. 실제 ROS2에서는 터미널 두 개에서 따로 실행하지만 이 실행기는 한 파일만 실행하므로 노드 두 개를 executor에 함께 올립니다.
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: 응답이 오지 않는 클라이언트 고치기
서비스에서 가장 흔한 문제는 오류 없이 응답이 안 오는 것입니다. 아래 코드는 클라이언트가 응답을 받지 못합니다. 실행해서 증상을 보고 버그 두 개를 찾아 고쳐 보세요.
2) 연습: 응답이 오지 않아요
진단 순서
- 출력 탭의 메시지를 읽습니다. 처음에는 서버를 찾지 못했다고 나옵니다.
- 고친 뒤에도 응답이 없다면 서비스 탭을 보세요. 요청은 갔는데 응답이 없는지 확인합니다.
- 아래를 점검합니다.
- 클라이언트와 서버의 서비스 이름이 정확히 같은가?
- 서비스 타입이 같은가?
- 서버 노드가 spin 중인가? (노드를 만들기만 하면 콜백은 호출되지 않습니다)
실제 ROS2에서는 ros2 service list로 이름을, ros2 service type 이름으로 타입을 확인할 수 있습니다.
정답 확인: 두 버그는 무엇이고 어떻게 고치나요?
- 서비스 이름 오타: 클라이언트가
'add_two_int'(s 없음)로 찾고 있었습니다. 서버의'add_two_ints'와 똑같이 맞춥니다. 이름이 다르면wait_for_service가False를 돌려줍니다. - 서버 노드가 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초에 멈추는 것을 확인하세요.
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처럼 한 번만 요청을 보내고 응답을 출력합니다.
한눈에 정리
- 서비스는 요청 한 번에 응답 한 번을 주고받는 통신이고 계속 흐르는 데이터는 토픽을 쓴다.
.srv파일은---로 요청과 응답을 나눈다. 서버 콜백은(request, response)를 받아 response를 return 한다.- 클라이언트는
call_async()로 future를 받고spin_until_future_complete()또는add_done_callback()으로 응답을 처리한다. - 응답이 안 오면 서비스 이름, 타입, 서버 노드의 spin 여부 순으로 점검한다.
- 콜백 안에서 서비스를 동기식으로 기다리면 데드락에 빠질 수 있다.
확인 문제
Q1. 로봇의 현재 배터리 잔량을 0.5초마다 다른 노드들에 알려 주려고 합니다. 토픽과 서비스 중 어느 쪽이 알맞나요?
토픽입니다. 값이 계속 바뀌고 여러 노드가 받아 볼 수 있어야 하므로 퍼블리셔가 주기적으로 발행하면 됩니다. 서비스는 "지금 잔량 알려 줘"처럼 필요할 때 한 번 물어보는 용도에 맞습니다.
Q2. 클라이언트에서 call_async 후 spin_until_future_complete가 끝나지 않습니다. 오류 메시지도 없습니다. 확인할 것 세 가지는?
- 서비스 이름이 서버와 정확히 같은지 (
ros2 service list) - 서비스 타입이 같은지 (
ros2 service type) - 서버 노드가 실행 중이고 spin되고 있는지 (같은 프로그램이라면 executor에 서버 노드를 추가했는지)
Q3. 서비스 서버 콜백 마지막에 return response를 빼먹으면 어떻게 되나요?
클라이언트에 올바른 응답을 돌려줄 수 없어 오류가 납니다. 서버 콜백은 반드시 채워 넣은 response 객체를 return 해야 합니다. 이 에뮬레이터도 이 경우 안내 메시지와 함께 오류를 냅니다.
다음 장
1.5장에서는 코드를 고치지 않고 노드 설정을 바꾸는 파라미터를 다룹니다. 그다음 1.6장에서 서비스로는 부족한 오래 걸리는 작업을 목표, 피드백, 결과로 주고받는 액션을 배웁니다.