Swift

Swift | Method Dispatch관하여

ziziDev 2024. 5. 21. 12:15
반응형

 

안녕하세요

오늘은 공식문서에는 없지만

Method Dispatch에 대해서 알아보고자합니다

 

Method Dispatch

|

 

공식문서에는 없지만 이걸 남기는 이유는 컴퓨터가 동작하는 방식에 대해서 더 잘 이해하기 위해서

정리했습니다 :)

 

우선 내가 알고있는 지식이라곤 컴파일 타임은 정적 타입 검사일 때

런타임에는 동적 타입 검사이고

컴파일은 기계어로 이해할 수 있는 0과 1로 변환되는 과정이며

런타임은 컴파일을 거친 코드인 기계어가 사용자가 사용하는 순간이라고 알고 있었답니다

 

강의에서 자세하게 풀이해줬기 때문에

좀 더 세세하게 나뉘고 있고 런타임과 컴파일 타입에서 일어날 수 있는 문제점에 대해서 

한 번 더 고찰할 수 있었던 강의가 아닌가 싶었습니다

 

그리고 메서드 디스패치는 간단히

어떤 메서드들의 특징에 대해서 다루는 시간 이라고 생각하면 된다라고 말해주셔서 

좀 더 편하게 들을 수 있었던 시간이였다

 

메서드는 CPU에 대한 명령어인데 이건 코드 영역에만 존재하고

코드 영역에 이미 저장되어 있는 함수를 실행 시키려면 그 코드 영역에 있는 주소가 필요하며

거기에 대한 3가지 영역을 다룰 수 있답니다

 

 

 

우선

컴파일에 대해서 알아봅시다

 

 

컴파일 타입

|

컴파일 시점에 함수의 메모리 주소나 함수의 명령 코드를 해당 위치에 코드를 심어버리기 때문에

성능상 런타임보다 좋습니다

왜냐 사용자가 사용되는 런타임 전에 미리 결정되기 때문에

성능이 좋다고 생각하면 편할것 같습니다

 

사용되는 것들엔

구조체, 열거형인 value Type에서 사용되기 때문에

상속이나 다형성의 이점을 적용할 수 없지만 빠르다

 

 

런타임 테이블

|

실제로 앱이 실행되고 있는 동안에 메모리 주소를 찾아서 실행시키는 동작이고

 

작게 나눈다면 동적 디스패치와 메세지 디스패치가 있는데

 

동적 디스패치 같은 경우 클래스 마다

함수 포인터(메모리주소)들을 배열형태인 Virtual Method Table로 관리한다

주로 클래스 테이블인 Virtual Table 프로토콜 테이블인 Witness(목격자) Table이 있다

다이렉트보단 느리고 메시지보단 빠르다고 생각하면 된다

 

런타임 테이블에 메세지 디스패치가 있는데 Objective-C런타임에  의존하고 있는 녀석이다

주로 Objective-C클래스에서 사용하는데

가장 느린데 느린 이유는 클래스 상속시 모든 상속구조를 훑은뒤에

실행할 메서드를 결정하기 때문이라고 한다

그리고 상속된 메서드에 재정의를 하지 않으면 훑어보고 재정의 되어있지 않은 메서드인 경우

부모 클래스를 찾아가서 동일한 메서드를 찾아서 실행하는 경우이다

 

(그냥 처음부터 끝까지 훑고 없으면 부모 로 가서 다시 훑는 경우다...라고 이해함)

 

 

extension과 만나게된다면?

|

확장과 만나게되면 또 말이 달라지게 된다

구조체의 경우 값타입이기 때문에 다이렉트 디스패치이며 확장을 만나도 그대로 유지를 한다

프로토콜의 경우 Witness Table로 런타임 테이블에 속하지만 확장과 만나게되면 다이렉트 디스패치가 된다

그리고 클래스의 경우 Virtual Table로 런타임 테이블이지만 확장을 만나게되면

재정의가 불가능(상속시 재정의 불가)하기 때문에 다이렉트  디스패치입니다

그리고 마지막으로 메시지 디스패치인 @Object dynamic의 경우 확장을 해도 그대로 유지하게됩니다

 

만약 클래스에서 final을 넣으면 direct dispatch의 방식으로 된다

 

 

Direct(Static) Dispatch

직접 디스패치

 

struct MyStruct {
	//각각 미리 컴파일 타임에 미리 주소 할당이 됨
    func method1() { print("Struct - Direct method1") }
    func method2() { print("Struct - Direct method2") }
}


let myStruct = MyStruct()
//미리 할당된 주소로 가서 실행을 하게된다
myStruct.method1()
myStruct.method2()

 

 컴파일 시간에 호출할 메서드가 결정되고 호출되는 방식입니다

클래스의 메서드가 final로 선언되거나 

구조체 또는 열거형일 경우에 발생하게됩니다

오버헤드가 적으면 실행 속도가 빠릅니다

 

오버헤드❓

어떤 작업을 수행하는 데 필요한 추가적인 비용이나 부담입니다

런타임 비용 / 메모리 사용량 / 호출속도

