TIL: 메서드 디스패치(Method Dispatch)

Royce·2025년 3월 29일

Swift 문법

목록 보기
55/63

메서드 디스패치(Method Dispatch)

  • Swift에서 메서드를 호출하는 방식(디스패치 방식) 은 크게 세 가지로 나뉜다
  • Swift는 성능 최적화를 위해 상황에 따라 적절한 디스패치 방식을 자동으로 사용한다

메서드 디스패치(Method Dispatch)의 세 가지 방식

방식사용되는 타입성능특징
Direct Dispatch(정적 디스패치)구조체, 열거형, 프로토콜 확장가장 빠름컴파일 타임에 메서드를 직접 호출
Table Dispatch(동적 디스패치)클래스보통런타임에 Virtual Table을 참조하여 호출
Witness Dispatch(프로토콜 디스패치)프로토콜을 준수하는 타입(구조체, 클래스)보통런타임에 프로토콜 요구사항을 확인하여 호출
Message Dispatch(메세지 디스패치)클래스(@objc dynamic)가장 느림objc_msgSend()를 사용하여 호출

직접 디스패치(Direct Dispatch, 정적 디스패치, Static Dispatch)

  • 컴파일 타임에 메서드 호출이 결정된다
  • 메서드의 주소가 컴파일 시점에 결정되고, 런타임에 별도의 테이블을 탐색하지 않고 호출된다
  • 구조체, 열거형, 프로토콜 확장에서 사용된다
  • 성능이 가장 빠르다 (직접 메서드를 호출하기 때문)

예제 코드(Direct Dispatch)

// 구조체 정의 (Direct Dispatch 사용)
struct Airplane {
    func takeOff() { print("Airplane is taking off") }
    func land() { print("Airplane is landing") }
}

// 구조체 인스턴스 생성
let myAirplane = Airplane()

// 메서드 호출 (Direct Dispatch 방식으로 호출)
myAirplane.takeOff()  // 출력: Airplane is taking off
myAirplane.land()     // 출력: Airplane is landing
  • 구조체 Airplane 은 두 개의 메서드 (takeOff(), land()) 를 가지고 있다
  • 메서드를 호출할 때, Swift는 컴파일 타임에 이 메서드의 주소를 결정하여 메모리에 저장한다
  • 런타임에 테이블을 탐색할 필요 없이, 직접 메서드의 주소로 이동하여 호출한다
  • 이 방식은 성능이 가장 뛰어나다

테이블 디스패치(Table Dispatch, 동적 디스패치, Virtual Table)

  • 런타임에 메서드 호출이 결정된다
  • 클래스는 Virtual Table (가상 테이블, V-Table) 을 이용하여 메서드를 호출한다
  • 상속을 사용하여 메서드를 오버라이딩할 경우, 테이블에서 주소를 대체하여 호출한다
  • 클래스에서 주로 사용된다
  • 성능은 Direct Dispatch 보다 느리지만, 다형성을 제공한다

예제 코드(Table Dispatch)

// 부모 클래스 정의 (Virtual Table 사용)
class Animal {
    func sound() { print("Animal makes a sound") }
    func sleep() { print("Animal is sleeping") }
}

// 자식 클래스 정의 (부모의 메서드를 오버라이딩하여 V-Table 수정)
class Cat: Animal {
    override func sound() { print("Cat meows") }
    func purr() { print("Cat is purring") }
}

// 부모 클래스 인스턴스 생성 (Virtual Table 사용)
let genericAnimal: Animal = Animal()
genericAnimal.sound()  // 출력: Animal makes a sound
genericAnimal.sleep()  // 출력: Animal is sleeping

// 자식 클래스 인스턴스 생성 (Virtual Table 사용)
let myCat: Animal = Cat()
myCat.sound()  // 출력: Cat meows (오버라이딩된 메서드 호출)
myCat.sleep()  // 출력: Animal is sleeping (부모 클래스의 메서드 호출)
  • Animal 클래스는 메서드 목록을 Virtual Table 에 저장한다 (sound(), sleep())
  • Cat 클래스가 sound() 메서드를 오버라이딩하면 Virtual Table 의 주소가 변경된다
  • myCat 인스턴스는 Virtual Table 을 사용하여 오버라이딩된 메서드를 호출한다
  • 부모 클래스의 메서드는 오버라이딩되지 않는 경우 그대로 사용된다

