iOS Runtime: 개념, iPhone 앱 실행 환경

저자: IT Sectr 게시일: 2026-05-17 읽는 시간: 12 분

iOS Runtime은 Apple iOS 운영 체제의 애플리케이션 실행 환경으로, Objective-C Runtime, Swift Runtime, Cocoa Touch 프레임워크 및 Automatic Reference Counting(ARC)을 통한 메모리 관리 메커니즘을 포함합니다. iOS Runtime은 동적 메서드 바인딩(message passing), 클래스 로딩, 메모리 관리 및 iOS 프레임워크를 통한 하드웨어 상호 작용을 담당합니다. Apple Developer Documentation에 따르면, 성능 최적화, 디버깅 및 안정적인 iOS 애플리케이션 개발을 위해 런타임 이해가 필요합니다.

핵심 요점

  • iOS Runtime — Objective-C Runtime, Swift Runtime 및 Cocoa Touch를 포함한 애플리케이션 실행 환경
  • Objective-C Runtime — message passing(objc_msgSend)을 통한 동적 메서드 바인딩
  • Swift Runtime — value types 및 generics를 통한 최적화가 적용된 정적 디스패치
  • ARC(Automatic Reference Counting) — 컴파일 시 자동 메모리 관리
  • dyld — 앱 시작 시 프레임워크와 라이브러리를 로드하는 동적 로더

iOS Runtime이란?

iOS Runtime은 iOS를 실행하는 Apple 기기에서 애플리케이션 실행을 보장하는 시스템 구성 요소 모음입니다. 여기에는 Objective-C Runtime(libobjc.A.dylib 라이브러리), Swift Runtime(libswiftCore.dylib), Core Foundation, Cocoa Touch 프레임워크(UIKit, Foundation), 동적 로더 dyld 및 메모리 관리, 스레드, 프로세스 간 통신을 위한 런타임 환경이 포함됩니다.

아키텍처적으로 iOS Runtime은 세 가지 수준에서 작동합니다. 최하위 수준 — Mach-O 바이너리 형식과 dyld로, 실행 파일과 라이브러리를 로드합니다. 중간 수준 — Objective-C Runtime과 Swift Runtime으로, 메서드 디스패치와 객체 관리를 담당합니다. 최상위 수준 — Cocoa Touch 프레임워크(UIKit, Foundation, Core Data, Metal)로, 개발자에게 API를 제공합니다.

iOS Runtime을 이해하면 개발자가 복잡한 문제를 해결할 수 있습니다. A/B 테스트 및 분석을 위한 메서드 스위즐링(Method Swizzling), 동적 클래스 로딩, ARC 이해를 통한 메모리 최적화, retain cycles 및 메모리 누수 디버깅, dyld를 통한 앱 시작 시간 최적화. 런타임 지식 없이는 시스템 수준에서의 프로파일링 및 최적화가 불가능합니다.

iOS Runtime 구성 요소

구성 요소라이브러리목적
Objective-C Runtimelibobjc.A.dylibMessage passing, 동적 클래스, 스위즐링
Swift RuntimelibswiftCore.dylibValue types, generics, protocol witnesses
Core FoundationCoreFoundation.frameworkCFType, toll-free bridging
dylddyld(usr/lib/dyld)Mach-O 로딩, 라이브러리 링킹
libSystemlibSystem.B.dylibPOSIX threads, libc, libdispatch(GCD)

Mach-O 형식

iOS용 애플리케이션은 Mach-O 형식(Mach Object)으로 컴파일됩니다. Mach-O 파일에는 헤더, 로드 명령 및 세그먼트가 포함됩니다: __TEXT(코드, 상수), __DATA(전역 변수, Objective-C 메타데이터), __LINKEDIT(심볼, 재배치 테이블). dyld는 첫 번째 명령어를 실행하기 전에 Mach-O를 파싱하고 종속성을 로드합니다.

Objective-C Runtime: Message Passing 및 동적 디스패치

Objective-C Runtime은 iOS Runtime에서 가장 강력한 부분입니다. 초기 바인딩(early binding)을 사용하는 C++와 달리, Objective-C는 message passing을 통한 지연 바인딩(late binding)을 사용합니다. 메서드 호출 [receiver message]은 직접 함수 호출이 아닌 objc_msgSend(receiver, @selector(message))로 컴파일되어 객체의 클래스에서 메서드 구현을 동적으로 찾습니다.

