제12부 실시간·성능
12.6 성능 안티패턴 카탈로그
12.6.1 이 장에서 배우는 것 / 선수 지식
이 장은 참조용 카탈로그다. 앞의 106개 장에서 흩어져 나온 성능 함정을 한자리에 모아, 증상 → 원인 → 조치 → 해당 장으로 찾을 수 있게 한다.
카탈로그가 필요한 이유는 실무의 순서 때문이다. 성능 문제를 만났을 때 우리는 "어느 장에서 봤더라"부터 시작하는데, 증상에서 출발하는 색인이 있으면 그 시간이 사라진다. 11.1이 진단 전반의 색인이었다면, 이 장은 성능에 특화된 색인이다.
배우는 것:
- 통신·복사 안티패턴.
- 실행 모델 안티패턴.
- QoS·큐 안티패턴.
- tf·시간 안티패턴.
- 그래프 구조 안티패턴.
- 데이터·알고리즘 안티패턴.
- 관측성·설정 안티패턴.
- 선별 순서 — 무엇부터 볼 것인가.
선수 지식: 이 책 전반. 특히 제2부(실행 모델)·제3부(미들웨어)·11.7(측정)·12.4(지연 예산).
정직: 여기 모은 것은 흔한 패턴이며 완전한 목록이 아니다. 그리고 "안티패턴"이 항상 불리하다는 뜻은 아니다 — 맥락에 따라 정당한 선택인 경우가 있으므로, 각 항목의 "언제 괜찮은가"를 함께 읽어야 한다. 성능 수치는 제시하지 않는다(3.6.1). 이 환경에서 검증하지 않았다(목차 3).(→ 187차 확인: 대부분의 항목은 다른 장을 가리키는 색인이라 그 장들이 이미 개별적으로 실측됐다(목차 6). 이 장 자체에서는 "성능 수치를 제시하지 않는다"는 원칙을 지키는 항목 하나(12.6.8의 9·10번, 빌드 타입)에 한해 실측 수치를 직접 측정해서 채워 넣었다 — 이 값은 특정 벤치마크에 한정되며 일반화하지 않는다. 아래 실행 확인(187차) 참고. → 190차 점검: "다른 장을 가리키는 색인"이라는 위 서술을 실제로 검증했다 — 인용 대상 절을 하나씩 대조해 잘못된 인용 하나(12.6.7의 11번, GPU·CPU 왕복에 잘못된 7.8.5를 인용)와 더 정확한 인용으로 바꿔야 할 것 하나(12.6.8의 4번)를 찾아 고쳤고, 이 장의 두 항목(인트라프로세스·노드 분할)에 14.1.7의 실측 근거를 추가했다. 컨테이너 실행은 없었다 — 문서 대조만으로 확인 가능한 범위였다. → 193차 점검: 190차가 표본만 보고 남겨 둔 나머지 인용을 마저 전수 대조했다(약 49개 확인) — 같은 종류의 잘못된 인용을 6곳 더 찾아 고쳤다(12.6.3·12.6.4·12.6.5·12.6.6·12.6.7의 총 8개 셀 — 1.9.7→1.7.3, 2.7.5→2.7.4, 5.1.5→5.1.9/5.1.6, 5.2.3→5.1.9(2곳), 1.6.4→1.6.6). 새로 놓친 인용은 찾지 못했다 — 이번엔 전부 "잘못된 절을 가리킴" 유형이었다. 나머지 약 39개는 정합적이었다. 이번에도 컨테이너 실행은 없었다.)
12.6.2 선별 순서
증상을 만났을 때 보는 순서다.
| 증상 | 먼저 볼 절 |
|---|---|
| 데이터가 안 옴·유실 | 12.6.5(QoS) |
| 지연이 큼 | 12.6.3(복사)·12.6.4(실행) |
| CPU가 높음 | 12.6.3·12.6.7 |
| 주기가 불규칙 | 12.6.4·12.6.8 |
| 네트워크 포화 | 12.6.3·12.6.7 |
| 메모리 증가 | 12.6.5·12.6.7 |
| 시작이 느림 | 12.6.6 |
12.6.3 통신·복사
| # | 안티패턴 | 증상 | 조치 | 장 |
|---|---|---|---|---|
| 1 | 대용량 원본을 그대로 발행 | 대역 포화·CPU | 압축·다운샘플·전송 방식 | 7.2.7·7.8 |
| 2 | 인트라프로세스 미사용 | 불필요한 직렬화 | 컴포넌트 + use_intra_process_comms | 2.7.5(→ 190차 점검: 14.1.7에 이 패턴의 가장 구체적인 실측 근거가 있다 — 별도 프로세스 배치는 p99 60,626~73,272µs의 거대한 꼬리 지연을 보였지만 같은 컴포넌트를 컨테이너로 묶으니 p99 194µs로 약 310~370배 줄었다(180·185~187차).) |
| 3 | shared_ptr 발행 | 이동 최적화 상실 | unique_ptr | 2.7.5·12.3.5(→ 193차 정정: 2.7.5는 틀린 인용이다 — 그 절은 "알려진 제약"(TRANSIENT_LOCAL·deadline 등)을 다루지 shared_ptr/unique_ptr와 무관하다. 정확한 출처는 2.7.4("복사 0의 정확한 조건") — "규칙 1번: unique_ptr로 발행하라"가 바로 이 절에 있다.) |
| 4 | 불필요한 구독자 | 대역 배수 증가 | 필요한 것만·운영 중 도구 끄기 | 11.4.6 |
| 5 | 큰 메시지를 고빈도로 | 대역·직렬화 | 빈도 감소·ROI | 7.8.2 |
| 6 | 이미 압축된 것을 재압축 | CPU 낭비 | 판단 | 11.2.5 |
| 7 | 포인트클라우드 전 필드 전송 | 대역 | 필요 필드만 | 7.4.4 |
| 8 | 제로 카피 조건 미충족 | 이득 없음 | 고정 크기·설정 확인 | 12.3.5 |
| 9 | 이미지 원본 기록 | 디스크 용량 급증 | 압축·선별 | 11.3.2 |
언제 괜찮은가: 개발·디버깅 중 일시적으로는 원본 전송이 편리하다. 문제는 그것이 운영에 남는 것이다.
12.6.4 실행 모델
| # | 안티패턴 | 증상 | 조치 | 장 |
|---|---|---|---|---|
| 1 | 콜백에서 긴 작업 | 다른 콜백이 막힘 | 작업 분리·별도 스레드 | 2.3.4 |
| 2 | 콜백에서 블로킹 대기 | 교착·정지 | 비동기·별도 그룹 | 1.9.7(→ 193차 정정: 틀린 인용이다 — 1.9.7은 YAML 파일 문법을 다루는 절이라 교착과 무관하다. 정확한 출처는 1.7.3("데드락 — ROS 2에서 가장 비용이 큰 실수") — 15.3도 같은 주장에 이 절을 정확히 인용한다.) |
| 3 | 콜백 안에서 서비스 동기 호출 | 교착 | 비동기 호출 | 1.9.7(→ 193차 정정: 위와 같은 오인용, 정확한 출처는 1.7.3.) |
| 4 | 멀티스레드인데 기본 그룹 | 병렬 안 됨 | Reentrant 그룹 분리 | 2.3.3 |
| 5 | 모든 콜백 한 실행기 | 우선순위 없음 | 경로별 실행기 | 2.4·12.4.6 |
| 6 | 스레드 과다 | 문맥 전환·경합 | 필요한 만큼 | 2.2.6 |
| 7 | 타이머 주기 < 콜백 시간 | 적체·지터 | 주기 조정·최적화 | 2.5.8 |
| 8 | spin_some 오용 | 처리 누락·바쁜 대기 | spin·구조 재검토 | 2.2.4 |
| 9 | RT 스레드에서 로깅·할당 | 지터 | 카운터·비RT 발행 | 12.3.6 |
| 10 | 주기 구동으로 지연 추가 | 반주기 손실 | 사건 구동(해당 경로) | 12.4.5 |
| 11 | 콜백에서 sleep | 실행기 점유 | 타이머 | 2.5.8 |
언제 괜찮은가: 긴 콜백도 그것이 유일한 콜백이고 주기가 충분하면 문제없다. 노드가 커지면서 문제가 되므로, 구조를 미리 설계해 두는 것이 예방이다.
12.6.5 QoS·큐
| # | 안티패턴 | 증상 | 조치 | 장 |
|---|---|---|---|---|
| 1 | 센서에 RELIABLE | 재전송·지연·적체 | BEST_EFFORT | 1.6.3.1 |
| 2 | 큐 깊이 과다 | 오래된 데이터·메모리 | depth=1(최신 중요 시) | 1.4.4·12.4.4 |
| 3 | 큐 깊이 과소 | 유실 | 소비 속도 확인 | 1.4.4 |
| 4 | TRANSIENT_LOCAL에 대용량 | 메모리·재전송 | VOLATILE·크기 축소 | 1.6.3.3 |
| 5 | 무한 이력 | 메모리 증가 | KEEP_LAST | 1.6.3 |
| 6 | QoS 불일치 | 데이터 0 | topic info -v | 1.5.4 |
| 7 | 무선에서 RELIABLE 대용량 | 꼬리 지연 폭증(p99) | 압축·다운샘플링이 먼저 — 큰 메시지엔 BEST_EFFORT도 무효 | 3.9.13 |
| 8 | 데드라인·라이브라인 남용 | 오버헤드·복잡 | 필요한 곳만 | 1.6.4(→ 193차 정정: 1.6.4는 큐 깊이(depth) 설계를 다루는 절이라 데드라인·라이브라인과 무관하다. 더 정확한 출처는 1.6.6("시스템 차원의 QoS 관리") — deadline() 체이닝 실사용 예가 있다.) |
언제 괜찮은가: 기록·안전 명령은
RELIABLE이 맞다. 절충의 기준은 "최신이 중요한가, 전부가 중요한가"다(12.4.7).
12.6.6 tf·시간·그래프 구조
| # | 안티패턴 | 증상 | 조치 | 장 |
|---|---|---|---|---|
| 1 | tf 고빈도 발행 | 대역·CPU | 필요 주기로·정적은 static | 5.1.5(→ 193차 정정: 5.1.5는 "시간 축과 보간"(tf 조회의 시간 보간)을 다루지 발행 빈도와 무관하다. 정확한 출처는 5.1.9("성능 고려") — "조회 빈도 감소" 완화책이 표에 정확히 있다.) |
| 2 | 정적 변환을 /tf로 반복 | 낭비 | /tf_static | 5.1.5(→ 193차 정정: 같은 오인용, 정확한 출처는 5.1.6("정적 변환 — 왜 별도인가") — "변하지 않는 변환을 계속 발행하는 것은 낭비다"로 시작한다.) |
| 3 | tf 조회를 콜백마다 반복 | CPU | 캐시·일괄 | 5.2.3(→ 193차 정정: 5.2.3은 Time(0)의 조회 정확도 의미론을 다루지 캐싱과 무관하다. 정확한 출처는 5.1.9 — "조회 결과 캐싱"이 완화책 표에 정확히 있다.) |
| 4 | 버퍼 시간 과다 | 메모리 | 필요한 만큼 | 5.2.2 |
| 5 | 프레임 과다 | 조회 비용·시각화 | 정리 | 5.1.8 |
| 6 | 노드를 과도하게 분할 | 경계 비용 누적 | 컴포넌트화 | 2.7.3·12.4.5(→ 190차 점검: 14.1.7도 함께 보라 — 컴포넌트 컨테이너로 묶었을 때의 실측 개선폭(p99 약 310~370배)이 여기 있다.) |
| 7 | 노드 수 과다 | 디스커버리 폭증 | 통합·Discovery Server | 3.3.6·3.4 |
| 8 | 디스커버리 설정 방치 | 시작 지연·대역 | 설정·서버 | 3.10.4 |
| 9 | 큰 파라미터를 자주 갱신 | 오버헤드 | 필요 시만 | 1.9.6 |
| 10 | 서비스로 스트리밍 | 부적합 | 토픽·액션 | 1.8.3 |
언제 괜찮은가: 노드 분할은 개발·재사용에 이롭다. 지연이 문제가 되는 경로만 합치면 되며, 컴포넌트화가 두 목적을 함께 만족시킨다.
12.6.7 데이터·알고리즘·자원
| # | 안티패턴 | 증상 | 조치 | 장 |
|---|---|---|---|---|
| 1 | 전 해상도로 전 처리 | CPU·지연 | 다운샘플·ROI | 7.4.5·7.8.4 |
| 2 | 매 프레임 전체 재계산 | CPU | 증분·캐시 | 8.3.5 |
| 3 | 코스트맵 과대·고해상도 | CPU·메모리 | 크기·해상도 조정 | 8.3.8 |
| 4 | 무제한 계획 시간 | 지연 폭증 | 시간 제한(anytime) | 9.2.8·12.1.3 |
| 5 | 불필요한 좌표 변환 반복 | CPU | 한 번에·일괄 | 5.2.3(→ 193차 정정: 12.6.6 1~3번과 같은 오인용 — 5.2.3은 캐싱과 무관하다. 정확한 출처는 5.1.9("성능 고려", "조회 결과 캐싱" 완화책).) |
| 6 | 복사 남발(값 전달) | CPU·메모리 | 참조·이동 | 12.3 |
| 7 | RT 경로 동적 할당 | 지터 | 사전 예약 | 12.3.6 |
| 8 | 메모리 누수 | 장기 운전 실패 | RSS 추이 감시 | 11.7.6 |
| 9 | CPU 여유 없음 | 지터 급증 | 여유 확보·부하 분산 | 12.2.7 |
| 10 | 디스크 I/O 병목 | 기록 유실·정지 | SSD·기록량 | 11.2.6 |
| 11 | GPU·CPU 왕복 과다 | 지연 | 파이프라인 유지 | 7.8.5(→ 190차 점검: 이 인용이 틀렸다 — 7.8.5는 역압·큐 설계(드롭-if-busy)를 다루지 GPU·CPU 왕복과 무관하다. GPU 가속 파이프라인 자체는 이 책이 명시적으로 범위 밖에 두고 자매서 nvidia에 넘긴 주제라(7.8.1), 이 책 안에는 해당하는 절이 없다.) |
| 12 | 열 스로틀링 방치 | 장시간 열화 | 냉각·측정 | 12.2.7 |
언제 괜찮은가: 정확도가 최우선인 오프라인 처리에서는 전 해상도 처리가 옳다. 문제는 실시간 경로에 그것을 두는 것이다.
12.6.8 관측성·설정
| # | 안티패턴 | 증상 | 조치 | 장 |
|---|---|---|---|---|
| 1 | 고빈도 콜백에서 로깅 | CPU·I/O·지터 | 스로틀 로깅·레벨 | 1.11.5 |
| 2 | DEBUG를 운영에 | 로그 폭주·디스크 | 레벨 관리 | 1.11.4 |
| 3 | 운영 중 RViz2 구독 | 대역·CPU | 필요 시만 | 11.4.6 |
| 4 | 온보드에서 GUI | 제어 방해 | 원격 | 11.4.5(→ 190차 점검: 더 정확한 출처는 11.4.6이다 — 이 항목과 정확히 일치하는 "개발 PC에서 실행 + 네트워크로 토픽(권장)" 권고표가 그쪽에 있다. 11.4.5는 RViz2가 느려지는 원인 목록이라 방향은 맞지만 이 항목의 직접 출처는 아니다.) |
| 5 | 모든 토픽 상시 기록 | 디스크·CPU | 계층적 기록 | 11.3.2 |
| 6 | 기록 중 압축 | CPU 부족 → 유실 | 후처리 | 11.2.5 |
| 7 | 트레이싱 상시 | 오버헤드 | 필요 시만 | 11.6.7 |
| 8 | 측정 도구가 대상 교란 | 오판 | 유무 비교 | 11.7.8 |
| 9 | 디버그 빌드로 운영 | 큰 성능 손실 | Release·RelWithDebInfo | 4.5.6 |
| 10 | 최적화 플래그 미설정 | 성능 손실 | 빌드 설정 확인 | 4.5.6 |
| 11 | use_sim_time 오설정 | 이상 동작 | 전체 일관 | 1.10.9 |
| 12 | 절전·거버너 방치 | 지터 | performance | 12.2.7 |
실행 확인(187차) — 9·10번("디버그 빌드로 운영"·"최적화 플래그 미설정")에 이 장이 원칙상 제시하지 않는 성능 수치(12.6.1)를, 이 항목 하나에 한해 실측으로 채워 넣었다. CPU 위주 연산(350×350 순수 삼중 루프 행렬곱, BLAS 미사용)을 하는 최소 rclcpp 노드를 만들어,
CMAKE_BUILD_TYPE을 ① 미지정 ②Debug③Release로 각각 별도 워크스페이스에서 빌드했다. 실제 컴파일 플래그를flags.make로 직접 확인하니 — 미지정은-std=gnu++17뿐(최적화 플래그 전무),Debug는-g -std=gnu++17(역시 최적화 플래그 없음, 디버그 심벌만 추가),Release는-O3 -DNDEBUG -std=gnu++17— 미지정이Release가 아니라 사실상Debug와 같은 컴파일 결과물임을 소스 증거로 확인했다(4.2.10·4.5.13의 "CMAKE_BUILD_TYPE 미지정" 함정이 정확히 이것). 실행 결과: 미지정 301.796ms,Debug299.717ms(둘이 0.7% 이내로 사실상 동일),Release47.730ms —Release가 미지정·Debug대비 약 6.3배 빠르다. 정직한 단서: 이 6.3배는 이 특정 CPU 위주 합성 벤치마크(순수 삼중 루프 행렬곱)에서 측정한 값이며, 이 장의 "성능 수치를 제시하지 않는다"는 원칙대로 일반화하지 않는다 — I/O 위주이거나 이미 최적화된 라이브러리 호출이 지배하는 워크로드는 격차가 훨씬 작을 수 있다. 원본:tools/gap_harness/gap187/build_type_bench/(pkg/·results/compiler_flags.log·results/run_ws_{none,debug,release}.log·findings_summary.txt).
견해: 9번(디버그 빌드로 운영)이 의외로 자주 일어난다. 개발 중 디버그로 빌드해 놓고 그대로 현장에 가져가면, 원인 불명의 전반적 성능 저하를 겪는다. 빌드 타입을 확인하는 것을 배포 점검에 넣으라(4.7).
견해: 1번(고빈도 로깅)은 이 책이 여러 번 경고했지만 계속 나온다. 개발 중 넣은
RCLCPP_INFO가 남아 1 kHz로 실행되는 경우가 흔하다.RCLCPP_*_THROTTLE을 기본 습관으로 삼으면 예방된다(1.11.5).
12.6.9 오해와 진실
성능에 관한 흔한 오해를 정리한다.
| 오해 | 진실 |
|---|---|
| "멀티스레드로 하면 빨라진다" | 콜백 그룹이 맞아야(2.3.3) |
| "노드를 나누면 병렬이 된다" | 경계 비용이 추가된다(12.4.5) |
| "RELIABLE이 안전하다" | 센서에는 해롭다(1.6.3.1) |
| "큐를 키우면 안전하다" | 지연이 커진다(12.4.4) |
| "RT 커널이면 실시간" | 전 층이 필요(12.1.4) |
| "알고리즘이 병목" | 대개 아니다 — 측정하라(12.4.5) |
| "평균이 좋으면 괜찮다" | 꼬리가 문제(11.7.3) |
| "시뮬 성능 = 실기 성능" | 다르다(10.6.2) |
| "개발 PC에서 되니까 괜찮다" | 임베디드는 다르다(11.7.7) |
견해: "알고리즘이 병목"이라는 오해가 가장 많은 시간을 낭비시킨다. 실제로는 복사·직렬화·실행기 대기·QoS 재전송이 지배하는 경우가 많다. 11.7.4의 구간 분해를 먼저 하면 이 낭비가 사라진다.
12.6.10 제12부를 마치며
제12부가 반복한 것은 셋이다.
- 실시간은 빠름이 아니라 보장이다. 평균이 아니라 최악값이 사양이고, 그 보장은 전 경로에서 나온다. 한 층만 수정해서는 얻어지지 않는다(12.1·12.2·12.3).
- 요구를 계층으로 나누고, 하드 요구는 아래로 밀어라. 모든 것을 ROS 2에서 실시간으로 만들려는 시도가 어려움을 자초한다. 하드 루프는 MCU·드라이브에, 안전은 독립 경로에 두는 것이 표준적 방법이다(12.1.7·12.1.8).
- 설계하고, 측정하고, 그다음 최적화하라. 지연 예산이 목표를 주고, 구간 측정이 순서를 정하고, 그 결과가 어느 층을 고칠지 알려준다. 순서를 뒤집으면 비용만 든다(12.4·11.7).
그리고 이 부가 앞부분에 부여하는 의미 — 제2부(실행 모델)에서 다룬 실행기·콜백 그룹, 제3부(미들웨어)의 QoS, 2.7의 컴포넌트는 모두 여기서 성능 도구가 된다. 구조를 이해한 것이 성능을 통제하는 힘이 되는 것이 ROS 2의 성격이다.
12.6.11 체크포인트
- 성능 문제 선별의 첫 단계와, 증상별로 먼저 볼 절을 말할 수 있는가.
- 통신·복사 안티패턴 아홉 가지 중, 인트라프로세스 관련 셋은?
- 실행 모델 안티패턴에서 "교착"으로 이어지는 것 둘은?
- "멀티스레드인데 병렬이 안 되는" 원인과 조치는?
- QoS 안티패턴 여덟 가지와, 절충의 판단 기준은?
- tf 관련 안티패턴 다섯 가지를 들 수 있는가.
- 노드 분할의 비용과, 그것을 완화하는 방법은?
- 데이터·알고리즘 안티패턴에서 "장기 운전 실패"로 이어지는 것은?
- 관측성 안티패턴 중, 배포 점검에 넣어야 할 것은?
- 고빈도 로깅을 예방하는 습관은?
- 성능에 관한 아홉 가지 오해를 반박할 수 있는가.
- "가장 많은 시간을 낭비시키는 오해"와 그 대처는?
- 제12부가 반복한 세 원칙을 말할 수 있는가.
여기까지가 제12부 실시간·성능이다. 정의와 한계(12.1), OS(12.2), 메모리(12.3), 설계(12.4), 결정성(12.5), 안티패턴(12.6).
지금까지 로봇을 동작하게 만들고, 관측하고, 시간을 지키게 했다. 그런데 아직 다루지 않은 질문이 있다 — 이 시스템은 안전한가? 여기서 안전은 물리적 안전(8.7)이 아니라 보안이다. 로봇이 네트워크에 연결되어 있다면 누가 명령을 보낼 수 있는가, 데이터를 볼 수 있는가, 노드를 사칭할 수 있는가. 제13부 보안(13.1~13.5) 의 주제다.