=> 오버헤드가 적다는 뜻은 추가적인 비용이나 부담이 적다는 뜻입니다

 

Table Dispatch

동적 디스패치

virtual table / witness table

 

class FirstClass {
    func method1() { print("Class - Table method1") }
    func method2() { print("Class - Table method2") }
}

//데이터 영역에 이렇게 정리를 하게 됨
/**================================================
 func method1() { print("Class - Table method1") }
 func method2() { print("Class - Table method2") }
===================================================**/


// 자식클래스에서 테이블을 따로 보유
class SecondClass: FirstClass {
    override func method2() { print("Class - Table method2-2") }
    func method3() { print("Class - Table method3") }
}
//주소도 다 배열로 지정하게됨
/**================================================
 func method1() { print("Class - Table method1") }
 func method2() { print("Class - Table method2-2") }
 func method3() { print("Class - Table method3") }
===================================================**/



let first = FirstClass()
first.method1()
first.method2()


let second = SecondClass()
second.method1()
second.method2()
second.method3()

 

 

Virtual Table

class FirstClass: MyProtocol {
    func method1() { print("Class - Virtual Table method1") }
    func method2() { print("Class - Virtual Table method2") }
    func anotherMothod() { print("Class - Virtual Table method3") }
}

/**==============================================================
[Class Virtual Table]
- func method1() { print("Class - Virtual Table method1") }
- func method2() { print("Class - Virtual Table method2") }
- func anotherMothod() { print("Class - Virtual Table method3") }
=================================================================**/

/**==============================================================
[Protocol Witness Table]
- func method1() { print("Class - Virtual Table method1") }   
// 요구사항 - 우선순위 반영⭐️
- func method2() { print("Class - Virtual Table method2") }   
// 요구사항 - 우선순위 반영⭐️
=================================================================**/



let first = FirstClass()
first.method1()           // Class - Virtual Table method1
first.method2()           // Class - Virtual Table method2
first.anotherMothod()     // Class - Virtual Table method3


let proto: MyProtocol = FirstClass()
proto.method1()           // Class - Virtual Table method1  
(Witness Table)
proto.method2()           // Class - Virtual Table method2  
(Witness Table)
proto.anotherMothod()     // Protocol Extension - Direct method

 

가상 테이블은 클래스의 인스턴스 메서드에 대한 호출을 동적으로 지원하기 위해 사용합니다

클래스가 상속을 통해 메서드를 오버라이드하면

해당 메서드에 대한 호출은 클래스의 인스턴스 유형에 따라 동적으로 결정됩니다

 

이로 인해 컴파일러는 클래스의 메서드를 가상 테이블에 저장하고 객체의 인스턴스 유형에 따라 해당 테이블을 참조하여

올바른 메서드를 호출합니다

그래서 가상 테이블은 메서드 호출에 대한 동적 디스패치입니다

 

 

Witness Table

protocol MyProtocol {
    func method1()    // 요구사항 - Witness Table
    func method2()    // 요구사항 - Witness Table
}


extension MyProtocol {
    // 요구사항의 기본 구현 제공
    func method1() { print("Protocol - Witness Table method1") }
    func method2() { print("Protocol - Witness Table method1") }
    
    // 필수 요구사항은 아님 ==> Direct Dispatch
    func anotherMothod() {
        print("Protocol Extension - Direct method")
    }
}

 

목격자 테이블이라고 말하는 위트니스 테이블은

프로토콜을 준수하는 유형에서 메서드 또는 속성의 구현을 보장하기 위해서 사용합니다

프로토콜은 일종의 게약으로 프로토콜을 준수하는 유형은 해당 프로토콜에 선언된 요구사항을 충족시켜야 합니다

그래서 컴파일러는 프로토콜의 각 요구사항에 대한 실제 구현을 저장하는 위트니스 테이블을 생성하게됩니다

프로토콜을 준수하는 유형이 해당 프로토콜을 사용할 때 요구사항을 충족시키는 데 사용됩니다

 

 

메세지 디스패치

class ParentClass {
    @objc dynamic func method1() { print("Class - Message method1") }
    @objc dynamic func method2() { print("Class - Message method2") }
}


/**================================================
 func method1() { print("Class - Message method1") }
 func method2() { print("Class - Message method2") }
===================================================**/



class ChildClass: ParentClass {
    @objc dynamic override func method2() 
    { print("Class - Message method2-2") }
    @objc dynamic func method3() 
    { print("Class - Message method3") }
}


/**================================================
 super class
 func method2() { print("Class - Message method2-2") }   
 // 재정의한 메서드는 다시 주소가짐
 func method2() { print("Class - Message method3") }
===================================================**/


let child = ChildClass()
child.method1()
child.method2()
child.method3()

 

메세지 디스패치는 Objective-C에서의 메세지 전달 방식과 유사한 개념이라고 생각하면 됩니다

Objective-C 객체가 메세지를 받으면 해당 메서드를 실행하기 위해 동적 디스패치를 사용합니다

swift에서는 반드시 dynamic 키워드가 필요한것을 볼 수 있습니다

 

 

✏️참고

앨런스위프트 문법 자료(강의) - ⭐️추천

반응형