██╗ ██╗██╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ██╗ █████╗
██║ ██║██║ ██║██╔══██╗██╔════╝ ╚════██╗██╔═████╗███║██╔══██╗
██║ █╗ ██║██║ █╗ ██║██║ ██║██║ █████╔╝██║██╔██║╚██║╚██████║
██║███╗██║██║███╗██║██║ ██║██║ ██╔═══╝ ████╔╝██║ ██║ ╚═══██║
╚███╔███╔╝╚███╔███╔╝██████╔╝╚██████╗ ███████╗╚██████╔╝ ██║ █████╔╝
╚══╝╚══╝ ╚══╝╚══╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚════╝
🎯 Session 411: Getting Started with Instruments
🏆 Performance Optimization for iOS & macOS Apps
WWDC 2019에서 소개된 세션 411 "Getting Started with Instruments"는 iOS 및 macOS 개발자들에게 필수적인 성능 최적화 도구인 Instruments의 사용법을 자세히 다룹니다. 이 세션에서는 Apple의 엔지니어들이 직접 실제 앱의 성능 문제를 진단하고 해결하는 과정을 보여주며, 개발자들이 자신의 앱에서도 동일한 방법을 적용할 수 있도록 실용적인 가이드를 제공합니다.
| 항목 | 내용 |
|---|---|
| 세션 제목 | Getting Started with Instruments |
| 세션 번호 | WWDC19-411 |
| 발표자 | Tibet Rooney-Rabdau, Ben Mitchell, Anand Subramanian |
| 카테고리 | Developer Tools |
| 난이도 | 초급 ~ 중급 |
발표자들은 세션 초반에 매우 중요한 메시지를 전달합니다:
"성능은 사용자와 앱 사이의 신뢰를 구축합니다"
현대의 모바일 사용자들은 앱이 즉각적으로 반응하고 부드럽게 작동하기를 기대합니다. 성능 문제는 다음과 같은 부정적인 결과를 초래할 수 있습니다:
Apple 엔지니어들은 다음과 같은 프로파일링 원칙을 강조합니다:
┌─ Time Profiler - WWDC 2019 세션 411 실제 화면 ─────────────────────┐
│ 🕒 Timeline │
│ ████████████████████████████████████████████████████████████████ │
│ 0s 10s 20s 30s 40s 50s 60s │
│ │
│ 📊 Call Tree - CPU Usage (실제 세션 데이터) │
│ ┌─────────────────────┬─────────┬────────┬───────────────────────┐ │
│ │ Symbol Name │ Weight │ Self │ Symbol │ │
│ ├─────────────────────┼─────────┼────────┼───────────────────────┤ │
│ │ parseData() │ 89.7% │ 89.7% │ SolarSystem.parseData │ │
│ │ loadDataFromFile() │ 5.2% │ 5.2% │ SolarSystem.loadData │ │
│ │ updateUI() │ 3.1% │ 3.1% │ SolarSystem.updateUI │ │
│ │ main() │ 2.0% │ 0.5% │ main │ │
│ └─────────────────────┴─────────┴────────┴───────────────────────┘ │
│ │
│ 🚨 병목 지점 발견: parseData() 함수가 전체 CPU 시간의 89.7% 차지! │
└──────────────────────────────────────────────────────────────────┘
Time Profiler는 앱의 CPU 사용률을 분석하는 가장 기본적이면서도 강력한 도구입니다.
📊 CPU 사용률 시각화
🕒 함수별 실행 시간 측정
📈 호출 스택 트레이스 분석
🎯 핫스팟(병목 지점) 식별
🔧 Instruments Time Profiler 실행 단계별 가이드
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ 1️⃣ Xcode에서 프로파일링 시작 │
│ 📱 Xcode → Product → Profile (⌘+I) │
│ ┌───────────────────────────────────────┐ │
│ │ [Xcode Menu Bar] │ │
│ │ Product │ │
│ │ ├─ Build │ │
│ │ ├─ Run │ │
│ │ ├─ Test │ │
│ │ └─ Profile ⌘I ← 여기 클릭 │ │
│ └───────────────────────────────────────┘ │
│ │
│ 2️⃣ Instruments 템플릿 선택 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Choose a profiling template for: │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Time │ │ Allocations │ │ Leaks │ │ │
│ │ │ Profiler │ │ │ │ │ │ │
│ │ │ 🎯 │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ [Choose] 버튼 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 3️⃣ 프로파일링 실행 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Instruments - Time Profiler [⏹] │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 🔴 Record │ │ │
│ │ │ ────────────────────────────────────────────────────── │ │ │
│ │ │ Recording... ███████████████████ │ │ │
│ │ │ Duration: 00:15.24 │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 4️⃣ 앱에서 성능 문제가 있는 작업 수행 │
│ • 앱의 느린 기능 실행 │
│ • 여러 번 반복 테스트 │
│ • 실제 사용자 시나리오 재현 │
│ │
│ 5️⃣ 프로파일링 종료 및 분석 │
│ 🛑 Stop 버튼 클릭 → 결과 분석 시작 │
└─────────────────────────────────────────────────────────────────────┘
Points of Interest는 Signpost API와 함께 사용하여 앱의 특정 영역을 추적하는 도구입니다.
┌─ Points of Interest - Signpost 타임라인 뷰 ────────────────────────┐
│ │
│ 📅 Timeline: Data Loading Process │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Network Request ████████████ │ │
│ │ Data Processing ██████████████ │ │
│ │ UI Update ████ │ │
│ │ ────────────────────────────────────────────────────────────── │ │
│ │ 0ms 500ms 1000ms 1500ms 2000ms 2500ms 3000ms │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 📋 Signpost Events: │
│ • [0ms] 🟢 Network Request - Begin │
│ • [1200ms] 🔵 Network Request - End (Status: 200) │
│ • [1205ms] 🟡 Data Processing - Begin │
│ • [2100ms] 🟠 Data Processing - End (Processed 1500 records) │
│ • [2105ms] 🟣 UI Update - Begin │
│ • [2450ms] 🟢 UI Update - End │
└──────────────────────────────────────────────────────────────────┘
import os.signpost
let log = OSLog(subsystem: "com.example.app", category: "networking")
// 작업 시작점 표시
os_signpost(.begin, log: log, name: "Data Download", "Starting download for user: %@", userID)
// 실제 작업 수행
performNetworkRequest()
// 작업 종료점 표시
os_signpost(.end, log: log, name: "Data Download", "Download completed successfully")
| 도구명 | 용도 | 주요 기능 |
|---|---|---|
| Allocations | 메모리 사용량 분석 | 메모리 누수, 과도한 할당 검출 |
| Leaks | 메모리 누수 탐지 | 해제되지 않은 객체 식별 |
| Energy Log | 배터리 사용량 분석 | CPU, GPU, 네트워크 등의 에너지 소모 측정 |
| Network | 네트워크 성능 분석 | 요청/응답 시간, 데이터 사용량 모니터링 |
세션에서는 "Solar System"이라는 샘플 앱의 성능 문제를 단계별로 해결하는 과정을 보여줍니다.
📱 Solar System 앱 - 성능 문제 재현
┌─────────────────────────────────────────────┐
│ 🪐 Solar System Explorer │
│ ┌─────────────────────────────────────────┐ │
│ │ │ │
│ │ 🌌 │ │
│ │ ⏳ Loading... │ │
│ │ (회전하는 스피너) │ │
│ │ │ │
│ │ 😟 UI가 3.2초간 응답 없음 │ │
│ │ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 사용자 행동: │
│ • 앱 실행 버튼 터치 │
│ • 화면이 멈춘 것처럼 보임 │
│ • 다른 버튼들도 반응하지 않음 │
│ • 사용자 좌절감 증가 📈 │
└─────────────────────────────────────────────┘
문제 분석:
🔍 Time Profiler 분석 결과 - Solar System 앱
┌────────────────────────────────────────────────────────────────────┐
│ 📊 CPU Usage Timeline (Main Thread) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 100% ████████████████████████████████████████████████████████ │ │
│ │ 90% █████████████████████████████████████████████████████████ │ │
│ │ 80% ██████████████████████████████████████████████████████ │ │
│ │ 70% █████████████████████████████████████████████████ │ │
│ │ 60% ████████████████████████████████████████████ │ │
│ │ 50% ███████████████████████████████████████ │ │
│ │ 40% ██████████████████████████████████ │ │
│ │ 30% █████████████████████████████ │ │
│ │ 20% ████████████████████████ │ │
│ │ 10% ███████████████████ │ │
│ │ 0% ────────────────────────────────────────────────────────── │ │
│ │ 0s 0.5s 1.0s 1.5s 2.0s 2.5s 3.0s 3.5s │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ 📈 핵심 발견사항: │
│ • Main Thread CPU 사용률: 95% (매우 높음!) │
│ • 지속 시간: 3.2초 │
│ • 핫스팟: parseData() 함수가 89.7% 차지 │
│ • UI 스레드 완전 블로킹 │
│ │
│ 🚨 결론: 메인 스레드에서 JSON 파싱 작업이 UI를 블로킹하고 있음 │
└────────────────────────────────────────────────────────────────────┘
상세 프로파일링 데이터:
함수별 CPU 사용률:
┌─────────────────────┬─────────┬────────┬─────────────────┐
│ 함수명 │ Weight │ Self │ 호출 횟수 │
├─────────────────────┼─────────┼────────┼─────────────────┤
│ parseData() │ 89.7% │ 89.7% │ 1회 │
│ loadDataFromFile() │ 5.2% │ 5.2% │ 1회 │
│ updateUI() │ 3.1% │ 3.1% │ 1회 │
│ main() │ 2.0% │ 0.5% │ 1회 │
└─────────────────────┴─────────┴────────┴─────────────────┘
문제가 된 원본 코드:
// 문제가 있는 코드 - 메인 스레드에서 파싱 수행
func loadSolarSystemData() {
let data = loadDataFromFile()
let parsedData = parseData(data) // 🚨 메인 스레드 블로킹!
updateUI(with: parsedData)
}
개선된 코드:
// 개선된 코드 - 백그라운드 큐에서 파싱 수행
func loadSolarSystemData() {
let data = loadDataFromFile()
// 백그라운드 큐에서 무거운 작업 수행
DispatchQueue.global(qos: .userInitiated).async {
let parsedData = parseData(data)
// UI 업데이트는 메인 큐에서 수행
DispatchQueue.main.async {
updateUI(with: parsedData)
}
}
}
🎯 성능 최적화 Before & After 비교
┌─────────────────────────────────────────────────────────────────────┐
│ BEFORE 최적화 전 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Main Thread: ████████████████████████████████████████████████ │ │
│ │ Background: ───────────────────────────────────────────────── │ │
│ │ UI Response: ❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌❌ (3.2초 블로킹) │ │
│ │ 0s 1s 2s 3s 4s │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ AFTER 최적화 후 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Main Thread: ████ │ │
│ │ Background: ─────██████████████████████████████████████████ │ │
│ │ UI Response: ✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ (즉시 반응) │ │
│ │ 0s 1s 2s 3s 4s │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
📊 상세 성능 메트릭 비교:
| 성능 지표 | 최적화 전 | 최적화 후 | 개선율 | 사용자 체감 |
|---|---|---|---|---|
| 메인 스레드 CPU | 95% | 15% | 84% ⬇️ | 앱이 멈춤 → 부드러움 |
| UI 반응 시간 | 3.2초 | 즉시 | 100% ⬇️ | 답답함 → 만족 |
| JSON 파싱 시간 | 3.2초 | 3.2초 | 변화없음 | 백그라운드에서 처리 |
| 앱 사용성 | ❌ 불가능 | ✅ 완전 가능 | 극적 개선 | 사용자 유지 |
| 배터리 효율성 | 나쁨 | 좋음 | 개선 | 발열 감소 |
🎉 핵심 성과:
<# Debug 모드 (개발 중)
- 최적화되지 않은 코드
- 디버그 심볼 포함
- 실제 성능과 차이 발생
# Release 모드 (배포용)
- 컴파일러 최적화 적용
- 실제 사용자 환경과 유사
- 정확한 성능 측정 가능
// ✅ 올바른 사용법
DispatchQueue.main.async {
// UI 업데이트만
label.text = result
activityIndicator.stopAnimating()
}
// ❌ 잘못된 사용법
DispatchQueue.main.async {
// 무거운 연산 수행 (메인 스레드 블로킹)
let result = performHeavyCalculation()
label.text = result
}
// 사용자 인터랙션에 직접적으로 영향
DispatchQueue.global(qos: .userInteractive).async { }
// 사용자가 시작한 작업
DispatchQueue.global(qos: .userInitiated).async { }
// 일반적인 작업
DispatchQueue.global(qos: .default).async { }
// 사용자가 인지하지 못하는 작업
DispatchQueue.global(qos: .utility).async { }
// 백그라운드 작업
DispatchQueue.global(qos: .background).async { }
// ❌ 강한 참조 순환 발생
class ViewController: UIViewController {
var completion: (() -> Void)?
func setupCompletion() {
completion = {
self.updateUI() // 강한 참조 순환!
}
}
}
// ✅ 약한 참조로 순환 방지
class ViewController: UIViewController {
var completion: (() -> Void)?
func setupCompletion() {
completion = { [weak self] in
self?.updateUI() // 약한 참조로 안전함
}
}
}
// 대용량 데이터 처리 시
func processLargeDataSet() {
autoreleasepool {
let largeData = loadLargeDataSet()
processData(largeData)
// autoreleasepool 종료 시 즉시 메모리 해제
}
}
import os.signpost
class NetworkManager {
private let signpostLog = OSLog(subsystem: "com.app.networking",
category: "requests")
func performRequest(url: URL) {
let signpostID = OSSignpostID(log: signpostLog)
os_signpost(.begin, log: signpostLog, name: "Network Request",
signpostID: signpostID, "URL: %@", url.absoluteString)
URLSession.shared.dataTask(with: url) { data, response, error in
defer {
os_signpost(.end, log: signpostLog, name: "Network Request",
signpostID: signpostID, "Status: %d",
(response as? HTTPURLResponse)?.statusCode ?? -1)
}
// 요청 처리 로직
}.resume()
}
}
func optimizeAlgorithm() {
os_signpost(.begin, log: signpostLog, name: "Algorithm Optimization")
// 알고리즘 실행 전 이벤트
os_signpost(.event, log: signpostLog, name: "Algorithm Optimization",
"Starting data preparation")
let preparedData = prepareData()
os_signpost(.event, log: signpostLog, name: "Algorithm Optimization",
"Data preparation completed, starting main algorithm")
let result = runMainAlgorithm(preparedData)
os_signpost(.end, log: signpostLog, name: "Algorithm Optimization",
"Algorithm completed with %d results", result.count)
}
복잡한 성능 문제를 해결할 때는 여러 Instrument를 동시에 사용하여 종합적인 분석을 수행할 수 있습니다:
Time Profiler + Allocations + Energy Log
→ CPU 사용률과 메모리 할당, 에너지 소모를 동시에 모니터링
Network + Points of Interest
→ 네트워크 요청과 앱의 주요 작업을 연관지어 분석
// ✅ 효율적인 셀 구성
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
// 백그라운드에서 이미지 로딩
loadImageAsync(for: indexPath) { image in
DispatchQueue.main.async {
// 셀이 아직 화면에 있는지 확인
if let visibleCell = tableView.cellForRow(at: indexPath) {
visibleCell.imageView?.image = image
}
}
}
return cell
}
// AppDelegate에서 측정
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
os_signpost(.begin, log: startupLog, name: "App Launch")
// 초기화 작업들...
os_signpost(.end, log: startupLog, name: "App Launch")
return true
}
// ❌ 델리게이트 강한 참조
class ParentViewController: UIViewController {
var childController: ChildViewController?
func setupChild() {
childController = ChildViewController()
childController?.delegate = self // 강한 참조 순환!
}
}
// ✅ 약한 참조로 해결
protocol ChildViewControllerDelegate: AnyObject {
func childDidFinish()
}
class ChildViewController: UIViewController {
weak var delegate: ChildViewControllerDelegate?
}
Apple에서 제공하는 샘플 프로젝트들을 활용하여 Instruments 사용법을 연습해보세요:
Instruments는 iOS 및 macOS 앱 개발에서 성능 최적화를 위한 필수 도구입니다. WWDC 2019 세션 411에서 소개된 내용을 바탕으로 다음과 같은 핵심 원칙을 기억하세요:
📋 성능 최적화 실행 체크리스트 (WWDC 2019 기준)
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ 🔍 1. 조기 발견 & 지속적 모니터링 │
│ ☐ 개발 초기 단계부터 Instruments 프로파일링 시작 │
│ ☐ 새 기능 개발 후 반드시 성능 테스트 실행 │
│ ☐ CI/CD에 성능 테스트 자동화 고려 │
│ ☐ 정기적인 성능 리뷰 일정 설정 │
│ │
│ 📱 2. 실제 환경 테스트 │
│ ☐ Release 모드에서 프로파일링 (Debug 모드 ❌) │
│ ☐ 지원하는 최소 사양의 실제 기기에서 테스트 │
│ ☐ 다양한 iOS 버전에서 성능 검증 │
│ ☐ 저전력 모드, 메모리 부족 상황 등 극한 조건 테스트 │
│ │
│ 🧵 3. 적절한 스레드 활용 │
│ ☐ 메인 스레드: UI 업데이트와 사용자 입력 처리만 │
│ ☐ 백그라운드 스레드: 네트워킹, 파일 I/O, 복잡한 연산 │
│ ☐ 적절한 QoS(Quality of Service) 레벨 선택 │
│ ☐ 스레드 간 데이터 전달 시 안전한 방법 사용 │
│ │
│ 📊 4. 데이터 기반 최적화 │
│ ☐ 추측 대신 Instruments 데이터를 바탕으로 결정 │
│ ☐ Time Profiler로 CPU 병목 지점 식별 │
│ ☐ Allocations으로 메모리 사용 패턴 분석 │
│ ☐ Signpost API로 커스텀 성능 지표 수집 │
│ │
│ 🔄 5. 지속적 성능 관리 │
│ ☐ 성능 회귀 방지를 위한 벤치마크 설정 │
│ ☐ 팀 내 성능 최적화 지식 공유 │
│ ☐ 사용자 피드백을 통한 실제 성능 이슈 모니터링 │
│ ☐ 정기적인 코드 리뷰에서 성능 관점 포함 │
└─────────────────────────────────────────────────────────────────────┘
"성능은 사용자와 앱 사이의 신뢰를 구축합니다"
"조기에, 그리고 자주 프로파일링하세요"
"추측하지 말고, 측정하세요"
성능 최적화는 일회성 작업이 아닌 지속적인 과정입니다. Instruments를 활용하여 사용자에게 더 나은 경험을 제공하는 앱을 만들어보세요!
이 글은 WWDC 2019 세션 411 "Getting Started with Instruments"를 바탕으로 작성되었습니다. 더 자세한 내용은 Apple Developer 사이트에서 확인하실 수 있습니다.
태그: #WWDC2019 #Instruments #iOS개발 #성능최적화 #Xcode #Swift #모바일개발