본문 바로가기

iOS

[iOS] Static 그리고 Dynamic Dispatch

1. Static Dispatch & Dynamic Dispatch


Dispatch 란 어떠한 메서드를 호출할 것인지를 결정하여 그것을 실행하는 메커니즘을 의미합니다.

 

Swift에서는 Static 방식과 Dynamic 방식을 지원하며 이는 내가 호출할 함수를 컴파일 타임에 결정하는가, 런타임에 결정하는가를 의미합니다.

 

Static Dispatch(Direct Call)

  • “컴파일 타임” 에 호출될 함수를 결정하여 런타임때 그대로 실행합니다.
  • 컴파일 타임에 결정이 나기 때문에 성능상 이점을 갖습니다.

 

Dynamic Dispatch(Indirect Call)

  • “런타임" 에 호출될 함수를 결정하여 그대로 실행합니다.
  • 때문에 Swift에서는 클래스마다 함수 포인터들의 배열 vTable(Virtual Dispatch Table)을 유지합니다.
  • 서브클래스가 메서드를 호출할 때, 이 vTable을 참조하여 실제 호출할 함수를 결정하기 때문에 성능상 손해를 보게 됩니다.

 

 

2. Swift 에서의 Dispatch


 

Reference Type에서의 Dispatch


Reference Type에 대표적인 예시 Class는 상속 가능성이 존재합니다.

 

따라서, 서브 클래스에서 함수를 호출할 수 있기 때문에 Dynamic Dispatch를 사용합니다.

 

즉 상속 가능성 → 오버라이딩의 가능성이 존재하기 때문에 Dynamic Dispatch를 사용합니다.

 

class Human {
	func sayHello() {
		print("Hello Human!")
	}
}

//Subclassing
class Developer: Human {
	// Overriding
	override func sayHello() {
		print("Hello Developer!")
	}
}

let ian: Human = Developer()
ian.sayHello()    // Hello Developer!

 

이처럼 서브클래스에서 부모 클래스의 메서드를 오버라이딩하게 되는 경우 Dynamic Dispathc가 일어납니다.

 

ian 이라는 변수의 타입은 Human 타입이지만 Developer 인스턴스를 업캐스팅 해서 가리키고 있기 때문에 이때는 Human 클래스의 sayHello 메서드를 참조하는 것이 아니라 Developer 클래스의 sayHello 메서드를 참조하게 됩니다.

 

이처럼 컴파일러는 클래스의 메서드가 서브클래스에서 오버라이딩이 될 경우를 대비해, 부모 클래스의 sayHello를 참조해야 하는지, 서브

클래스의 sayHello를 참조해야 하는지를 확인하는 작업이 필요합니다.

 

sayHello라는 함수는 각 클래스마다 가지고 있는 vTable 내에 함수 포인터로 두고, 실제 런타임 시점에 vTable을 사용하여 어떠한 메서드가 불릴지를 결정합니다.

 

즉, 런타임 과정에 해당 클래스의 vTable에서 함수를 찾아 메모리 주소를 읽고 → 그 주소로 점프하여 수행해야 하기 떄문에 추가적인 명령이 수행되며 오버헤드가 발생하여 성능상 손해를 보게 됩니다.

 

그러면 "나는 상속을 안 할 건데 자꾸 함수를 호출할 때 마다 Dynamic DIspatch로 동작하게 되면 성능에서 손해를 보는게 아닌가?" →

네 맞습니다, 따라서 상속이 필요없는 Class는 Static Dispatch로 동작하게 하여 성능 향상을 챙기는것이 성능상 이점을 갖습니다.

 

 

Value Type에서의 Dispatch


Value Type 인 구조체, 열거형은 상속을 할 수 없다는 특징 때문에 오버라이딩 가능성이 존재하지 않으며 따라서 Static Dispatch를 사용합니다.

 

struct Human {
	func sayHello() {
		print("Hello Human!")
	}
}

 

구조체의 경우 어디서 sayHello 메서드를 호출해도 늘 Human 이라는 구조체 안에서 함수가 불릴 것을 보장하기 떄문에 런타임 시점에 별도 추적이 필요하지 않아 컴파일 시점에 결정이 됩니다.

 

Extension 에서의 Dispatch


 

Value Type Extension Dispatch

  • Value Type은 상속의 가능성이 없기 때문에, Extension을 하여도 Static DIspatch로 동작합니다.

 

Reference Type Extension Dispatch

  • Class에서 Extension으로 메서드를 추가 정의할 경우 → 서브클래스에서 오버라이딩이 불가하죠 → Static DIspatch로 동작합니다.

 

class Human {
	func sayHello() {
		print("Hello Human!")
	}
}

extension Human {
	func overrideItIfYouCan() {
		print("Can you?")
	}
}

class Developer: Human {
	override func overrideItIfYouCan() {
		// error! Overriding non-@objc declarations from extensions is not supported
    }
}

 

Reference Type에서 extension으로 메서드를 추가할 경우, non-objc 의 경우에는 메서드 오버라이딩이 불가합니다.

(물론 @objc 를 붙여주면 오버라이딩이 가능합니다)

 

따라서 일반적인 Extension을 통해 제공된 메서드의 경우 오버라이딩이 불가하기 때문에 부모 클래스의 Extension내에 존재하는 메서드가 호출될 것을 보장합니다.

→ Static DIspatch로 동작합니다 😀

 

Swift에서는 클래스보다 구조체 사용을 더 장려하는 이유가 메모리 관점의 성능 차이도 있지만, Dispatch로 인한 성능 차이도 있는것 같습니다 :D

 

Dynamic Dispatch는 줄이고 Static Dispatch를 적극 활용하자!


 

상속, 오버라이딩이 될 필요가 없는 클래스, 메서드, 프로퍼티에는 final을


final 키워드는 클래스, 메소드 또는 프로퍼티의 선언을 오버라이드 할 수 없도록 제한합니다.

 

따라서 컴파일러가 간접 호출(Dynamic Dispatch) 대신 직접 호출(Static Dispatch) 을 사용할 수 있게 됩니다.

 

예를 들어 다음 C.array1 및 D.array1 에서 직접 호출이 사용됩니다. 반면 D.array2 는 vTable을 통해 간접 호출됩니다.

// Final 
final class C {
	var array1: [Int]
	func doSomething() { ... }
}

// Non-Final
class D {
	final var array1: [Int]
	var array2: [Int]
}

func usingC(_ c: C) {
	c.array1[i] = ...
	c.doSomething() = ...
}

func usingD(_ d: D) {
	d.array1[i] = ...
	d.array2[i] = ...
}

 

파일 외부에서 접근할 필요가 없다면 private 을


private 키워드는 같은 scope 내에서만 접근이 가능하도록 제한합니다.

→ 컴파일러는 private 키워드가 참조될 수 있는 곳에서 오버라이딩 가능 여부를 판단합니다.

→ 오버라이딩이 될 가능성이 없다면 스스로 final 키워드를 추론하여 Static Dispatch로 동작시킵니다.