제12부 실시간·성능
12.5 결정성과 실행 순서 제어
12.5.1 이 장에서 배우는 것 / 선수 지식
지연이 "얼마나 빠른가" 라면, 결정성은 "매번 같은가" 다. 둘은 다른 축이며, 서로 절충 관계에 있기도 하다.
결정성이 중요한 이유는 세 가지다. 첫째, 재현 가능해야 디버깅할 수 있다 — 같은 bag을 두 번 실행했는데 결과가 다르면 원인을 좁힐 수 없다. 둘째, 시험이 의미를 가진다 — 통과·실패가 실행마다 바뀌면 CI가 신뢰를 잃는다. 셋째, 검증·인증이 가능해진다 — 동작을 예측할 수 있어야 논증할 수 있다.
그리고 정직한 결론을 먼저 말한다 — ROS 2에서 완전한 결정성은 일반적으로 달성하기 어렵다. 이 장은 그것을 인정한 위에서, 어디까지 가능하고 무엇을 포기해야 하는지 다룬다.
배우는 것:
- 결정성의 정의 — 무엇이 같아야 하는가.
- 비결정성의 원천 — 층별로.
- 실행기 순서 — ROS 2 결정성의 핵심 문제.
- 콜백 그룹과 격리 — 부분적 제어.
- 시간 구동 설계 — 결정성을 높이는 구조.
- 시험 — 결정성을 확인하는 방법.
- 정직한 한계 — 어디까지인가.
선수 지식: 2.2(실행기·대기 집합), 2.3(실행기 선택), 2.4(콜백 그룹), 12.1(실시간), 12.4(지연 예산), 11.3(bag 재생).
정직: rclcpp 실행기의 결정성 관련 기능(정적 실행기·이벤트 기반 실행기 등)은 배포판마다 존재 여부와 특성이 다르다
(확인 필요). Jazzy에서 무엇이 제공되는지·기본 실행기의 정확한 선택 규칙은 문서와 소스로 확인해야 한다. 이 장은 개념과 판단 원칙을 다룬다. 이 환경에서 대부분 검증하지 않았다(목차 3) — 예외는 엔티티 유형별 실행 순서로, 93~95차에 걸쳐SingleThreadedExecutor에서TIMER → SUB → SRV → CLIENT → WAITABLE전 단계를, 95차에MultiThreadedExecutor에서의 붕괴(16~47%)까지 실행 확인했다(12.5.4·2.3.14).
12.5.2 결정성이란
| 종류 | 요구 | 난이도 |
|---|---|---|
| 값 | 같은 입력에 같은 결과 | 비교적 쉬움 |
| 순서 | 콜백 실행 순서가 같음 | 어려움 |
| 시간 | 같은 시각에 실행 | 매우 어려움 |
견해: 대부분의 실무 요구는 "값 결정성"이다. bag을 두 번 실행했을 때 같은 경로가 나오면 충분하고, 나노초 단위로 같은 시점에 실행될 필요는 없다. 요구를 이 세 층으로 나누면 목표가 현실적이 된다.
함정: 값 결정성도 순서에 의존한다. 두 콜백이 공유 상태를 조작하는데 순서가 바뀌면 결과가 달라진다. 순서 비결정성이 값 비결정성으로 전파되는 것이 흔한 경로다.
결정성이 필요한 곳:
| 상황 | 이유 |
|---|---|
| 재생 기반 시험 | 통과·실패가 일정해야 |
| 회귀 비교 | 차이가 변경 때문인지 판별 |
| 디버깅 | 재현 |
| 검증·논증 | 예측 가능성 |
| 시뮬 반복 | 실험 통제 |
필요 없는 곳:
| 상황 | 이유 |
|---|---|
| 실기 운영 | 입력이 어차피 매번 다름 |
| 탐색적 개발 | 속도 우선 |
| 통계적 평가 | 반복으로 평균 |
12.5.3 비결정성의 원천
| 층 | 원천 |
|---|---|
| 실행기 | 여러 준비된 콜백 중 선택 순서(2.2.5) |
| 스레드 | 스케줄링·경합 |
| DDS | 도착 순서·큐·재전송 |
| 시간 | 벽시계·타이머 드리프트 |
| 알고리즘 | 난수·해시 순회·부동소수 순서 |
| 병렬 처리 | 리덕션 순서 |
| 외부 | 파일 시스템·네트워크 |
| 초기화 | 노드 시작 순서·디스커버리(3.3) |
견해: 초기화 순서 비결정성이 과소평가된다. 노드가 기동되는 순서·디스커버리 완료 시점이 매번 달라, 초기 몇 초의 동작이 실행마다 다르다(1.12.6). 재생 시험에서 앞부분을 버리거나, 준비 상태를 명시적으로 기다리는(1.12) 것이 대처다.
함정: 부동소수 연산 순서가 바뀌면 결과가 미세하게 달라진다. 병렬 리덕션·컴파일러 최적화·라이브러리 버전에 따라 마지막 비트가 다르고, 그것이 분기를 바꿔 큰 차이로 증폭될 수 있다. 비트 단위 재현성을 요구하면 병렬화·최적화를 제약해야 한다.
함정: 난수 시드를 고정하지 않으면 계획이 매번 다르다. OMPL 같은 샘플링 기반 계획기(9.2.3)는 본질적으로 확률적이므로, 재현이 필요하면 시드를 고정해야 한다 — 다만 그러면 실제 운영의 다양성을 반영하지 못한다.
12.5.4 실행기 순서 — 핵심 문제
여러 콜백이 동시에 준비되면 무엇을 먼저 실행하는가?
| 요인 | 설명 |
|---|---|
| 엔티티 유형 우선순위 | TIMER → SUB → SRV → CLIENT → WAITABLE(Jazzy, SingleThreadedExecutor, 93~95차 전 단계 실행 확인 — 2.3.14) |
| 등록 순서 | 추가된 순서 영향 |
| 대기 집합 재구성 | 엔티티 변경 시 |
| 한 번의 wait에서 여러 개 처리 | 처리 방식 |
정직: 2.2.5에서 설명한 선택 규칙은 구현 세부이며 배포판·실행기 종류에 따라 다르다
(확인 필요). 위 순서는 Jazzy의SingleThreadedExecutor에서 측정한 스냅숏이지, 문서화된 계약이 아니다 — 실제로MultiThreadedExecutor에서는(95차 실행 확인, 2.3.14) 이 순서가 16~47% 확률로 깨진다(스레드가 적을수록 자주).EventsExecutor는 97차에 확인했는데 같지 않다 — 정반대다(SUB→CLIENT→SRV→TIMER, 타이머가 맨 뒤, 14/14 재현, 2.3.14). 애플리케이션이 특정 순서에 의존하도록 설계하지 말라는 것이SingleThreadedExecutor에서도 안전한 원칙이지만,MultiThreadedExecutor·EventsExecutor에서는 선택이 아니라 필수다 — 같은 코드베이스가 executor 종류에 따라 서로 다른 결정적 순서를 낸다는 것이 실측으로 확인됐기 때문이다.
견해: "실행 순서에 의존하지 않는 설계"가 가장 실용적인 대처다. 순서를 통제하려 애쓰기보다, 어느 순서로 실행돼도 결과가 같도록 만든다 — 콜백은 상태를 저장만 하고, 계산은 명시적 시점(타이머)에 한다(12.5.6).
부분적 제어 수단:
| 수단 | 효과 |
|---|---|
| 콜백 그룹 분리 + 별도 실행기 | 그룹 간 독립(2.4) |
| 전용 스레드·RT 우선순위 | 중요 경로 우선(12.2) |
| 노드 분리 | 프로세스 격리 |
| 단일 콜백으로 통합 | 순서 문제 소멸 |
정적 실행기(확인 필요) | 재구성 비용 감소 |
견해: "순서가 중요한 두 콜백"이 있다면 그것을 하나로 합치는 것을 검토하라. A 다음 B가 실행되어야 한다면, 하나의 콜백에서 A와 B를 차례로 호출하면 된다. 실행기에 순서를 맡기지 않는 것이 확실하다.
12.5.5 콜백 그룹과 격리
2.3의 구조가 결정성에도 쓰인다.
| 구성 | 결정성 |
|---|---|
| 모두 한 그룹(MutuallyExclusive) | 동시 실행 없음 — 경쟁 조건 없음, 순서는 여전히 불명 |
| Reentrant 그룹 | 병렬 — 비결정성 증가 |
| 경로별 그룹 + 별도 실행기 | 경로 간 독립 — 각 경로 내 순서는 통제 |
| 전용 스레드 | 격리 |
견해: "경로별로 격리하고, 경로 안에서는 단일 스레드"가 결정성과 성능의 좋은 절충이다. 경로 간에는 병렬로 돌아 처리량을 얻고, 경로 안에서는 순차라 경쟁 조건이 없다. 2.3·2.4에서 다룬 구조가 여기서 또 다른 목적으로 쓰인다.
함정: Reentrant 그룹은 같은 콜백이 동시에 여러 번 실행될 수 있다(2.3.3). 콜백 안에서 멤버 변수를 조작하면 경쟁 조건이 생긴다. 병렬성을 얻는 대신 스레드 안전을 스스로 책임져야 한다.
견해: 공유 상태를 줄이는 것이 근본 대책이다. 콜백들이 공유 변수를 조작하지 않으면 순서가 결과를 바꾸지 않는다. 메시지로 주고받고 상태는 한 곳에서만 갱신하는 설계가 결정성에 유리하다.
12.5.6 시간 구동 설계
결정성을 높이는 구조적 접근이다.
| 축 | 사건 구동 | 시간 구동 |
|---|---|---|
| 지연 | 낮음 | 평균 반주기 추가 |
| 결정성 | 낮음 | 높음 |
| 부하 변동 | 입력에 따라 | 일정 |
| 순서 의존 | 큼 | 작음 |
| 과부하 거동 | 적체 | 자연스러운 드롭 |
// 시간 구동 패턴
class TimeDriv : public rclcpp::Node {
void on_scan(LaserScan::SharedPtr msg) { latest_scan_ = msg; } // 저장만
void on_odom(Odometry::SharedPtr msg) { latest_odom_ = msg; }
void on_timer() { // 여기서만 계산
if (!latest_scan_ || !latest_odom_) return;
auto result = compute(*latest_scan_, *latest_odom_);
pub_->publish(result);
}
};
견해: 제어 루프는 시간 구동이 표준적 방법이다(6.5·6.6). 일정한 주기로 실행되는 것이 제어 이론의 전제이고, 부하가 일정해 예측 가능하며, 입력이 늦어도 이전 값으로 진행할 수 있다. 6.2의
ros2_control루프가 정확히 이 구조다.
견해: "과부하 시 자연스러운 드롭"이 시간 구동의 숨은 장점이다. 사건 구동은 입력이 폭주하면 큐가 쌓여 지연이 무한히 늘지만, 시간 구동은 최신값만 보므로 오래된 것이 자동으로 버려진다. 로봇에서는 대개 이것이 옳은 거동이다.
함정: 시간 구동은 지연을 더한다(12.4.5). 안전 감시처럼 지연이 결정적인 경로는 사건 구동이 낫다. 경로마다 다르게 선택하는 것이 옳다.
입력 정합:
| 방법 | 설명 |
|---|---|
| 최신값 사용 | 가장 단순 |
| 스탬프 확인 | 너무 오래된 값 거부(7.7.7) |
| 동기화 | message_filters(7.7.5) |
| 보간 | 시각을 맞춤 |
견해: 최신값을 쓰되 스탬프를 검사하라. 센서가 고장 나도 마지막 값이 계속 쓰이면 위험하다(7.7.7). "N ms보다 오래된 값이면 사용하지 않고 안전 동작" 이 기본 규칙이다.
12.5.7 결정성 시험
주장하지 말고 확인한다.
| 시험 | 방법 |
|---|---|
| 재생 반복 | 같은 bag을 N회 — 출력 비교 |
| 허용 오차 비교 | 완전 일치 대신 임계 |
| 차이 지점 특정 | 첫 발산 시점 |
| 부하 변동 시험 | 부하를 가해도 같은가 |
| 순서 교란 | 의도적으로 순서를 바꿔 봄 |
# 개념: 두 번 실행해 결과 비교
for i in 1 2; do
ros2 launch my_pkg replay.launch.py bag:=$BAG out:=run$i.bag
done
# 스크립트로 두 결과의 지표를 비교 (11.2.8)
견해: "완전 일치"를 요구하면 대개 실패한다. 부동소수 미세 차이·타임스탬프 차이가 있기 때문이다. 의미 있는 지표(경로 길이·성공 여부·최종 자세)가 허용 오차 내인지를 기준으로 삼는 것이 실용적이다.
견해: 차이가 나는 첫 지점을 찾는 것이 진단의 핵심이다. 두 실행의 로그·출력을 시간순으로 비교해 발산 시작점을 찾으면, 그 시점에 무슨 콜백이 다르게 실행됐는지 좁힐 수 있다. 트레이싱(11.6)이 여기서 유용하다.
함정: 재생 시험도 완전히 결정적이지 않다. bag 재생 자체의 타이밍·디스커버리·시스템 부하가 영향을 준다. 재생 시험의 비결정성과 애플리케이션의 비결정성을 구분해야 한다.
12.5.8 정직한 한계
어디까지 가능한가.
| 목표 | 달성 가능성 |
|---|---|
| 값 결정성(허용 오차 내) | 대체로 가능 — 설계하면 |
| 단일 노드 내 순서 결정성 | 가능(단일 콜백·시간 구동) |
| 다중 노드 간 순서 결정성 | 어려움 |
| 시간 결정성 | 일반적으로 불가 |
| 비트 단위 재현 | 조건이 많음 |
견해: "ROS 2는 결정적 실행을 보장하지 않는다"를 전제로 설계하라. 보장을 기대하고 순서에 의존하는 코드를 쓰면, 부하가 바뀌거나 배포판이 바뀔 때 깨진다. 순서에 무관한 설계 + 시간 구동 + 상태 최소화가 실질적 해법이다.
견해: 결정성이 정말 필요한 부분을 좁혀라. 시스템 전체를 결정적으로 만들려는 시도는 비용이 크고 대개 실패한다. 시험 대상 알고리즘만 결정적으로(입력을 파일로 주고 순수 함수로 계산) 만들면 시험은 안정되고, 통합 계층은 통계적으로 평가한다.
정직: 결정적 실행을 목표로 하는 대안 실행기·프레임워크가 연구·개발되고 있다
(확인 필요: 현재 상태). 이 영역은 변화가 있으므로, 강한 결정성 요구가 있다면 현재의 선택지를 조사할 가치가 있다.
실용적 지침:
| 지침 | 이유 |
|---|---|
| 순서에 의존하지 않는다 | 보장이 없다 |
| 시간 구동을 기본으로 | 결정성·부하 안정 |
| 공유 상태 최소화 | 경쟁 조건 제거 |
| 핵심 알고리즘을 순수 함수로 | 단위 시험이 결정적 |
| 난수 시드 고정(시험 시) | 재현 |
| 허용 오차 기반 비교 | 현실적 |
| 초기화 완료를 명시적으로 대기 | 시작 비결정성 완화 |
견해: "핵심 알고리즘을 순수 함수로 분리"가 가장 가치 있는 조치다. 계획·제어·필터의 계산부를 ROS와 무관한 함수로 만들면, 그것만은 완전히 결정적으로 시험할 수 있다(4.10.3의 단위 시험). ROS 계층의 비결정성은 통합 시험에서 통계적으로 다룬다. 이 분리가 시험 전략 전체를 단순하게 만든다.
12.5.9 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | 실행 순서에 의존 | 환경 바뀌면 깨짐 | 무관한 설계 |
| 2 | 순서 비결정 → 값 비결정 | 결과가 다름 | 공유 상태 제거 |
| 3 | Reentrant에서 멤버 접근 | 경쟁 조건 | 동기화·그룹 선택 |
| 4 | 초기화 순서 무시 | 초반 동작 상이 | 명시적 대기 |
| 5 | 난수 시드 미고정 | 시험 불안정 | 시험 시 고정 |
| 6 | 부동소수 순서 변화 | 미세 차이 증폭 | 허용 오차 비교 |
| 7 | 완전 일치 요구 | 시험 실패 반복 | 지표 기반 |
| 8 | 시간 구동을 모든 곳에 | 지연 추가 | 경로별 선택 |
| 9 | 최신값을 스탬프 검사 없이 | 종료된 센서 값 사용 | 나이 검사 |
| 10 | 전 시스템 결정성 시도 | 비용 과다 | 범위 좁히기 |
| 11 | 재생 시험 자체의 비결정성 혼동 | 오진 | 구분 |
| 12 | 결정성 보장을 가정 | 깨짐 | 전제 인식 |
12.5.10 체크포인트
- 결정성의 세 종류와 각각의 난이도를 말할 수 있는가.
- 순서 비결정성이 값 비결정성으로 전파되는 경로는?
- 결정성이 필요한 상황 다섯과 필요 없는 상황 셋을 구분할 수 있는가.
- 비결정성의 원천 여덟 층을 들 수 있는가. 과소평가되는 것은?
- 부동소수 순서와 난수 시드가 각각 만드는 문제는?
- 실행기 순서 문제의 네 요인과, 가장 실용적인 대처 원칙은?
- "순서가 중요한 두 콜백"에 대한 권고는?
- 콜백 그룹 구성 네 가지의 결정성 특성과, 권장 절충은?
- 시간 구동과 사건 구동의 다섯 축 비교와, 각각이 맞는 경로는?
- "과부하 시 자연스러운 드롭"이 왜 시간 구동의 장점인가.
- 최신값 사용 시 스탬프를 검사해야 하는 이유는?
- 결정성 시험 다섯 방법과, "완전 일치"를 요구하지 말라는 근거는?
- 시험에서 차이의 첫 발산 지점을 찾는 것이 왜 핵심인가.
- 결정성 목표 다섯 가지의 달성 가능성을 평가할 수 있는가.
- 실용적 지침 일곱 가지 중 "가장 가치 있는 조치"와 그 이유는?
다음 장 12.6은 제12부의 마지막이자 실용적 참조 — 성능 안티패턴 카탈로그다. 이 책 전체에서 만난 성능 함정을 한자리에 모은다.