통신이나 펌웨어 성능을 이야기할 때 “응답이 느리다”와 “응답 시간이 들쭉날쭉하다”는 서로 다른 문제입니다. 첫 번째는 레이턴시(latency, 지연 시간)를, 두 번째는 지터(jitter, 타이밍 변동)를 살펴봐야 합니다.

예를 들어 어떤 장치는 명령을 받은 뒤 항상 10ms 후 동작하고, 다른 장치는 2ms 만에 동작하기도 하지만 17ms까지 걸리기도 합니다. 평균 응답 시간이 같아도 두 장치의 동작 특성은 다릅니다. 주기적으로 센서를 읽거나 모터를 제어할 때는 이 차이가 설계 판단에 영향을 줍니다.

이 글의 숫자는 개념 설명을 위한 가상 데이터입니다. 특정 장비의 실측 결과나 모든 시스템에 적용되는 허용 기준이 아닙니다.

1. 레이턴시는 무엇을 측정하는가

레이턴시는 "정의한 시작 이벤트부터 종료 이벤트까지 걸린 시간"입니다.

레이턴시 L = 종료 시각 - 시작 시각

“응답 시간이 3ms다”라는 설명만으로는 부족합니다. 무엇을 시작으로 보고 무엇을 완료로 보는지가 먼저 정해져야 합니다.

측정 대상 시작 이벤트 종료 이벤트
인터럽트 진입 지연 외부 인터럽트 신호 발생 ISR 진입 지점 실행
태스크 시작 지연 태스크 실행 예정 시각 실제 태스크 시작
태스크 실행 시간 함수 또는 태스크 시작 해당 실행 구간 종료
통신 편도 지연 송신 측의 정의된 전송 지점 수신 측의 정의된 수신 지점
명령 응답 시간 요청 전송 시작 응답을 받아 처리 완료
센서에서 출력까지의 지연 센서 샘플 획득 제어 결과가 출력에 반영

위 값들은 각각 유용하지만 동일한 구간을 측정하지 않습니다. 특히 함수 실행 시간만 측정하면, 함수가 실행되기 전에 큐에서 기다린 시간은 포함되지 않습니다.

네트워크 편도 지연도 송신과 수신의 측정 지점, 패킷 조건을 명확히 해야 비교할 수 있습니다. RFC 7679는 IP 패킷의 편도 지연 측정 기준을 정의합니다.

2. 지터는 무엇이 달라지는가

지터는 반복되는 이벤트의 지연 시간이나 발생 시각, 주기가 일정하지 않은 정도를 뜻합니다. 따라서 한 번의 측정으로 지터를 판단할 수는 없습니다.

다만 지터를 계산하는 공식이 모든 분야에서 하나로 정해져 있는 것은 아닙니다. 통신에서는 패킷별 지연 차이를, 주기 태스크에서는 실행 간격이나 예정 시각과의 오차를, 클럭에서는 신호 에지의 시간 오차를 볼 수 있습니다.

IP 패킷 지연 변동의 경우, RFC 3393은 선택한 두 패킷의 편도 지연 차이를 사용합니다. 지연 차이를 어떤 패킷 쌍으로 계산하는지까지 정의해야 합니다. RFC 5481은 서로 다른 패킷 지연 변동 지표의 적용을 설명합니다.

따라서 보고서에는 “지터 5ms”보다 “연속 샘플 지연 차이의 절댓값 평균 5ms” 또는 “지연 시간 최대−최소 범위 5ms”처럼 측정 대상을 적는 편이 좋습니다.

3. 평균 레이턴시가 같아도 지터는 다르다

다음 세 시스템의 응답 시간을 비교해 보겠습니다.

샘플 A: 일정한 응답 B: 변동하는 응답 C: 느리지만 일정한 응답
1 10ms 2ms 40ms
2 10ms 14ms 40ms
3 10ms 6ms 40ms
4 10ms 17ms 40ms
5 10ms 11ms 40ms
평균 10ms 10ms 40ms
최소 10ms 2ms 40ms
최대 10ms 17ms 40ms
최대−최소 0ms 15ms 0ms

A와 B는 평균 레이턴시가 같지만, B는 응답 시점을 예측하기 어렵습니다. 반대로 C는 A보다 느리지만 관측한 지연 변동은 없습니다.

같은 시점에 시작한 요청이 A에서는 항상 10ms 후, B에서는 2~17ms 후 완료되는 비교 그림

그림은 각 요청의 시작을 0ms로 정렬한 개념도입니다. 실제 요청을 모두 동시에 보냈다는 의미는 아닙니다.

