os_signpost와 xctrace로 핵심 경로 성능 게이트 구축하기
로컬 Instruments 측정에서는 “첫 화면이 약 1초”로 보였지만, 병합된 버전을 원격으로 테스트하자 눈에 띄게 느려지는 경우가 있습니다. 대개 문제는 도구가 아니라 측정 대상의 경계가 불분명하다는 데 있습니다. 네트워크 대기, JSON 파싱, 데이터베이스 쓰기, UI 반영을 하나의 총소요 시간으로 묶으면 시스템 상태가 조금만 달라져도 결과를 비교하기 어려워집니다. 더 신뢰할 수 있는 방법은 os_signpost로 비즈니스 구간을 표시하고, 클라우드 Mac의 xctrace에서 동일한 시나리오를 반복 수집하는 것입니다.
반복 가능한 핵심 경로부터 정의하기
먼저 “앱 전체가 얼마나 빠른가”를 묻지 말고, 시작점과 종료점이 명확한 사용자 동작을 선택합니다. 예를 들어 주문 페이지에 진입한 뒤 로컬 캐시 읽기를 시작하는 시점부터 첫 번째 목록 셀 묶음이 데이터 바인딩을 완료하는 시점까지를 측정할 수 있습니다. 이 경로를 다음 세 구간으로 나눕니다.
| 구간 | 시작점 | 종료점 | 게이트 적용 적합성 |
|---|---|---|---|
load_cache |
캐시 읽기 요청 | 구조화된 데이터 반환 | 적합 |
decode_payload |
디코딩 시작 | 도메인 객체 생성 | 적합 |
render_first_batch |
첫 번째 모델 묶음 전달 | 첫 번째 뷰 묶음의 레이아웃 완료 | 적합 |
wait_remote_api |
외부 요청 시작 | 응답 수신 | 진단 전용 |
외부 네트워크 시간은 회선과 서버 상태의 영향을 받으므로 병합을 직접 차단하는 기준으로 사용하기에는 적합하지 않습니다. 성능 게이트는 프로세스 내부에서 실행되고, 입력이 고정되어 있으며, 반복적으로 재현할 수 있는 작업을 우선 대상으로 삼아야 합니다.
하나의 구간은 하나의 질문에만 답해야 합니다. 동일한 표시 구간에 네트워크, 디스크, 렌더링이 모두 포함되면 제한을 초과해도 어디를 수정해야 하는지 판단할 수 없습니다.
안정적이고 연관 가능한 계측 추가하기
고정된 subsystem과 category를 사용하고, 구간 이름에 사용자 데이터를 이어 붙이지 마세요. 동시 실행 작업을 측정해야 한다면 각 비즈니스 동작마다 별도의 OSSignpostID를 생성해 인접한 두 작업의 시작점과 종료점이 잘못 연결되지 않도록 합니다.
import os
enum PerfTrace {
static let log = OSLog(
subsystem: "com.example.mobile",
category: "CriticalPath"
)
static func begin(_ name: StaticString) -> OSSignpostID {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: name, signpostID: id)
return id
}
static func end(_ name: StaticString, id: OSSignpostID) {
os_signpost(.end, log: log, name: name, signpostID: id)
}
}
호출부에서는 반드시 defer로 구간을 닫아야 합니다. 그렇지 않으면 예외가 발생하는 분기에 시작점만 있고 종료점은 없는 구간이 남습니다.
let traceID = PerfTrace.begin("decode_payload")
defer { PerfTrace.end("decode_payload", id: traceID) }
let models = try decoder.decode([Item].self, from: fixtureData)
토큰, 파일 내용, 사용자 식별자를 signpost 텍스트에 기록하지 마세요. 성능 증거는 빌드 산출물에 포함되므로 각 필드는 카디널리티를 낮게 유지하고 안전하게 보관할 수 있어야 합니다.
xctrace 실행 조건 고정하기
먼저 그래픽 인터페이스에서 템플릿 이름이 올바르고 측정 구간이 실제로 표시되는지 확인한 다음 명령줄로 전환합니다. 테스트 앱에는 고정된 데이터 픽스처, 고정된 시뮬레이터 모델, 동일한 빌드 구성을 사용해야 합니다. 수집을 시작하기 전에 기존 프로세스를 종료해 이전 실행 상태가 남지 않도록 합니다.
set -euo pipefail
OUT="$PWD/perf-artifacts"
mkdir -p "$OUT"
xcrun xctrace record \
--template "Points of Interest" \
--time-limit 45s \
--output "$OUT/critical-path.trace" \
--launch -- "$APP_PATH" \
-PerfScenario first-batch \
-PerfFixture "$PWD/Fixtures/items.json"
xcrun xctrace export \
--input "$OUT/critical-path.trace" \
--toc > "$OUT/toc.xml"
xctrace export의 XPath와 테이블 구조는 도구 버전에 따라 달라질 수 있습니다. 따라서 파싱 스크립트는 먼저 대상 schema가 존재하는지 확인한 뒤 구간을 읽어야 합니다. 열 위치에 의존하는 awk를 장기적인 인터페이스로 사용하지 마세요. 내보낸 XML을 우선 파싱하고, 버전을 업그레이드할 때 계약 테스트에 사용할 최소 trace 파일을 하나 보관하는 편이 좋습니다.
LemonVM 원격 환경에서 실행할 때는 먼저 xcodebuild -version, 시스템 버전, 빌드 구성, 테스트 픽스처 해시를 기록합니다. 노드나 도구 체인이 변경되면 새 기준선을 구축해야 하며, 서로 다른 환경에서 얻은 수치를 억지로 비교해서는 안 됩니다.
여러 차례의 샘플로 임계값 설정하기
단 한 번의 실행은 최초 로딩, 백그라운드 인덱싱, 온도 상태의 영향을 받기 쉽습니다. 먼저 한 차례 워밍업한 다음, 최소 다섯 차례의 유효한 수집을 수행하는 것이 좋습니다. 성능 게이트에서는 중앙값을 비교하되 가장 느린 샘플에는 여유 있는 상한을 별도로 설정합니다.
기준선 중앙값이 420 ms라면 경고선을 기준선의 1.15배인 483 ms로 설정할 수 있습니다. 다만 이 비율을 모든 구간에 그대로 적용해서는 안 됩니다. 수십 밀리초가 걸리는 작은 함수와 수초가 걸리는 데이터 준비 작업은 변동 양상이 서로 다릅니다.
판정 스크립트는 최소한 다음 세 가지 결과를 구분해야 합니다.
- trace를 생성할 수 없음: 인프라 실패이며 성능 회귀로 판정하지 않습니다.
- 대상 구간 누락: 시나리오 또는 계측 실패이므로 테스트를 수정해야 합니다.
- 유효한 샘플이 임계값 초과: 성능 게이트 실패로 판정하고 증거를 보관합니다.
기준선 파일은 코드와 함께 검토해야 하며 구간 이름, 샘플 수, 중앙값, 허용 상한, 도구 체인 버전, 픽스처 해시를 포함해야 합니다. 임계값을 높이는 모든 변경은 커밋 설명에 이유를 명시해 기준이 계속 완화되기만 하는 상황을 방지해야 합니다.
흔한 가짜 회귀 제외하기
첫 실행만 느리고 이후에는 안정적이라면 대개 캐시 또는 동적 링크 준비가 원인입니다. 이 경우 콜드 스타트와 웜 경로를 두 그룹으로 분리해야 합니다. 모든 실행이 느릴 때는 코드 변경 사항을 확인합니다. 특정 실행 한 번만 비정상적이라면 당시 인덱싱, 백업, 추가 시뮬레이터 또는 병렬 빌드가 실행 중이었는지 먼저 살펴봅니다.
다음 항목도 확인해야 합니다.
- Release 데이터와 Debug 데이터를 혼용하지 않습니다.
- 시뮬레이터 시스템 버전별로 기준선을 따로 관리합니다.
- 매 테스트 실행 전에 동일한 비즈니스 상태를 복원합니다.
- 성능 작업과 대규모 빌드 작업을 동시에 실행하지 않습니다.
- trace, 파싱 결과, 표준 오류 출력을 함께 보관합니다.
- 구간을 파싱하지 못했을 때 기본값으로
0 ms를 기록하지 않습니다. - 시간 초과로 종료되면 테스트 대상 앱과 남아 있는 수집 프로세스를 종료합니다.
테스트가 그래픽 세션에 의존한다면 작업을 시작하기 전에 화면 표시 여부를 확인하는 사전 검사를 추가해야 합니다. 순수한 파싱 또는 저장소 계층만 측정한다면 독립된 테스트 타깃을 사용해 UI 상태에서 발생하는 노이즈를 줄이는 것이 좋습니다.
실패 결과를 즉시 활용할 수 있게 만들기
성능 게이트 출력에 “성능 실패”만 표시해서는 안 됩니다. 최소한 구간 이름, 현재 중앙값, 기준선, 변화율, 유효한 샘플 수, trace 경로를 출력해야 합니다. 예시는 다음과 같습니다.
FAIL render_first_batch
median_ms=512
baseline_ms=420
change=21.9%
samples=7
trace=perf-artifacts/run-06.trace
검토자는 결과를 확인한 뒤 해당 trace를 바로 다운로드하고 구체적인 구간으로 이동해, 비즈니스 코드의 회귀인지 측정 환경의 이상인지 판단할 수 있어야 합니다. 안정적인 성능 게이트의 목표는 실행할 때마다 완전히 동일한 수치를 얻는 것이 아닙니다. 동일한 입력, 동일한 도구 체인, 동일한 실행 조건에서 발생한 변화를 설명하고 재검증할 수 있게 만드는 것입니다. 신뢰하기 어려운 지표 수십 개를 한꺼번에 추가하기보다 가치가 높은 경로 하나에서 시작해 변동을 지속적으로 관찰한 뒤 구간을 점진적으로 늘리는 편이 더 효과적입니다.
자주 묻는 질문
성능 게이트는 평균값과 중앙값 중 무엇을 사용해야 하나요?
여러 유효 실행의 중앙값을 우선 사용하고 높은 백분위수나 최대 허용값을 함께 제한해야 합니다. 평균값은 한 번의 시스템 변동에 쉽게 왜곡됩니다.
xctrace 기록 실패를 성능 회귀로 판정해야 하나요?
아닙니다. 기록 실패, 측정 구간 누락, 성능 한도 초과에 서로 다른 종료 코드를 사용하고 유효한 표본을 얻은 뒤에만 성능 회귀를 판정해야 합니다.
모든 os_signpost 구간을 CI에서 검사해도 되나요?
시작과 종료 지점이 안정적이고 고정 입력으로 반복할 수 있는 핵심 경로만 검사해야 합니다. 수동 조작이나 외부 실시간 서비스에 의존하는 구간은 진단용으로 남깁니다.
작업 기간에 맞춰 LemonVM 클라우드 Mac 선택
Lemon M4와 Lemon M4 Pro는 싱가포르, 도쿄, 서울, 홍콩, 미국 서부에 제공되며 일간, 주간, 월간, 분기별 결제를 지원합니다.