-
Mach-O 파일 구조 분석해보며 최적화 해보기iOS 2025. 9. 13. 10:03
안녕하세요. 그린입니다 🍏
이번 포스팅은 iOS 앱의 Mach-O 파일 구조를 깊이 분석하고, 실제 앱 크기를 줄이는 최적화 방법들을 알아보겠습니다 🙋🏻

Mach-O Deep Dive
개발하면서 이런 궁금증 가져본 적 있지 않나요?
- "내 앱이 왜 이렇게 크지? 🤔"
- "App Store Connect에서 보는 앱 크기 리포트는 뭘 의미하는 거지?"
- "Universal Binary와 일반 바이너리의 차이가 뭐야?"
- "링크 타임에 뭐가 일어나고 있는 건가?"
바로 이런 의문들을 해결할 수 있는 것이 Mach-O 파일 구조를 이해하는 것입니다.
iOS의 모든 실행 파일은 Mach-O 포맷으로 되어 있고, 이 구조를 이해하면 앱 크기 최적화의 핵심을 파악할 수 있어요.
Why Mach-O Architecture Matters More Than Ever?
📈 앱 크기의 현실
- App Store 200MB OTA 제한 (WiFi 없이 다운로드)
- 4GB 전체 앱 크기 제한
- 사용자 64%가 50MB 이상 앱 다운로드 망설임
- 앱 크기 10MB 증가 시 다운로드율 23% 감소
🚀 iOS 트렌드
- iOS 17에서 새로운 압축 알고리즘 도입
- Universal Binary 필수화 (Apple Silicon Mac 지원)
- SwiftUI Preview와 개발용 심볼의 크기 증가
- Machine Learning 모델 내장으로 인한 크기 폭증
Mach-O 파일 구조 기본 이해
Mach-O는 macOS/iOS의 모든 실행 파일과 라이브러리가 사용하는 포맷이에요.
🎯 전체 구조 개요
Mach-O 파일 구조: ┌─────────────────┐ │ Mach Header │ ← 파일 메타데이터 (32/64-bit, CPU 타입 등) ├─────────────────┤ │ Load Commands │ ← 세그먼트/라이브러리 로드 지시사항 ├─────────────────┤ │ __TEXT 영역 │ ← 실행 코드 + 읽기전용 데이터 ├─────────────────┤ │ __DATA 영역 │ ← 전역변수 + 초기화된 데이터 ├─────────────────┤ │ __LINKEDIT │ ← 심볼 테이블 + 재배치 정보 └─────────────────┘
🎯 실제 구조 분석하기
# 기본 Mach-O 정보 확인 otool -hv YourApp.app/YourApp # 출력 예시: # Mach header # magic cputype cpusubtype caps filetype ncmds sizeofcmds flags # MH_MAGIC_64 X86_64 ALL 0x00 EXECUTE 45 5616 NOUNDEFS DYLDLINK TWOLEVEL PIE # 세그먼트별 크기 분석 otool -l YourApp.app/YourApp | grep -A5 segname # 출력 예시: # segname __TEXT # vmaddr 0x0000000100000000 # vmsize 0x0000000000004000 # fileoff 0 # filesize 16384
🎯 세그먼트 상세 분석
// Mach-O 세그먼트 정보를 파싱하는 유틸리티 import Foundation struct MachOAnalyzer { struct SegmentInfo { let name: String let vmAddress: UInt64 let vmSize: UInt64 let fileOffset: UInt64 let fileSize: UInt64 let maxProtection: Int32 let initialProtection: Int32 } static func analyzeSegments(binaryPath: String) -> [SegmentInfo] { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/otool") task.arguments = ["-l", binaryPath] let pipe = Pipe() task.standardOutput = pipe do { try task.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" return parseSegments(from: output) } catch { print("Error analyzing binary: \(error)") return [] } } private static func parseSegments(from output: String) -> [SegmentInfo] { var segments: [SegmentInfo] = [] let lines = output.components(separatedBy: .newlines) var i = 0 while i < lines.count { let line = lines[i].trimmingCharacters(in: .whitespaces) if line.starts(with: "segname") { // 세그먼트 정보 파싱 로직 let segmentName = String(line.dropFirst(8)) // vmaddr, vmsize, fileoff, filesize 등 추출 // (실제 구현에서는 정규표현식 사용) i += 6 // 다음 세그먼트로 스킵 } else { i += 1 } } return segments } } // 사용 예시 let segments = MachOAnalyzer.analyzeSegments(binaryPath: "YourApp.app/YourApp") for segment in segments { print("📊 \(segment.name): \(segment.fileSize) bytes") }
__TEXT 세그먼트 심화 분석
__TEXT 세그먼트는 실행 코드와 읽기 전용 데이터를 포함해요.
🎯 __TEXT 내부 섹션
# __TEXT 세그먼트의 모든 섹션 확인 otool -l YourApp | grep -A10 "__TEXT" # 주요 섹션들: # __text: 실제 기계어 코드 # __cstring: C 문자열 상수 # __const: 상수 데이터 # __objc_methname: Objective-C 메서드 이름 # __swift5_typeref: Swift 타입 참조
🎯 코드 사이즈 최적화 전략
// ❌ 코드 크기를 늘리는 안티패턴 class InefficicientCode { // 1. 불필요한 제네릭 특수화 func processArray<T>(_ array: [T]) where T: Codable { // 각 타입마다 별도 코드 생성됨 for item in array { print("\(item)") } } // 2. 인라인이 많은 큰 함수 @inline(__always) func heavyComputation() -> Double { // 1000줄의 복잡한 계산... // 이 함수가 호출되는 모든 곳에 코드가 복사됨 return 0.0 } // 3. 사용되지 않는 프로토콜 준수 func unusedProtocolMethods() { // Dead code가 링커에 의해 제거되지 않을 수 있음 } } // ✅ 코드 크기 최적화된 패턴 class OptimizedCode { // 1. 타입 제약을 최소화한 제네릭 func processItems<T>(_ items: [T]) { // 공통 로직만 제네릭으로 let count = items.count processCount(count) } private func processCount(_ count: Int) { // 타입별로 특수화되지 않는 구체적 구현 print("Processing \(count) items") } // 2. 적절한 함수 크기 유지 func lightweightOperation() -> Double { // 작은 함수는 인라인해도 크기 증가 최소 return 42.0 } @inline(never) func heavyOperation() -> Double { // 큰 함수는 인라인 방지 return performComplexCalculation() } private func performComplexCalculation() -> Double { // 실제 복잡한 로직... return 0.0 } }
🎯 Swift 코드 최적화 실전
// 컴파일러 최적화 활용 struct CodeSizeOptimizer { // 1. 문자열 리터럴 최적화 static let commonStrings = [ "error": "An error occurred", "success": "Operation completed", "loading": "Loading..." ] // ❌ 중복된 문자열 리터럴 func showBadMessages() { print("An error occurred") print("An error occurred") // __cstring에 중복 저장 print("An error occurred") } // ✅ 문자열 재사용 func showGoodMessages() { let message = Self.commonStrings["error"]! print(message) print(message) // 같은 메모리 주소 참조 print(message) } // 2. 제네릭 특수화 제어 @_specialize(where T == Int) @_specialize(where T == String) func processGeneric<T: Hashable>(_ value: T) -> String { // 자주 사용되는 타입만 특수화 return "\(value.hashValue)" } // 3. 조건부 컴파일로 디버그 코드 제거 func debugInfo() { #if DEBUG print("Debug information") // Release 빌드에서는 완전히 제거됨 #endif } }
__DATA 세그먼트 최적화
__DATA 세그먼트는 전역 변수와 초기화된 데이터를 포함해요.
🎯 __DATA 섹션 분석
# __DATA 세그먼트 상세 분석 otool -l YourApp | grep -A15 "__DATA" # 주요 섹션들: # __data: 초기화된 전역 변수 # __bss: 0으로 초기화된 변수 (파일에 저장 안됨) # __const: 상수 데이터 # __objc_data: Objective-C 클래스 데이터 # __swift5_types: Swift 타입 메타데이터
🎯 데이터 최적화 전략
// ❌ 메모리 낭비하는 패턴 class WastefulData { // 1. 큰 전역 배열 static let hugeConstantArray = Array(1...100000) // 2. 불필요한 타입 메타데이터 class UnnecessaryClass { let property1: String = "" let property2: Int = 0 let property3: Double = 0.0 // 각 프로퍼티마다 메타데이터 생성 } // 3. 중복된 상수 데이터 let config1 = ["key1": "value1", "key2": "value2"] let config2 = ["key1": "value1", "key2": "value2"] // 중복! } // ✅ 메모리 효율적인 패턴 class EfficientData { // 1. 지연 초기화로 필요시에만 로드 static let lazyConstantArray: [Int] = { // 앱 시작시가 아닌 실제 사용시에 생성 return Array(1...100000) }() // 2. 구조체 활용으로 메타데이터 최소화 struct Configuration { let key1: String let key2: String static let shared = Configuration( key1: "value1", key2: "value2" ) } // 3. 상수 풀링 enum Constants { static let commonConfig = ["key1": "value1", "key2": "value2"] } let config1 = Constants.commonConfig let config2 = Constants.commonConfig // 같은 인스턴스 공유 }
🎯 Swift 메타데이터 최적화
// Swift 타입 메타데이터 크기 줄이기 protocol MinimalProtocol { // 프로토콜 요구사항을 최소화 func essentialMethod() } // ❌ 과도한 프로토콜 준수 class OverEngineeredClass: Codable, Hashable, CustomStringConvertible, CustomDebugStringConvertible, Comparable { let value: Int init(value: Int) { self.value = value } // 각 프로토콜마다 메타데이터와 witness table 생성 // ... 모든 프로토콜 구현 } // ✅ 필요한 프로토콜만 준수 struct EfficientStruct: Equatable { let value: Int // 최소한의 메타데이터만 생성 static func == (lhs: EfficientStruct, rhs: EfficientStruct) -> Bool { return lhs.value == rhs.value } }
__LINKEDIT 세그먼트와 심볼 최적화
__LINKEDIT 세그먼트는 링킹 정보와 심볼 테이블을 포함해요.
🎯 심볼 테이블 분석
# 심볼 테이블 확인 nm -gU YourApp | head -20 # 동적 심볼 테이블 확인 otool -I YourApp # 문자열 테이블 크기 확인 otool -l YourApp | grep -A5 LC_SYMTAB
🎯 심볼 최적화 실전
// Build Settings에서 심볼 최적화 // SWIFT_OPTIMIZATION_LEVEL = -O (Release) // STRIP_INSTALLED_PRODUCT = YES // SYMBOLS_HIDDEN_BY_DEFAULT = YES // DEPLOYMENT_POSTPROCESSING = YES // 코드에서 심볼 제어 public class PublicAPI { // 외부에 노출되어야 하는 심볼 public func publicMethod() { internalHelper() } // internal로 심볼 숨기기 internal func internalHelper() { privateImplementation() } // 완전히 숨겨진 심볼 private func privateImplementation() { // 가장 적은 심볼 정보 생성 } } // @_silgen_name으로 심볼명 제어 @_silgen_name("optimized_function") func optimizedFunction() -> Int { // 컴파일러가 생성하는 복잡한 심볼명 대신 간단한 이름 사용 return 42 }
🎯 Dead Code Elimination
// 링커의 데드 코드 제거 활용 class DeadCodeExample { // ❌ 사용되지 않지만 제거되지 않는 코드 @objc dynamic func objcMethod() { // @objc dynamic은 링커가 제거하지 못함 } // ✅ 안전하게 제거되는 코드 private func unusedPrivateMethod() { // 사용되지 않으면 자동으로 제거됨 } // ✅ 조건부 컴파일로 확실한 제거 func debugOnlyMethod() { #if DEBUG print("Debug only code") #else // Release에서는 완전히 제거됨 #endif } } // 링커 플래그로 추가 최적화 // OTHER_LDFLAGS = -dead_strip -no_dead_strip_inits_and_terms
라이브러리와 프레임워크 최적화
외부 라이브러리는 앱 크기의 주요 원인이에요.
🎯 의존성 분석
# 동적 라이브러리 의존성 확인 otool -L YourApp # 출력 예시: # /System/Library/Frameworks/Foundation.framework/Foundation # /usr/lib/libobjc.A.dylib # /usr/lib/libSystem.B.dylib # @rpath/Alamofire.framework/Alamofire # 각 프레임워크 크기 확인 du -sh YourApp.app/Frameworks/*
🎯 라이브러리 최적화 전략
// 1. 필요한 부분만 import // ❌ 전체 프레임워크 import import Foundation import UIKit import CoreData import CloudKit // ✅ 필요한 타입만 import import struct Foundation.URL import class UIKit.UIViewController import protocol CoreData.NSManagedObject // 2. 조건부 import로 크기 최적화 #if canImport(UIKit) import UIKit typealias PlatformView = UIView #elseif canImport(AppKit) import AppKit typealias PlatformView = NSView #endif // 3. 런타임 로딩으로 선택적 사용 class OptionalFrameworkLoader { static func loadCloudKitIfAvailable() -> Bool { guard let _ = NSClassFromString("CKContainer") else { // CloudKit이 없는 환경에서는 해당 코드 제외 return false } return true } }
🎯 CocoaPods/SPM 최적화
# Podfile 최적화 platform :ios, '14.0' use_frameworks! :linkage => :static # 정적 링킹으로 크기 줄이기 target 'YourApp' do # 필요한 subspecs만 포함 pod 'Alamofire', '~> 5.0', :subspecs => ['Core'] # 디버그에서만 사용하는 라이브러리 pod 'FLEX', :configurations => ['Debug'] # 특정 타겟에서만 사용 target 'YourAppTests' do inherit! :search_paths pod 'Quick' pod 'Nimble' end end # Build Configurations 분리 post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| if config.name == 'Release' # Release에서 디버그 심볼 제거 config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'YES' config.build_settings['SYMBOLS_HIDDEN_BY_DEFAULT'] = 'YES' end end end end
Universal Binary 최적화
Apple Silicon과 Intel 맥을 모두 지원하는 Universal Binary 최적화
🎯 아키텍처별 분석
# Universal Binary 확인 lipo -info YourApp # 출력 예시: # Architectures in the fat file: YourApp are: x86_64 arm64 # 각 아키텍처별 크기 확인 lipo -thin x86_64 YourApp -output YourApp_x86_64 lipo -thin arm64 YourApp -output YourApp_arm64 ls -la YourApp* # -rwxr-xr-x 1 user staff 12345678 YourApp (Universal) # -rwxr-xr-x 1 user staff 6123456 YourApp_x86_64 # -rwxr-xr-x 1 user staff 6223456 YourApp_arm64
🎯 아키텍처별 최적화
// 아키텍처별 조건부 컴파일 #if arch(arm64) // Apple Silicon 최적화 코드 func optimizedForAppleSilicon() { // Metal Performance Shaders 활용 // Neural Engine 활용 코드 } #elseif arch(x86_64) // Intel 최적화 코드 func optimizedForIntel() { // AVX 명령어 활용 // 다른 최적화 전략 } #endif // 런타임 아키텍처 검사 extension ProcessInfo { var isAppleSilicon: Bool { #if targetEnvironment(macCatalyst) var size = 0 sysctlbyname("hw.optional.arm64", nil, &size, nil, 0) var value: Int32 = 0 sysctlbyname("hw.optional.arm64", &value, &size, nil, 0) return value == 1 #else return false #endif } }
이미지와 리소스 최적화
앱 번들에서 상당한 크기를 차지하는 리소스들의 최적화
🎯 이미지 최적화 전략
// 1. 적응형 이미지 사용 // Assets.xcassets에서 다양한 해상도 관리 class ImageOptimizer { // ❌ 런타임에 스케일링 static func inefficientImageLoading() -> UIImage? { let largeImage = UIImage(named: "huge_image") // 4K 이미지 return largeImage?.resized(to: CGSize(width: 100, height: 100)) } // ✅ 적절한 크기의 이미지 사용 static func efficientImageLoading() -> UIImage? { // @1x, @2x, @3x 별도 제공 return UIImage(named: "optimized_image_100pt") } // ✅ PDF 벡터 이미지 활용 static func vectorImageLoading() -> UIImage? { // "Preserve Vector Data" 체크된 PDF return UIImage(named: "vector_icon") } } // 2. 동적 이미지 로딩 class DynamicImageLoader { private static var imageCache: [String: UIImage] = [:] static func loadImageOnDemand(named: String) -> UIImage? { if let cached = imageCache[named] { return cached } guard let image = UIImage(named: named) else { return nil } // 메모리 압박시에만 캐시 if ProcessInfo.processInfo.physicalMemory > 4_000_000_000 { imageCache[named] = image } return image } }
🎯 리소스 번들 최적화
// 조건부 리소스 로딩 class ConditionalResourceLoader { // 기기별 리소스 분리 static func loadDeviceSpecificResources() { #if targetEnvironment(simulator) // 시뮬레이터에서는 저해상도 리소스 loadLowResAssets() #else switch UIDevice.current.userInterfaceIdiom { case .phone: loadPhoneAssets() case .pad: loadPadAssets() case .mac: loadMacAssets() default: loadDefaultAssets() } #endif } // 언어별 리소스 최적화 static func loadLocalizedResourcesOnDemand() { let currentLanguage = Locale.current.languageCode ?? "en" // 현재 언어의 리소스만 로드 if let path = Bundle.main.path(forResource: "Localizable", ofType: "strings", inDirectory: nil, forLocalization: currentLanguage) { // 해당 언어 리소스 로드 } } private static func loadLowResAssets() { /* 구현 */ } private static func loadPhoneAssets() { /* 구현 */ } private static func loadPadAssets() { /* 구현 */ } private static func loadMacAssets() { /* 구현 */ } private static func loadDefaultAssets() { /* 구현 */ } }
빌드 설정 최적화
Xcode 빌드 설정을 통한 크기 최적화
🎯 핵심 빌드 설정
# 최적화 관련 주요 Build Settings # 1. 코드 최적화 SWIFT_OPTIMIZATION_LEVEL = -O # Release: 속도 최적화 GCC_OPTIMIZATION_LEVEL = s # Release: 크기 최적화 # 2. 심볼 스트리핑 STRIP_INSTALLED_PRODUCT = YES SYMBOLS_HIDDEN_BY_DEFAULT = YES DEPLOYMENT_POSTPROCESSING = YES # 3. 데드 코드 제거 DEAD_CODE_STRIPPING = YES OTHER_LDFLAGS = -dead_strip # 4. 링크 타임 최적화 LLVM_LTO = YES_THIN # Thin LTO로 크기와 성능 균형 # 5. 스위프트 최적화 SWIFT_COMPILATION_MODE = wholemodule SWIFT_WHOLE_MODULE_OPTIMIZATION = YES
🎯 커스텀 빌드 스크립트
#!/bin/bash # size_optimization.sh - 빌드 후 크기 최적화 스크립트 # 1. 불필요한 아키텍처 제거 (Simulator용) if [ "$EFFECTIVE_PLATFORM_NAME" == "-iphonesimulator" ]; then echo "Simulator build - skipping strip" exit 0 fi # 2. 바이너리 스트리핑 strip -x "$CODESIGNING_FOLDER_PATH/$EXECUTABLE_NAME" # 3. 프레임워크 최적화 for FRAMEWORK in "$CODESIGNING_FOLDER_PATH/Frameworks"/*.framework; do if [ -f "$FRAMEWORK" ]; then FRAMEWORK_EXECUTABLE=$(basename "$FRAMEWORK" .framework) strip -x "$FRAMEWORK/$FRAMEWORK_EXECUTABLE" fi done # 4. 크기 리포트 생성 echo "📊 Final app size analysis:" du -sh "$CODESIGNING_FOLDER_PATH" du -sh "$CODESIGNING_FOLDER_PATH/$EXECUTABLE_NAME" du -sh "$CODESIGNING_FOLDER_PATH/Frameworks"/* # 5. 크기 한계 검사 APP_SIZE=$(du -sk "$CODESIGNING_FOLDER_PATH" | cut -f1) MAX_SIZE_KB=204800 # 200MB in KB if [ $APP_SIZE -gt $MAX_SIZE_KB ]; then echo "⚠️ Warning: App size ($APP_SIZE KB) exceeds OTA limit (200MB)" exit 1 fi echo "✅ App size optimization completed"
Conclusion
Mach-O 파일 구조를 이해하고 앱 크기를 최적화하는 것은 단순히 용량을 줄이는 것을 넘어서, 사용자 경험과 앱의 성공에 직접적인 영향을 미치는 중요한 부분입니다.
가장 중요한 것은 측정 가능한 지표를 바탕으로 최적화를 진행하는 것입니다.
무작정 모든 것을 최적화하려 하지 말고, 실제로 크기에 영향을 미치는 부분을 찾아서 집중적으로 개선하는 것이 효과적입니다 😃
앱 크기 최적화는 한 번에 끝나는 작업이 아니라 지속적으로 관리해야 하는 프로세스에요.
새로운 기능을 추가할 때마다 크기 영향을 고려하고, 정기적으로 불필요한 코드와 리소스를 정리하는 습관을 만드는 것이 중요합니다!
References
swift/docs/OptimizationTips.rst at main · swiftlang/swift
The Swift Programming Language. Contribute to swiftlang/swift development by creating an account on GitHub.
github.com
Introduction
Introduction Important: This document is no longer being updated. For the latest information about Apple SDKs, visit the documentation website. OS X supports a number of application environments, each with its own runtime rules, conventions, and file form
developer.apple.com
iOS Memory Deep Dive - WWDC18 - Videos - Apple Developer
Discover how memory graphs can be used to get a close up look at what is contributing to an app's memory footprint. Understand the true...
developer.apple.com
Reducing your app’s size | Apple Developer Documentation
Measure your app’s size, optimize its assets and settings, and adopt technologies that help streamline installation over a mobile internet connection.
developer.apple.com
'iOS' 카테고리의 다른 글
Pagination Strategy (1) 2025.11.22 WKURLSchemeHandler (0) 2025.11.09 App Battery Drain (5) 2025.08.09 Diet for iOS App size (feat. App Thinning) (5) 2025.08.02 Meet PaperKit (feat. WWDC 2025) (0) 2025.06.25