ROS 2에서 두 정수를 더해 돌려주는 간단한 서비스 서버와 클라이언트를 만들어 봤어요. 요청을 보내면 응답이 오는 걸 확인하는 것까지는 교재 예제 그대로였는데, 그 예제에는 실제로 쓰면 서버를 멈추게 하는 구멍이 하나 있었어요.
서비스는 요청과 응답이 한 번씩 오가요
| 토픽 | 서비스 | |
|---|---|---|
| 흐름 | 한 방향으로 계속 흐르는 스트림 | 한 번 요청하고 한 번 응답받는 왕복 |
| 예 | 카메라 영상 | ”이 값을 계산해 줘” |
서버가 create_service로 콜백을 등록해 두면, 클라이언트가 요청할 때마다 그 콜백이 불려서 응답을 채워 돌려줘요. 콜백은 요청 객체를 받아 계산한 뒤, 미리 만들어져 넘어온 응답 객체에 값을 넣고 그대로 반환해야 해요. 반환을 빠뜨리면 응답에 None이 나가서 클라이언트가 조용히 실패해요.
콜백에서 예외가 나면 서버가 죽어요
이 예외가 어디서 나는지 보면 감이 잡혀요. 두 수의 합을 응답의 int64 필드에 넣는데, 파이썬의 int는 자릿수 제한이 없는 반면 ROS 2의 int64는 8바이트 고정이에요. 파이썬에서는 아무렇지 않게 계산되던 큰 수가 응답 필드에 대입되는 순간, 메시지 정의에서 생성된 검증 코드가 범위를 벗어났다며 예외를 던져요. 입력이 조금만 커져도 서버가 죽을 수 있다는 뜻이에요.
콜백을 감싸서 죽지 않게 만들어요
INT64_MIN, INT64_MAX = -(2**63), 2**63 - 1
try:
result = request.a + request.b
if not (INT64_MIN <= result <= INT64_MAX):
raise OverflowError('결과가 int64 범위를 벗어남')
response.sum = result
except OverflowError as exc:
self.get_logger().error(str(exc))
response.sum = 0
except Exception as exc:
self.get_logger().error(f'처리 실패: {exc}')
response.sum = 0
return response넓은 범위의 except는 보통 피해야 할 패턴이지만, 서비스 콜백에서는 “죽지 않고 응답은 돌려준다”가 더 중요해서 예외적으로 허용해요. 대신 반드시 로그에 원인을 남겨 나중에 추적할 수 있게 해요.
sum에 0을 넣으면, 클라이언트는 그게 정말 합이 0인지 실패해서 0인지 구분할 수 없어요. 그래서 실무에서는 응답에 bool success와 string message 필드를 함께 둬요. 같이 만든 모터 on/off 서비스의 응답은 이미 그 형태로, 성공 여부와 사유 문자열을 함께 돌려줘요.
클라이언트도 자신을 지켜야 해요
서버만 고쳐서는 부족해요.
| 지점 | 방어 | 안 하면 |
|---|---|---|
wait_for_service | 제한 시간 | 서버가 안 뜨면 영원히 대기 |
spin_until_future_complete | 제한 시간 | 응답이 안 오면 영원히 대기 |
future.result() | None 확인 후 접근 | 이번엔 클라이언트가 예외로 죽음 |
future.result()는 호출이 실패하면 None을 돌려주는데, 확인 없이 곧바로 response.sum에 접근하면 클라이언트 쪽이 죽어요. 결과가 None인지 먼저 보고 접근하는 습관이 필요해요.
정리하면 서비스는 두 규칙으로 지켜요. 서버는 콜백을 감싸 어떤 요청에도 죽지 않고 응답을 돌려주고, 클라이언트는 타임아웃으로 무한 대기를 끊어내요. 계산 로직 몇 줄보다 이 두 방어를 갖추는 게 실제로 돌아가는 서비스에 훨씬 가까운 일이었어요.