Objective-C 객체는 해당 클래스에 대한 isa 포인터를 저장합니다. 클래스에는 메서드 목록, 메서드 캐시 및 슈퍼클래스에 대한 포인터가 포함됩니다. objc_msgSend는 상속 체인을 탐색합니다: 클래스 캐시를 확인한 다음 메서드 목록, 그 다음 슈퍼클래스로 이동합니다. 메서드를 찾을 수 없으면 포워딩이 트리거됩니다: resolveInstanceMethod, forwardingTargetForSelector 및 forwardInvocation.

Method Swizzling은 런타임에서 IMP(구현 포인터)를 교환하여 메서드 구현을 즉시 바꾸는 기술입니다. A/B 테스트, 분석(자동 화면 추적) 및 모니터링에 사용됩니다. OS 업데이트와 충돌할 수 있으므로 심각한 필요 없이 프로덕션에는 권장되지 않습니다.

예제: Objective-C의 Method Swizzling

objective-c
// viewDidLoad 추적을 위한 Method Swizzling
#import 

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(swizzled_viewDidLoad);

        Method originalMethod = class_getInstanceMethod(
            class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(
            class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(
            class,
            originalSelector,
            method_getImplementation(swizzledMethod),
            method_getTypeEncoding(swizzledMethod)
        );

        if (didAddMethod) {
            class_replaceMethod(
                class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod)
            );
        } else {
            method_exchangeImplementations(
                originalMethod, swizzledMethod);
        }
    });
}

- (void)swizzled_viewDidLoad {
    // 이벤트 추적
    NSLog(@"View Did Load: %@", self.class);
    // 원래 구현 호출
    [self swizzled_viewDidLoad];
}

@end

UIViewController(Tracking) 범주는 애플리케이션의 모든 UIViewController에서 viewDidLoad를 swizzled_viewDidLoad로 대체합니다. dispatch_once는 한 번의 스위즐링을 보장합니다. class_addMethod는 이중 스위즐링과 슈퍼클래스와의 충돌을 방지합니다. 컨트롤러 소스 코드를 수정하지 않고 분석에서 자동 화면 보기 추적에 사용됩니다.

isa 포인터 및 태그된 포인터

최신 iOS(arm64)에서 Apple은 isa 포인터를 최적화했습니다: 단순한 클래스 주소가 아니라 메모리 관리 플래그와 클래스 정보를 포함하는 비트 필드(non-pointer isa)입니다. 태그된 포인터는 또 다른 최적화입니다: 작은 NSNumber, NSDate 및 NSString 값은 힙의 객체로 저장되지 않고 포인터에 직접 저장되어 malloc 및 retain/release 오버헤드를 제거합니다. 태그된 포인터는 isa의 최하위 비트로 인식됩니다.

Swift Runtime: 정적 디스패치 및 최적화

Swift Runtime은 Objective-C Runtime과 근본적으로 다릅니다. Swift는 기본적으로 클래스 메서드에 vtable을 통한 정적 디스패치(static dispatch)를, value types 및 확장 메서드에 direct call을 사용합니다. 동적 디스패치(dynamic dispatch)는 @objc 또는 dynamic으로 표시된 메서드에만 사용됩니다. 이는 Objective-C에 비해 최대 40%의 성능 향상을 제공합니다.

Value types(struct, enum)은 Swift에서 Objective-C와의 주요 차이점입니다. 스택이나 다른 객체 내부에 저장되며 retain/release를 사용하지 않고 참조 카운팅을 위해 ARC에 참여하지 않습니다. Struct에는 isa 포인터가 없으며 objc_msgSend를 통해 전송할 수 없습니다. 프로토콜 위트니스는 프로토콜을 위한 vtable의 아날로그로, existential containers에 대한 동적 디스패치를 가능하게 합니다.

Swift Runtime에는 구체화(reified generics, mangled symbols 통해)를 포함한 generics와 string, array, dictionary, set을 최적화하기 위한 COW(Copy-on-Write)도 포함됩니다. 컬렉션을 복사할 때 실제 복사는 복사본 중 하나가 수정될 때만 발생합니다. 이는 함수 간 컬렉션 전달 시 오버헤드를 최소화합니다.

Swift vs Objective-C 디스패치

swift
import Foundation

// Swift: 정적 디스패치(클래스의 vtable)
class Animal {
    func makeSound() { print("...") }  // vtable
}

class Dog: Animal {
    override func makeSound() { print("Woof") }  // vtable override
}