특성 가능한 영향
낮은 레이턴시, 낮은 지터 빠르고 일정한 응답
높은 레이턴시, 낮은 지터 응답은 느리지만 타이밍을 예측하기 쉬움
낮은 평균 레이턴시, 높은 지터 대체로 빠르지만 특정 응답이 늦을 수 있음
높은 레이턴시, 높은 지터 응답이 느리고 변동도 큼

어떤 시스템이 적합한지는 데드라인과 용도에 달려 있습니다. 일정한 40ms 응답도 5ms 안에 완료해야 하는 제어 작업에는 사용할 수 없습니다.

4. 지터를 숫자로 표현하는 네 가지 방법

최대−최소 범위

측정한 지연의 최댓값과 최솟값을 뺍니다.

지연 범위 = max(L) - min(L)
B의 지연 범위 = 17 - 2 = 15ms

관측된 변동 폭을 설명하기 쉽지만, 하나의 극단값에 크게 영향을 받습니다. 측정 시간이 길어질수록 더 큰 값을 관측할 수 있으므로 샘플 수와 측정 시간도 함께 기록해야 합니다.

지연 시간의 표준편차

지연 값이 평균 주변에 얼마나 퍼져 있는지 나타냅니다. 여기서는 관측한 전체 데이터의 모집단 표준편차를 사용합니다.

평균 μ = (L1 + ... + LN) / N
표준편차 σ = sqrt(sum((Li - μ)^2) / N)

B의 편차: -8, +4, -4, +7, +1ms
제곱합: 64 + 16 + 16 + 49 + 1 = 146
σ = sqrt(146 / 5) ≈ 5.40ms

표준편차는 분포의 퍼짐을 요약합니다. 다만 발생 순서를 표현하지 않으므로, 같은 값들을 다른 순서로 나열해도 표준편차는 같습니다. 표본 표준편차를 사용하면 분모가 N - 1이 되므로 수치도 달라집니다.

연속 샘플의 지연 차이

바로 이전 샘플과 현재 샘플의 지연을 비교합니다.

ΔLi = Li - L(i-1)
B의 연속 차이: +12, -8, +11, -6ms
절댓값 평균 = (12 + 8 + 11 + 6) / 4 = 9.25ms

부호가 있는 차이는 지연이 증가했는지 감소했는지를 보여줍니다. 변동 크기를 평균낼 때 단순히 부호 있는 값을 평균하면 서로 상쇄될 수 있습니다. 위의 절댓값 평균은 이 글에서 명시적으로 선택한 요약 통계이며, 모든 도구가 사용하는 표준 지터 값은 아닙니다.

패킷에서는 송신 순서로 비교하는지 도착 순서로 비교하는지, 유실된 패킷을 어떻게 처리하는지도 결과에 영향을 줍니다.

프로토콜에서 정의한 추정값

RTP/RTCP에서는 도착 간격과 송신 타임스탬프 간격의 차이 D를 사용해 다음처럼 지터 추정값을 갱신합니다.

D(i-1, i) = (Ri - R(i-1)) - (Si - S(i-1))
Ji = J(i-1) + (abs(D(i-1, i)) - J(i-1)) / 16

R과 S는 동일한 RTP 타임스탬프 단위로 맞춰야 합니다. 패킷은 도착 순서로 갱신합니다. 이 값은 단순 표준편차나 전체 샘플의 절댓값 평균과 다릅니다. 정의는 RFC 3550, 6.4.1절에서 확인할 수 있습니다.

서로 다른 프로그램의 지터 값을 비교할 때는 계산식부터 확인해야 합니다.

5. 수신 간격의 변동은 지연 변동과 어떤 관계인가

패킷의 송신 시각을 Si, 수신 시각을 Ri, 편도 지연을 Li라고 하면, 같은 시간 기준에서 다음 관계가 성립합니다.

Ri = Si + Li
Ri - R(i-1) = (Si - S(i-1)) + (Li - L(i-1))

송신 주기가 일정한 T라면 수신 간격은 T에 지연 차이를 더한 값입니다.

B의 패킷을 20ms 간격으로 보냈다고 가정해 보겠습니다.

패킷 송신 시각 편도 지연 수신 시각 이전 패킷과의 수신 간격
1 0ms 2ms 2ms —
2 20ms 14ms 34ms 32ms
3 40ms 6ms 46ms 12ms
4 60ms 17ms 77ms 31ms
5 80ms 11ms 91ms 14ms