메세지 디스패치(Message Dispatch, Objective-C 방식)

  • Objective-C 스타일의 디스패치 방식
  • @objc dynamic 키워드를 사용하여 Swift에서도 Message Dispatch 를 사용할 수 있다
  • 메서드를 호출할 때, objc_msgSend() 함수가 실행되어 메서드를 검색하여 호출한다
  • 런타임에 메서드를 동적으로 추가, 교체할 수 있는 유연성을 제공한다
  • 성능은 가장 느리다 (검색 과정이 포함되기 때문)

예제 코드(Message Dispatch)

import Foundation  // Objective-C 기능을 사용하기 위해 필요

// 부모 클래스 정의 (Message Dispatch 사용)
class Vehicle: NSObject {  // NSObject를 상속하여 Objective-C 방식 사용 가능
    @objc dynamic func start() { print("Vehicle is starting") }
    @objc dynamic func stop() { print("Vehicle is stopping") }
}

// 자식 클래스 정의 (메서드를 오버라이딩하여 Message Dispatch 방식으로 호출)
class Motorcycle: Vehicle {
    @objc dynamic override func start() { print("Motorcycle is starting") }
    @objc dynamic func accelerate() { print("Motorcycle is accelerating") }
}

// 부모 클래스 인스턴스 생성
let genericVehicle: Vehicle = Vehicle()
genericVehicle.start()  // 출력: Vehicle is starting
genericVehicle.stop()   // 출력: Vehicle is stopping

// 자식 클래스 인스턴스 생성
let myMotorcycle: Vehicle = Motorcycle()
myMotorcycle.start()    // 출력: Motorcycle is starting
myMotorcycle.stop()     // 출력: Vehicle is stopping
  • Vehicle 클래스는 @objc dynamic 키워드를 사용하여 메서드를 정의한다
  • Motorcycle 클래스는 부모 클래스의 메서드를 오버라이딩하고 새로운 메서드를 추가한다
  • 메서드를 호출할 때, Swift는 objc_msgSend() 함수를 사용하여 메서드를 검색한다
  • 런타임에 메서드를 교체하거나 추가할 수 있는 유연성을 제공한다

프로토콜 디스패치(Witness Table, Protocol Dispatch)

  • Swift에서 프로토콜의 메서드를 호출할 때 사용하는 방식이다
  • 프로토콜을 채택한 타입(구조체, 클래스)은 Witness Table을 사용하여 메서드를 호출한다
  • Swift의 프로토콜은 객체지향 프로그래밍의 다형성을 제공하는 중요한 도구이다

Witness Table의 특징

  • 프로토콜 요구사항에 포함된 메서드는 Witness Table 을 사용한다
  • 프로토콜 확장에서 제공된 기본 구현은 정적 디스패치(Direct Dispatch) 방식으로 호출된다
  • 클래스가 프로토콜을 채택하면 Virtual Table과 Witness Table이 함께 사용된다
  • 구조체가 프로토콜을 채택하면, Witness Table을 사용하여 동적 디스패치 방식으로 호출된다
  • 프로토콜을 타입으로 사용하면, 런타임에 해당 구현이 결정되기 때문에 약간의 성능 손실이 발생한다

예제 코드(Witness Table Dispatch)

// 프로토콜 정의 (프로토콜 요구사항 - Witness Table 사용)
protocol Device {
    func powerOn()    // 프로토콜 요구사항 - Witness Table로 관리
    func powerOff()   // 프로토콜 요구사항 - Witness Table로 관리
}

// 프로토콜 확장 (기본 구현 제공 - Direct Dispatch 방식)
extension Device {
    func powerOn() { print("Device is powering on (Default Implementation)") }
    func powerOff() { print("Device is powering off (Default Implementation)") }
    func reset() { print("Device is resetting (Direct Dispatch)") }  // 요구사항이 아니므로 정적 디스패치로 호출
}