// @objc dynamic: Objective-C Runtime 디스패치
class Cat: Animal {
    @objc dynamic override func makeSound() {
        print("Meow")
    }  // objc_msgSend
}

// Struct — 런타임 디스패치 없음
struct Cow {
    func makeSound() { print("Moo") }  // direct call
}

// Protocol with protocol witness
protocol SoundMaker {
    func makeSound()
}

struct Duck: SoundMaker {
    func makeSound() { print("Quack") }
}

// existential container 사용
let soundMakers: [SoundMaker] = [Dog(), Cow(), Duck()]
for maker in soundMakers {
    maker.makeSound()  // protocol witness dispatch
}

// 성능 테스트
func testDispatch() {
    let dog = Dog()
    let cat = Cat()
    var cow = Cow()

    let start = CFAbsoluteTimeGetCurrent()
    for _ in 0..<1000000 {
        dog.makeSound()  // vtable: ~3ns
        cat.makeSound()  // objc_msgSend: ~15ns
        cow.makeSound()  // direct: ~1ns
    }
    let elapsed = CFAbsoluteTimeGetCurrent() - start
    print("Elapsed: (elapsed) sec")
}

예제는 Swift의 세 가지 디스패치 유형을 보여줍니다: class(Dog)의 vtable, @objc dynamic(Cat)의 objc_msgSend, struct(Cow)의 direct call입니다. existential containers([SoundMaker])의 프로토콜 위트니스는 오버헤드를 추가합니다. 실제로 Swift는 가능한 경우 정적 디스패치를 선택하여 C에 가까운 성능을 제공합니다.

Swift Runtime 및 Objective-C 브리지

Swift Runtime은 Objective-C Runtime과의 완전한 호환성을 위해 설계되었습니다. NSObject를 상속하는 모든 Swift 클래스는 자동으로 Objective-C Runtime에 등록되며 objc_msgSend를 통해 호출할 수 있습니다. @objc 속성은 Swift 메서드를 Objective-C에서 액세스 가능하게 만듭니다. 문자열 브리지: Swift String은 Objective-C API(toll-free bridging)에 전달될 때 자동으로 NSString으로 브리지됩니다.

ARC: 자동 참조 카운팅 및 메모리 관리

ARC(Automatic Reference Counting)는 컴파일 시 작동하는 iOS의 메모리 관리 시스템입니다. 컴파일러(Clang)는 객체 수명을 분석하고 자동으로 retain/release/autorelease 호출을 삽입합니다. 개발자가 수동으로 호출할 필요가 없습니다 — iOS 5 이전의 Manual Retain-Release(MRR)와 달리. ARC는 Objective-C 및 Swift 객체 수준에서 작동하지만 value types(struct, enum)에는 작동하지 않습니다.

Objective-C 및 Swift 클래스 객체에는 non-pointer isa 내부의 extra_rc 필드에 저장된 참조 카운트(retain count)가 있습니다. 객체 생성 시 retain count = 1입니다. retain 시 카운터가 증가하고 release 시 감소합니다. 카운터가 0에 도달하면 객체는 dealloc(Objective-C) 또는 deinit(Swift)을 통해 할당 해제됩니다. ARC는 스레드 안전합니다: retain/release는 원자적 연산(OSAtomicIncrement32/OSAtomicDecrement32)을 사용합니다.

Retain cycles은 ARC의 주요 문제입니다. 객체 A가 B에 대한 strong 참조를 가지고 있고 B가 A에 대한 strong 참조를 가지고 있으면 두 객체의 참조 카운트가 절대 0에 도달하지 않아 할당 해제되지 않습니다. 해결책은 weak 참조(Objective-C의 __weak, Swift의 weak) 또는 unowned 참조입니다. weak 참조는 retain count를 증가시키지 않으며 객체 할당 해제 시 자동으로 0(nil)이 됩니다.

Instruments를 통한 Retain Cycles 디버깅

swift
import Foundation

// Retain cycle 예제
class Parent {
    var child: Child?
    deinit { print("Parent deallocated") }
}

class Child {
    var parent: Parent?  // strong — retain cycle 생성!
    deinit { print("Child deallocated") }
}

var parent: Parent? = Parent()
var child: Child? = Child()
parent?.child = child
child?.parent = parent  // 사이클: Parent -> Child -> Parent
parent = nil
child = nil
// deinit 호출 안 됨 — 메모리 누수!