송신 간격은 항상 20ms인데, 수신 간격은 12~32ms로 달라집니다. 수신 간격에서 송신 간격을 빼면 앞에서 계산한 +12, -8, +11, -6ms가 나옵니다.

반대로 송신 자체가 불규칙하다면, 수신 간격만으로 전송 경로의 지연 변동을 분리하기 어렵습니다. 송신 시각을 함께 기록해야 하는 이유입니다. 패킷 재정렬이 발생하면 패킷 번호와 비교 순서도 관리해야 합니다.

6. RTOS에서는 시작 지터와 실행 시간을 나누어 봐야 한다

10ms 주기의 태스크가 있다고 해보겠습니다. 예정 시작 시각을 Pi, 실제 시작 시각을 Ai, 완료 시각을 Fi로 정의합니다.

예정 시작:  0, 10, 20, 30, 40ms
실제 시작:  0, 12, 21, 34, 41ms
시작 오차:  0,  2,  1,  4,  1ms
실제 간격:    12,  9, 13,  7ms

이때 시작 오차의 범위는 4 - 0 = 4ms이고, 실제 간격의 범위는 13 - 7 = 6ms입니다. 둘 다 타이밍 변동을 설명하지만 측정 대상이 다릅니다.

지표 식 확인하는 내용
시작 오차 Ai - Pi 예정 시각보다 얼마나 늦거나 일찍 시작했는가
실제 실행 간격 Ai - A(i-1) 실제 시작 사이의 간격은 일정한가
해당 구간의 경과 시간 Fi - Ai 시작 후 완료까지 얼마나 걸렸는가
예정 시각부터 완료까지 Fi - Pi 대기와 실행을 합쳐 얼마나 걸렸는가

태스크가 매번 1ms 만에 완료돼도, 실행 시작이 4ms 늦어지면 예정 시각부터 완료까지는 5ms가 걸립니다. 또한 Fi - Ai는 구간 중 선점이나 대기가 발생하면 그 시간까지 포함하므로, 실제 CPU 사용 시간과 구별해야 합니다.

한편 “작업이 끝난 다음 10ms 기다리는 방식”은 작업 시간 + 대기 시간이 전체 주기가 됩니다. 절대적인 예정 시각을 기준으로 다음 실행을 예약하면 이 누적을 줄일 수 있지만, 데드라인을 넘긴 경우의 처리 방식은 별도로 정해야 합니다.

예정 시각 Pi가 릴리스 시각이고 상대 데드라인이 5ms라면 완료 조건은 Fi <= Pi + 5ms입니다. 평균이 아니라 각 실행의 완료 여부를 봐야 합니다.

7. 임베디드 시스템에서 타이밍이 흔들리는 지점

다음은 원인을 찾기 위한 점검 예시입니다. 특정 현상의 원인이라고 단정하기보다, 구간별 측정으로 확인해야 합니다.

구간 변동을 만들 수 있는 조건 확인할 관측값
인터럽트 진입 전 인터럽트 마스킹, 더 높은 우선순위 ISR 입력 신호와 ISR 마커 사이 시간
태스크 실행 전 스케줄링, 우선순위, 락 대기 준비 상태부터 실제 시작까지
메모리 접근 캐시 상태, 외부 메모리, 버스 경합 부하 조건별 실행 구간 시간
통신 송신 전 소프트웨어 큐, 드라이버 큐 enqueue부터 실제 송신까지
통신 경로 매체 접근 대기, 혼잡, 오류 복구 실제 송신부터 실제 수신까지
결과 반영 버퍼링, 다음 출력 주기 대기 계산 완료부터 출력 반영까지

CAN 메시지는 큐와 버스 대기를 구분하기

애플리케이션이 메시지를 송신 큐에 넣은 시점과 CAN 버스에서 프레임 전송이 시작되는 시점은 같지 않을 수 있습니다. 다른 프레임과의 중재, 이미 진행 중인 전송, 오류 복구 조건에 따라 대기가 달라질 수 있습니다.

애플리케이션 enqueue
    → 소프트웨어/하드웨어 큐 대기
    → 버스 사용 가능 시점 및 중재 대기
    → 프레임 전송
    → 수신 측 처리

비트레이트만 올려도 해결되는 문제인지, 큐와 버스 부하를 함께 분석해야 하는 문제인지는 구간별 시간에 따라 달라집니다. CAN Timing 계산기는 비트 타이밍 설정을 확인하는 도구이며, 전체 메시지 응답 시간이나 최악 대기 시간을 분석하는 도구는 아닙니다.

로그가 측정을 바꾸기도 한다

