iOS) Dispatch(1) - Dynamic / Static Dispatch
Method Dispatch
method dispatch 는 Swift 에서 메서드를 호출할 때 현재 메모리에서 어떻게 어떤 메소드를 실행시킬지를 결정할때 사용하는 방법입니다.
클래스의 dispatch 과정을 예시를 들어봅시다.
class Animal {
func bark() {
print("bark!")
}
}
class Cow: Animal {
func bark() {
print("moo!")
}
}
class Dog: Animal { }
let animal: Animal = Animal()
animal.bark()
let cow: Animal = Cow()
cow.bark()
let dog: Animal = Dog()
dog.bark()
인스턴스 animal, cow, dog 가 bark() 메소드를 호출할 때 어떤 결과가 나올까요? 즉, 어떤 클래스의 메소드를 호출할까요?
이때 컴파일러는 클래스의 메소드가 자식 클래스에서 오버라이딩이 될 경우를 대비해서 런타임에 참조를 확인합니다.
즉, 클래스는 오버라이딩이 될 수 있는 가능성이 존재하기 때문에 Dog 클래스처럼 오버라이딩이 되지 않았더라도 런타임 때 어떤 메소드를 실행시킬지 결정합니다. 이것이 바로 method dispatch 중 **dynamic dispatch**
입니다.
method dispatch 에는 dynamic dispatch
외에도 static dispatch
가 있습니다. 알아봅시다!
Static / Dynamic Dispatch
- Static Dispatch(Direct Call)
컴파일 타임에 컴파일러가 어떤 클래스의 메소드를 실행할지 결정하여 빠르게 수행됩니다.
- Dynamic Dispatch(Indirect Call)
런타임에 어떤 클래스의 메소드를 실행할지 결정합니다.
Reference / Value / Protocol Type 의 dispatch
Swift 에서 각 타입들의 어떤 dispatch 를 사용하는지 알아봅시다.
- Reference Type
앞서 보았던 예시의 결과는 어떻게 나올까?
class Animal {
func bark() {
print("bark!")
}
}
class Cow: Animal {
func bark() {
print("moo!")
}
}
class Dog: Animal { }
let animal: Animal = Animal()
animal.bark()
// ✅ bark!
let cow: Animal = Cow()
cow.bark()
// ✅ moo!
let dog: Animal = Dog()
dog.bark()
// ✅ bark!
cow 의 경우 Animal 타입이지만 Cow 타입의 인스턴스를 할당했습니다. 이때 업캐스팅하기 때문에 Animal 클래스의 bark() 메소드가 아닌 Cow 클래스의 bark() 메소드를 호출하기로 런타임에 결정합니다.
dog 의 경우 Animal 타입이지만 Dog 타입의 인스턴스를 할당했습니다. 이때 Dog 에는 bark() 메소드가 없지만 상속받은 부모 클래스 Animal 의 bark() 메소드가 호출됩니다.
이처럼 참조 타입의 Class 는 상속이 가능합니다. 따라서 상속과 오버라이드를 한 함수를 호출 할 가능성이 있기 때문에 dynamic dispatch
를 수행합니다.
그렇다면 이 참조 타입의 dynamic dispatch 은 어떻게 이루어지는걸까요?
클래스에는 메소드를 호출할 때 실질적으로 어떤 메소드를 호출하는지 결정하는 역할을 하는 함수 포인터가 담긴 vTable 이 있습니다.
상속을 통해 자식 클래스에는 부모 클래스의 vTable 의 복사본이 존재합니다. 오버라이딩하지 않은 메소드가 있다면 부모 클래스의 함수 포인터가 그대로 담겨 있게 됩니다.
이를 통해 런타임 때 클래스의 vTable 의 해당 함수 포인터를 확인하고, 자식 클래스의 메소드인지 부모 클래스의 메소드인지 결정할 수 있게 됩니다.
자식 클래스가 새 메소드를 추가하면 해당 메소드 포인터는 vTable 의 끝에 추가됩니다.
dynamic dispatch 는 vTable 에서 함수 포인터를 찾아 메모리 주소를 읽고 접근해야하기 때문에 성능상의 손해를 겪게 됩니다.
- Value Type
값 타입은 상속을 할 수 없기 때문에 오버라이딩 될 가능성이 없고, static dispatch
를 수행합니다.
- Protocol Type
프로토콜을 통해 호출하는 메소드는 프로토콜을 채택한 타입들이 구현한 메소드입니다. 이때는 메소드를 구현하고 있음이 보장됩니다. 혹은 protocol 의 extension 에서는 메소드의 기본 구현을 작성할 수 있습니다.
감이 오시나요? 이때 해당 타입에서 구현이 됐는지 안됐는지를 확인해야하기 때문에 dynamic dispatch
를 수행합니다.
그런데 앞서 reference type 과는 달리 vTable 을 조회하지 않습니다. 대신 프로토콜이 보유한 정보를 가진 witness table 을 통해서 dynamic dispatch
를 수행할 수 있게 됩니다.
(위에서 언급된 클래스의 메소드 뿐만 아니라 상속을 통해 오버라이딩이 가능한 프로퍼티 역시 동일하게 dynamic dispatch 를 통해서 결정됩니다.)
출처
Swift) Static Dispatch & Dynamic Dispatch (1/2)
[iOS - swift] Static Dispatch, Dynamic Dispatch 성능 최적화 방법, Witness Table, (final, private을 사용하는 이유)