// 수정: weak
class WeakChild {
    weak var parent: Parent?  // weak — retain count 증가 안 함
    deinit { print("WeakChild deallocated") }
}

// 수정: unowned(보장된 수명용)
class UnownedChild {
    unowned let parent: Parent
    init(parent: Parent) { self.parent = parent }
    deinit { print("UnownedChild deallocated") }
}

// Instruments를 통한 확인
func profileMemory() {
    // 1. Instruments > Leaks 실행
    // 2. 객체를 생성하는 작업 수행
    // 3. Leaks에서 누수 확인
    // 4. Allocations에서 dealloc 없는 객체 찾기
    for _ in 0..<1000 {
        let p = Parent()
        let c = WeakChild()
        p.child = c as? Child
        // c.parent = p — 추가 안 함, weak
    }
}

Parent와 Child 간의 retain cycle 예제: 둘 다 서로에 대한 strong 참조를 보유하고 있어 ARC가 카운터를 0으로 만들 수 없습니다. 수정은 Child의 weak parent입니다. weak는 parent 할당 해제 시 자동으로 0이 됩니다. unowned는 parent의 수명이 child보다 길다고 보장되는 경우(viewController 및 view 등)에 사용합니다. retain cycles를 조기에 감지하려면 Instruments > Leaks를 사용하세요.

Autorelease Pool

Autorelease pool은 명시적 소유권 없이 생성된 객체를 위한 지연 해제 메커니즘입니다. Swift 및 Objective-C의 @autoreleasepool { }은 블록 끝에서 해제되어 풀의 각 객체에 release를 보내는 풀을 생성합니다. 루프(수천 개의 임시 객체 생성) 및 RunLoop가 없는 백그라운드 스레드에서 매우 중요합니다. UIKit RunLoop는 각 반복마다 메인 autorelease pool을 자동으로 해제합니다.

dyld: 동적 로더 및 앱 실행

dyld(dynamic link editor)는 iOS 앱 시작 시 Mach-O 실행 파일 및 관련 동적 라이브러리(dylib)를 로드하는 시스템 로더입니다. dyld는 /usr/lib/dyld에 위치하며 libSystem의 일부입니다. 로드 프로세스에는 여러 단계가 포함됩니다: Mach-O 파싱, 종속성 로드(Library Loader, LC_LOAD_DYLIB), 주소 재배치(ASLR), Objective-C Runtime 초기화 및 main() 호출.

앱 시작 시간은 dyld에 크게 의존합니다: 동적 라이브러리와 Objective-C 클래스가 많을수록 pre-main time이 길어집니다. Apple은 +load 메서드 수를 최소화하고(main 전에 실행됨) +initialize(지연 초기화)로 대체할 것을 권장합니다. 2020년부터 Apple은 iOS에서 사전 빌드된 dyld 캐시를 사용합니다: 시스템 라이브러리가 단일 캐시에 사전 연결되어 로딩 속도를 높입니다.

Pre-main Time 측정

swift
import Foundation

// DYLD_PRINT_STATISTICS를 통한 시작 시간 측정
// Xcode: Edit Scheme > Run > Arguments > Environment Variables
// DYLD_PRINT_STATISTICS = 1
// DYLD_PRINT_STATISTICS_DETAILS = 1

// Pre-main time 프로그래밍 방식 측정
@main
struct AppMain {
    static func main() {
        let launchStart = CFAbsoluteTimeGetCurrent()

        // UIApplicationMain이 여기서 발생
        AppDelegate.main()

        let launchEnd = CFAbsoluteTimeGetCurrent()
        let preMainTime = launchEnd - launchStart
        print("Pre-main time: (preMainTime) sec")
    }
}

// 최적화: +load를 +initialize로 대체
class OptimizedClass {
    // ❌ +load는 main 전에 실행
    // override class func load() { }

    // ✅ +initialize는 첫 액세스 시 실행
    static let shared = OptimizedClass()
    private init() {
        // 여기서 초기화
    }
}

// dylib 수 최적화
// 정적 라이브러리 병합은 LC_LOAD_DYLIB 수를 줄임
// 사용된 Objective-C 클래스만 링크하려면 -ObjC 플래그 사용
// Xcode: Build Settings > Mach-O Type > Static Library