// ✅ 구조체 정의 (프로토콜을 채택 - Witness Table 사용)
struct Computer: Device {
    func powerOn() { print("Computer is powering on") }   // 직접 구현 (Witness Table에 등록됨)
    func powerOff() { print("Computer is powering off") } // 직접 구현 (Witness Table에 등록됨)
}

// ✅ 클래스 정의 (프로토콜을 채택 - Virtual Table & Witness Table 함께 사용)
class Tablet: Device {
    func powerOn() { print("Tablet is powering on") }   // 직접 구현 (Virtual Table & Witness Table에 등록됨)
    func powerOff() { print("Tablet is powering off") } // 직접 구현 (Virtual Table & Witness Table에 등록됨)
}

// 구조체 인스턴스 생성 (Device 타입으로 선언하여 Witness Table 사용)
let myComputer: Device = Computer()
myComputer.powerOn()    // 출력: Computer is powering on (Witness Table을 사용하여 호출)
myComputer.powerOff()   // 출력: Computer is powering off (Witness Table을 사용하여 호출)
myComputer.reset()      // 출력: Device is resetting (Direct Dispatch 방식으로 호출)

// 클래스 인스턴스 생성 (Device 타입으로 선언하여 Witness Table 사용)
let myTablet: Device = Tablet()
myTablet.powerOn()    // 출력: Tablet is powering on (Witness Table을 사용하여 호출)
myTablet.powerOff()   // 출력: Tablet is powering off (Witness Table을 사용하여 호출)
myTablet.reset()      // 출력: Device is resetting (Direct Dispatch 방식으로 호출)
  1. 프로토콜 정의(Device)
    • powerOn()powerOff() 메서드를 요구한다
    • 이 요구사항은 모든 채택한 타입의 Witness Table 에 등록된다
  2. 프로토콜 확장(extension Device)
    • 기본 구현을 제공하는 메서드는 정적 디스패치(Direct Dispatch) 방식으로 호출된다
    • reset() 메서드는 프로토콜 요구사항이 아니기 때문에 Witness Table 에 포함되지 않는다
  3. 구조체(Computer)
    • Device 프로토콜을 채택하여, 직접 구현된 메서드가 Witness Table 에 등록된다
    • reset() 메서드는 Witness Table 에 포함되지 않고, 정적 디스패치 방식으로 호출된다
  4. 클래스(Tablet)
    • Device 프로토콜을 채택하며, Virtual Table 과 Witness Table 을 동시에 사용한다
    • 클래스는 Virtual Table 에 등록된 메서드를 Witness Table 이 참조하는 방식으로 동작한다
  5. 프로토콜 타입으로 사용(let myComputer: Device = Computer())
    • 프로토콜 타입으로 사용하면 Witness Table 을 통해 메서드를 호출한다
    • 이 경우, 메서드를 호출할 때마다 테이블을 탐색하는 과정이 필요하므로, 정적 디스패치보다 약간 느리다

Witness Table의 동작 방식 정리

  1. 프로토콜의 요구사항을 채택한 타입은 Witness Table 에 메서드를 등록한다
  2. 프로토콜 타입으로 선언된 객체는 Witness Table 을 통해 메서드를 호출한다
  3. 프로토콜 확장에서 제공되는 메서드는 Direct Dispatch 방식으로 호출된다
  4. 클래스가 프로토콜을 채택할 경우, Virtual Table 과 Witness Table 이 함께 사용된다

요약(Method Dispatch의 차이점)

  • Direct Dispatch: 구조체, 열거형, 프로토콜 확장에서 사용되는 매우 빠른 방식이다
  • Table Dispatch: 클래스에서 사용하는 방식으로, 상속과 오버라이딩을 지원한다
  • Message Dispatch: Objective-C 스타일로 @objc dynamic 메서드에 사용된다 (가장 느림)
  • Witness Table Dispatch: 프로토콜을 채택한 타입에서 사용하는 방식이며, 구조체, 클래스 모두에서 사용 가능하다
profile
iOS 개발자 지망생

0개의 댓글