중개 서버를 아무것도 안 켰는데 두 노드가 연결돼요. 이게 ROS2의 가장 큰 구조적 변화이고, 이 글의 모든 함정이 여기서 나와요.
마스터가 없다는 말의 실제 의미
이전 세대에는 중앙 등록소가 있었어요. 모든 노드가 먼저 거기에 “나 여기 있다”고 등록하면 등록소가 짝을 맺어줬죠. 그래서 등록소가 죽으면 전체가 죽었어요.
지금은 각 노드가 켜질 때 같은 네트워크에 “나 이런 토픽을 발행하거나 구독한다”고 알림을 뿌리고, 조건이 맞는 상대를 서로 발견해 직접 연결해요. 이걸 디스커버리라 하고 하부 미들웨어가 담당해요.
짝이 맺어지는 조건은 셋인데, 하나라도 어긋나면 연결이 안 돼요.
| 조건 | 어긋나면 |
|---|---|
| 토픽 이름이 문자열로 정확히 같을 것 | 양쪽 다 정상 실행되는데 아무 일도 안 일어남 |
| 메시지 타입이 같을 것 | 이름이 같아도 연결 안 됨 |
| 정책이 호환될 것 | 가장 찾기 어려운 실패 |
세 줄 모두 오른쪽 칸이 “에러”가 아니에요. 아무 일도 안 일어나요. 중앙 등록소가 있던 시절에는 등록소에 물어보면 누가 무엇을 하는지 알 수 있었는데, 지금은 물어볼 곳이 없어요. 그래서 진단 감각 자체가 달라져야 해요.
남의 로봇이 내 목록에 떠요
디스커버리는 같은 네트워크에 뿌리는 방식이라 같은 무선망의 다른 로봇이나 옆자리 노트북의 노드까지 서로 발견해요.
실습실에서 여러 명이 동시에 로봇을 돌리면 남의 토픽이 내 목록에 뜨고, 심하면 남의 로봇에 내 명령이 들어가요. 재현 안 되는 이상 동작을 며칠 쫓다가 원인이 옆자리였다는 결말이 실제로 나와요.
해결은 환경변수예요. 같은 값을 쓰는 노드끼리만 한 세계를 이뤄요.
export ROS_DOMAIN_ID=7 # 같은 숫자끼리만 서로 발견한다한 발 더 나가면 탐색 범위 자체를 자기 기기로 제한하는 설정이 있어요. 혼자 실습할 때는 이쪽이 확실해요. 네트워크로 나가지 않으니 애초에 섞일 일이 없거든요.
증상이 “내 노드가 안 보인다” 또는 “남의 노드가 보인다”면 코드를 보기 전에 이 둘을 먼저 봐요.
정책이 필요한 이유
로봇 안의 데이터는 성격이 정반대인 것들이 섞여 있어요.
카메라 프레임은 초당 서른 장씩 오니 한 장 놓쳐도 다음 장이 곧 오고, 오히려 재전송하느라 늦어지는 게 더 나빠요. 반대로 비상 정지 명령은 절대 유실되면 안 되고 늦더라도 반드시 도착해야 해요.
이 둘을 같은 규칙으로 나를 수는 없어서 토픽마다 정책을 고르게 한 거예요. 세 축만 알면 실무의 대부분이 커버돼요.
| 축 | 값 | 뜻 |
|---|---|---|
| 신뢰성 | 재전송함 | 도착할 때까지 다시 보냄. 기본값 |
| 한 번만 쏨 | 유실 감수, 대신 빠름 — 센서용 | |
| 내구성 | 휘발 | 늦게 들어온 구독자는 과거 메시지를 못 받음. 기본값 |
| 보관 | 마지막 메시지를 들고 있다가 늦게 온 구독자에게도 줌 | |
| 이력 | 최근 n개 | 버퍼에 최근 것만. 기본값 |
| 전부 | 메모리 주의 |
내구성의 두 번째 값이 쓰이는 자리가 분명해요. 지도나 로봇 구조 정보처럼 한 번 발행되고 마는 것이요. 이게 휘발이면 나중에 뜬 노드는 영영 못 받아요. 노드 시작 순서에 따라 되기도 하고 안 되기도 하는 증상이 여기서 나와요.
못 지킬 약속은 거절돼요
이 글에서 가장 중요한 규칙 하나예요. 구독자가 발행자보다 더 엄격한 것을 요구하면 짝이 안 맺어져요.
| 발행자 | 구독자 | 결과 |
|---|---|---|
| 재전송함 | 재전송함 | 연결 |
| 재전송함 | 한 번만 쏨 | 연결 — 덜 요구하는 건 괜찮아요 |
| 한 번만 쏨 | 재전송함 | 연결 안 됨 |
| 한 번만 쏨 | 한 번만 쏨 | 연결 |
세 번째 줄이 왜 거절되는지 생각해 보면 납득이 돼요. 발행자가 “나는 재전송 안 해”라고 선언했는데 구독자가 “재전송해 줘”를 요구한 거예요. 지킬 수 없는 약속이라 아예 안 맺는 거죠. 반대로 두 번째 줄은 발행자가 더 후하게 주는 거라 문제가 없어요.
증상이 고약한 이유
거리 센서나 카메라 드라이버는 대개 한 번만 쏘는 쪽으로 발행해요. 그런데 구독을 만들 때 큐 길이 숫자만 주면 기본값이 재전송하는 쪽이라 위 표의 세 번째 줄에 걸려요.
그러면 이렇게 돼요.
- 토픽 목록에 토픽이 보여요
- 내용을 확인하는 명령에 데이터가 흘러요 — 그 명령은 한 번만 쏘는 쪽으로 붙거든요
- 주기를 재는 명령도 정상으로 나와요
- 그런데 내 노드의 콜백만 한 번도 안 불려요
에러도 경고도 없어요. 확인 명령마다 다 정상이니 코드에 잘못이 없어 보여서 며칠을 태울 수 있어요.
진단은 발행 쪽과 구독 쪽의 정책을 나란히 출력하는 옵션 하나로 끝나요. 어디가 어긋났는지 눈으로 보여요. 그 습관만 있으면 며칠이 몇 분이 돼요.
해결은 센서용으로 미리 만들어진 프로파일을 쓰는 거예요. 숫자 대신 그 프로파일을 넘기면 돼요. 값을 손으로 조합하지 않아도 되고, 이름 자체가 의도를 말해 줘요.
**“센서 토픽인데 콜백이 안 불린다 → 정책부터 본다”**를 반사로 만들어 두면 이 함정은 다시 안 걸려요.
조용한 실패를 다루는 감각
정리하면 이 영역의 실패는 전부 같은 얼굴이에요. 크래시도 예외도 로그도 없고, 그냥 일어나야 할 일이 안 일어나요.
그래서 진단 순서가 정해져 있어요. 발견 자체가 안 되는지(같은 세계에 있는지), 발견은 됐는데 짝이 안 맺어지는지(이름·타입·정책), 짝은 맺어졌는데 데이터가 안 오는지를 차례로 갈라요. 각 단계마다 확인 명령이 하나씩 있고, 어느 단계에서 끊겼는지만 알면 원인 후보가 서너 개로 줄어요.
기록을 남길 때 토픽별 메시지 개수를 먼저 보는 습관도 여기서 나와요. 개수가 0인 토픽은 이름 오타이거나 정책이 안 맞아 구독 자체가 실패한 것이거든요. 그 이야기는 rosbag2 — 시계 축이 어긋난 기록은 조용히 틀려요에 있어요.
노드를 만드는 쪽 코드는 C++ rclcpp — 콜백 수명과 스레딩이 만드는 사고에, 주기를 계약으로 다루는 이야기는 ros2_control — update()의 주기 계약에 있어요.