메서드 디스패치(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)
struct Airplane {
func takeOff() { print("Airplane is taking off") }
func land() { print("Airplane is landing") }
}
let myAirplane = Airplane()
myAirplane.takeOff()
myAirplane.land()
- 구조체
Airplane 은 두 개의 메서드 (takeOff(), land()) 를 가지고 있다
- 메서드를 호출할 때, Swift는 컴파일 타임에 이 메서드의 주소를 결정하여 메모리에 저장한다
- 런타임에 테이블을 탐색할 필요 없이, 직접 메서드의 주소로 이동하여 호출한다
- 이 방식은 성능이 가장 뛰어나다
테이블 디스패치(Table Dispatch, 동적 디스패치, Virtual Table)
- 런타임에 메서드 호출이 결정된다
- 클래스는 Virtual Table (가상 테이블, V-Table) 을 이용하여 메서드를 호출한다
- 상속을 사용하여 메서드를 오버라이딩할 경우, 테이블에서 주소를 대체하여 호출한다
- 클래스에서 주로 사용된다
- 성능은 Direct Dispatch 보다 느리지만, 다형성을 제공한다
예제 코드(Table Dispatch)
class Animal {
func sound() { print("Animal makes a sound") }
func sleep() { print("Animal is sleeping") }
}
class Cat: Animal {
override func sound() { print("Cat meows") }
func purr() { print("Cat is purring") }
}
let genericAnimal: Animal = Animal()
genericAnimal.sound()
genericAnimal.sleep()
let myCat: Animal = Cat()
myCat.sound()
myCat.sleep()
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
class Vehicle: NSObject {
@objc dynamic func start() { print("Vehicle is starting") }
@objc dynamic func stop() { print("Vehicle is stopping") }
}
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()
genericVehicle.stop()
let myMotorcycle: Vehicle = Motorcycle()
myMotorcycle.start()
myMotorcycle.stop()
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)
protocol Device {
func powerOn()
func powerOff()
}
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)") }
}
struct Computer: Device {
func powerOn() { print("Computer is powering on") }
func powerOff() { print("Computer is powering off") }
}
class Tablet: Device {
func powerOn() { print("Tablet is powering on") }
func powerOff() { print("Tablet is powering off") }
}
let myComputer: Device = Computer()
myComputer.powerOn()
myComputer.powerOff()
myComputer.reset()
let myTablet: Device = Tablet()
myTablet.powerOn()
myTablet.powerOff()
myTablet.reset()
- 프로토콜 정의(
Device)
powerOn() 과 powerOff() 메서드를 요구한다
- 이 요구사항은 모든 채택한 타입의 Witness Table 에 등록된다
- 프로토콜 확장(
extension Device)
- 기본 구현을 제공하는 메서드는 정적 디스패치(Direct Dispatch) 방식으로 호출된다
reset() 메서드는 프로토콜 요구사항이 아니기 때문에 Witness Table 에 포함되지 않는다
- 구조체(Computer)
Device 프로토콜을 채택하여, 직접 구현된 메서드가 Witness Table 에 등록된다
reset() 메서드는 Witness Table 에 포함되지 않고, 정적 디스패치 방식으로 호출된다
- 클래스(Tablet)
Device 프로토콜을 채택하며, Virtual Table 과 Witness Table 을 동시에 사용한다
- 클래스는 Virtual Table 에 등록된 메서드를 Witness Table 이 참조하는 방식으로 동작한다
- 프로토콜 타입으로 사용(
let myComputer: Device = Computer())
- 프로토콜 타입으로 사용하면 Witness Table 을 통해 메서드를 호출한다
- 이 경우, 메서드를 호출할 때마다 테이블을 탐색하는 과정이 필요하므로, 정적 디스패치보다 약간 느리다
Witness Table의 동작 방식 정리
- 프로토콜의 요구사항을 채택한 타입은 Witness Table 에 메서드를 등록한다
- 프로토콜 타입으로 선언된 객체는 Witness Table 을 통해 메서드를 호출한다
- 프로토콜 확장에서 제공되는 메서드는 Direct Dispatch 방식으로 호출된다
- 클래스가 프로토콜을 채택할 경우, Virtual Table 과 Witness Table 이 함께 사용된다
요약(Method Dispatch의 차이점)
- Direct Dispatch: 구조체, 열거형, 프로토콜 확장에서 사용되는 매우 빠른 방식이다
- Table Dispatch: 클래스에서 사용하는 방식으로, 상속과 오버라이딩을 지원한다
- Message Dispatch: Objective-C 스타일로
@objc dynamic 메서드에 사용된다 (가장 느림)
- Witness Table Dispatch: 프로토콜을 채택한 타입에서 사용하는 방식이며, 구조체, 클래스 모두에서 사용 가능하다