ISR이나 주기 태스크 안에서 매번 문자열 로그를 출력하면 측정하려던 동작에 추가 작업이 들어갑니다. 메모리 버퍼에 타임스탬프만 기록하고 나중에 출력하거나, GPIO 마커를 이용하는 방식을 검토할 수 있습니다. 이 경우에도 버퍼 기록과 GPIO 접근의 오버헤드는 확인해야 합니다.

8. 제어와 스트리밍에서 영향이 달라지는 이유

센서 샘플링부터 제어 출력까지 지연이 크면, 제어기는 더 오래된 측정값으로 동작하게 됩니다. 샘플링이나 출력 간격까지 불규칙하면 고정 주기를 가정한 계산과 실제 시간 간격 사이에도 차이가 생깁니다. 영향의 크기는 제어 대상과 알고리즘에 따라 달라집니다.

따라서 “평균 응답 시간 1ms”만으로 충분한지 판단하지 말고, 샘플의 시각, 완료 시각, 데드라인 위반 여부를 함께 봐야 합니다. 실제 시간 간격을 사용하는 계산이 필요한지도 설계 조건에 따라 확인합니다.

음성·영상처럼 일정한 간격으로 데이터를 소비하는 경우에는 버퍼에 잠시 저장했다가 일정한 시각에 재생할 수 있습니다. 예를 들어 수신 변동을 흡수하기 위해 20ms를 더 기다리도록 구성하면 재생 간격을 안정시키는 데 도움이 되지만, 그만큼 재생 지연이 늘어납니다.

버퍼로 기다리는 전략은 모든 패킷이 정해진 재생 시각 전에 도착하는 조건에서 효과가 있습니다. 늦게 온 데이터, 유실, 시계 드리프트에 대한 별도 처리도 필요합니다. 즉 변동을 줄이기 위해 일정한 지연을 추가하는 절충이 생길 수 있습니다.

9. 측정할 때 놓치기 쉬운 함정

RTT를 무조건 2로 나누지 않기

왕복 시간에는 요청 경로, 상대 장치의 처리 시간, 응답 경로 등이 들어갑니다. 두 경로가 대칭이라고 확인되지 않았다면 RTT / 2는 실제 편도 지연과 다를 수 있습니다. RFC 7679는 편도 측정이 필요한 이유로 경로 비대칭을 설명합니다.

서로 다른 장치의 시계를 그대로 빼지 않기

송신 측 타임스탬프가 100ms이고 수신 측 타임스탬프가 105ms라고 해도, 두 시계의 기준이 다르면 지연이 5ms라고 결론 내릴 수 없습니다. 동기화 오차와 타임스탬프를 찍는 위치를 확인해야 합니다.

지연 차이를 계산하면 일정한 시계 오프셋이 상쇄될 수 있지만, 측정 중 시계의 진행 속도가 달라지는 드리프트까지 자동으로 제거되는 것은 아닙니다.

같은 MCU 내부의 이벤트를 측정할 때는 동일한 하드웨어 카운터를 쓰는 방법이 유용합니다. 카운터 해상도, 주파수 변동, 래핑, 읽기 오버헤드도 기록합니다. 낮은 해상도 때문에 관측한 변동이 0으로 나왔다고 실제 변동이 없다고 단정할 수는 없습니다.

평균만 보고 끝내지 않기

평균과 함께 중앙값, 상위 백분위, 관측 최대값, 데드라인 위반율을 확인하면 느린 구간을 더 잘 볼 수 있습니다.

데드라인 위반율 = 데드라인을 넘긴 완료 건수 / 전체 대상 건수

완료하지 못한 요청이나 유실된 패킷을 통계에서 조용히 빼면 결과가 좋아 보일 수 있습니다. 이들은 별도 실패 건수로 기록하고, 지연 통계의 모집단도 명시해야 합니다.

샘플 5개로 구한 P99는 장기적인 99백분위 성능을 판단하기에 충분하지 않습니다. 관측 최대값도 시스템의 이론적 최악 시간이나 WCET를 증명하지 않습니다.

10. 재현 가능한 측정 순서

  1. 구간을 정합니다. 예를 들어 “외부 에지에서 ISR 첫 GPIO 마커까지”로 정의합니다.
  2. 시각 기준을 정합니다. 동일 카운터, 외부 계측기, 동기화된 장치 시계 중 무엇을 사용하는지 적습니다.
  3. 순서와 원본 값을 저장합니다. 평균만 남기지 말고 이벤트 번호, 시작·종료 시각, 상태, 부하 조건을 함께 기록합니다.
  4. 유휴와 부하 상태를 나눕니다. 통신량 증가, 다른 태스크 실행, 로그 활성화 등 조건을 명확히 합니다.
  5. 시간 순서와 분포를 함께 봅니다. 특정 주기로 튀는지, 부하와 함께 증가하는지 확인합니다.
  6. 지표와 실패를 함께 보고합니다. 평균, 최대, 지연 범위, 선택한 지터 지표, 데드라인 위반·유실을 기록합니다.