pre-main time을 측정하려면 Xcode 스킴에서 DYLD_PRINT_STATISTICS를 사용하세요. 출력에는 총 시간, dylib 로드 시간, rebase/bind 시간, Objective-C 설정 시간 및 초기화 시간이 표시됩니다. 목표 값: 콜드 시작 총 < 400ms, 웜 시작 < 200ms. 최적화: 라이브러리 병합, +load를 +initialize로 대체, Objective-C 클래스 수 감소(Swift 사용), 동적 프레임워크 최소화.

dsc(dyld Shared Cache)

dyld shared cache는 iOS에서 사전 연결된 시스템 라이브러리의 캐시입니다. 모든 시스템 dylib(UIKit, Foundation, CoreGraphics)는 하나의 파일로 결합됩니다: /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64. 이렇게 하면 각 시스템 라이브러리를 개별적으로 로드할 필요가 없어지며 dyld가 캐시에 액세스하여 시작 속도가 크게 향상됩니다. 10개 이상의 동적 프레임워크를 사용하는 앱은 사용자 정의 dylib가 dsc에 포함되지 않아 가장 큰 지연을 경험합니다.

자주 묻는 질문

iOS Runtime이란 무엇이며 어떤 구성 요소로 이루어져 있나요?

iOS Runtime은 iOS의 애플리케이션 실행 환경으로, Objective-C Runtime(libobjc.dylib), Swift Runtime(libswiftCore.dylib), Cocoa Touch 프레임워크, dyld(동적 로더) 및 ARC(메모리 관리)를 포함합니다. Objective-C의 message passing, Swift의 정적 디스패치, Mach-O 파일 로딩 및 자동 메모리 관리를 제공합니다.

Objective-C Runtime과 Swift Runtime의 차이점은 무엇인가요?

Objective-C Runtime은 지연 바인딩으로 objc_msgSend(message passing)를 통한 동적 바인딩을 사용합니다. Swift Runtime은 성능을 위해 정적 디스패치(클래스의 vtable, struct의 direct call)를 사용합니다. @objc dynamic은 Swift 클래스에 Objective-C Runtime을 활성화합니다. Swift struct에는 isa 포인터가 없으며 retain/release를 사용하지 않습니다.

iOS에서 ARC는 어떻게 작동하나요?

ARC(Automatic Reference Counting)는 컴파일 시 메모리 관리입니다. Clang 컴파일러가 자동으로 retain/release 호출을 삽입합니다. 각 객체에는 참조 카운트가 있으며 0에 도달하면 dealloc이 호출됩니다. Retain cycles(상호 strong 참조)은 weak/unowned 참조로 방지됩니다. 누수를 감지하려면 Instruments > Leaks를 사용하세요.

dyld란 무엇이며 앱 실행에 어떤 영향을 미치나요?

dyld는 Mach-O 파일의 동적 로더입니다. 실행 파일과 모든 종속 dylib를 로드하고, 재배치(ASLR)를 수행하고, Objective-C Runtime을 초기화하고 main()을 호출합니다. Pre-main time은 dylib 및 +load 메서드 수에 따라 달라집니다. 측정에는 DYLD_PRINT_STATISTICS를 사용하세요. 최적화: 라이브러리 병합, +load를 +initialize로 대체.

Method Swizzling이란 무엇이며 언제 사용해야 하나요?

Method Swizzling은 Objective-C Runtime의 class_getInstanceMethod 및 method_exchangeImplementations를 통해 메서드의 IMP(구현 포인터)를 즉시 교환하는 기술입니다. A/B 테스트, 분석(자동 화면 추적) 및 모니터링에 사용됩니다. 심각한 필요 없이 프로덕션에는 권장되지 않습니다. Swift에서는 @objc dynamic + Method Swizzling으로 대체됩니다.

요약

  • iOS Runtime — Objective-C Runtime, Swift Runtime, dyld 및 ARC를 포함한 iOS 앱 실행 환경
  • Objective-C Runtime — message passing(objc_msgSend), isa 포인터, 스위즐링, 동적 클래스
  • Swift Runtime — 정적 디스패치(vtable, direct call), value types, protocol witnesses
  • ARC(Automatic Reference Counting) — 컴파일 시 retain/release를 통한 자동 메모리 관리
  • dyld — 앱 실행 속도(pre-main time)를 결정하는 동적 Mach-O 로더
  • Retain cycles — weak/unowned 참조로 방지; Instruments Leaks를 통한 디버깅
  • 최적화 — +load 최소화, dylib 병합, value types에 Swift struct 사용

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기