보고서에는 다음처럼 적을 수 있습니다.

측정 구간: 센서 샘플 획득 → PWM 출력 반영
시간 기준: 동일 장치의 하드웨어 카운터
조건: 주기 1ms, 지정 통신 부하, 로그 비활성화
측정 길이 및 샘플 수: 실제 값을 기록
레이턴시: 평균 / 중앙값 / P99 / 관측 최대
변동 지표: 지연 시간의 모집단 표준편차와 최대−최소
데드라인: 샘플 시각으로부터 1ms
실패: 데드라인 위반, 누락, 계측 버퍼 오버플로 건수

11. Python으로 예제 값을 직접 계산하기

아래 코드는 외부 라이브러리 없이 실행할 수 있습니다. stddev_ms는 모집단 표준편차이고, mean_abs_adjacent_delta_ms는 입력 순서대로 인접한 샘플의 지연 차이 절댓값 평균입니다. P99는 nearest-rank 방식으로 계산합니다.

import math
import statistics


def summarize(latencies_ms):
    values = [float(value) for value in latencies_ms]
    if not values:
        raise ValueError("측정값이 필요합니다.")
    if any(not math.isfinite(value) or value < 0 for value in values):
        raise ValueError("지연은 유한한 0 이상의 값이어야 합니다.")

    ordered = sorted(values)
    deltas = [
        current - previous
        for previous, current in zip(values, values[1:])
    ]
    return {
        "count": len(values),
        "mean_ms": statistics.mean(values),
        "median_ms": statistics.median(values),
        "max_ms": max(values),
        "range_ms": max(values) - min(values),
        "stddev_ms": statistics.pstdev(values),
        "mean_abs_adjacent_delta_ms": (
            statistics.mean(abs(delta) for delta in deltas)
            if deltas else None
        ),
        "p99_nearest_rank_ms": ordered[math.ceil(0.99 * len(values)) - 1],
    }


for name, values in {
    "A": [10, 10, 10, 10, 10],
    "B": [2, 14, 6, 17, 11],
    "C": [40, 40, 40, 40, 40],
}.items():
    print(name, summarize(values))

B의 평균은 10ms, 지연 범위는 15ms, 표준편차는 약 5.40ms, 인접 지연 차이 절댓값 평균은 9.25ms입니다. 이 작은 예제에서는 nearest-rank P99가 최대값인 17ms가 됩니다. 실무에서는 충분한 샘플과 측정 조건을 갖추어 백분위를 해석해야 합니다.

12. 개선 목표를 분리해서 정하기

레이턴시를 줄이려면 먼저 시간이 어디에 쓰이는지 찾습니다. 실제 처리 비용, 큐 대기, 전송 시간, 출력 대기 중 큰 구간을 줄이는 방향으로 접근합니다.

지터를 줄이려면 매번 달라지는 조건을 찾습니다. 공유 자원 경쟁, 우선순위, 긴 임계 구역, 불규칙한 부하, 로그 출력 같은 요인을 구간별로 확인합니다. DMA나 우선순위 조정도 측정한 병목에 맞을 때 적용하고, 다른 작업에 주는 영향까지 확인합니다.

요구사항 우선 확인할 지표
사용자가 빠르게 응답을 받는가 응답 시간의 중앙값·상위 백분위
제어 작업이 기한 내 끝나는가 릴리스부터 완료까지 시간·데드라인 위반
샘플링이나 출력이 일정한가 시작 오차·실행 간격·출력 간격
스트림이 끊기지 않는가 도착 변동·버퍼 상태·유실·재생 지연

개선 전후에는 같은 조건과 같은 계산식으로 비교해야 합니다. 평균이 줄었지만 관측 최대값이나 데드라인 위반이 늘었다면, 목표에 맞는 개선인지 다시 판단해야 합니다.

레이턴시는 정해진 구간의 소요 시간이고, 지터는 반복되는 타이밍의 변동입니다. 요구사항을 “평균적으로 빠르게”와 “매번 정해진 시간 안에”로 나누면, 무엇을 측정하고 개선할지 더 명확해집니다.

참고 자료와 관련 도구