Extension의 일반적으로 알려진 이점
Swift의 extension
키워드는 일반적으로 아래와 같은 기능을 수행
- 원하는 타입에 대해 인스턴스 속성이나 타입 속성(
static
)을 추가적으로 정의할 수 있음 - 새로운 인스턴스 메소드나 타입 메소드를 정의할 수 있음
- 새로운 생성자를 정의할 수 있음
- 서브스크립트를 정의할 수 있음
- 새로운 내장 타입(nested type)을 정의할 수 있음
- 이미 존재하는 타입에 대해 새로운 프로토콜의 채택을 추가할 수 있음
각 언급에 대한 예시는 여기 를 추후 필요 시에 참고하도록 하자!
Extension과 Method Disaptch디스패치
값 타입, 참조 타입, 프로토콜에
extension
을 적용했을 때 각각 Static Dispatch, Dynamic Dispatch 중 어느 것의 대상이 되는 지 알아보자!
-
Static Dispatch, Dynamic Dispatch는 각각 스위프트 코드에서 성능을 좌우하는 중요 요소 중 하나임
Method Dispatch?
-
간단히 말해, 메소드를 호출해야 할 때 이와 관련된 명령 정보가 메모리상의 어디에 위치했는 지를 찾아가는 과정이라 보면 됨
-
좀 더 구체적으로 얘기하면, 타입별로 가지고 있는 Virtual Method Table 이라는 배열을 참고해서 위치를 찾아가는 것
-
상황에 따라서 컴파일 시점에 작성된 코드만 보고도 위치를 알 수 있는 상황이 있고, 런타임 시점에 프로세스가 어떻게 동작하는 지를 봐야 알 수 있는 상황이 있음
-
전자의 경우에는 Static Dispatch, 후자의 경우에는 Dynamic Dispatch가 각각 적용된다고 보면 됨
-
당연히 위치를 찾는 과정이 런타임 시점에 일어나는 것은 부가적인 연산이 발생함을 의미하기 때문에 성능을 떨어뜨리는 요인이 될 수 있음
-
-
일반적으로는 명령의 위치를 찾는 과정은 런타임 시점보다는 컴파일 시점에 미리 알아두는 것이 효율적이기 때문에, 가능하면 Static Dispatch의 대상이 되도록 하는 것이 성능상 좋음
-
보통, 구조체나 열거형과 같이 상속의 가능성이 없는 값 타입(Value Type) 데이터 구조는 Static Dispatch의 대상임
내부에 참조 타입 속성을 가지고 있다면 이야기가 달라지겠지만,
여기서는 거기까진 생각하지 않겠다//상속이 불가능하기 때문에, 아래 타입의 인스턴스들은 형태가 바뀌지 않으므로 컴파일 시점에 명령의 수행 정보를 미리 확정지어 알 수 있음 struct SomeStruct { var name: String = "Some Struct" func someFunction() { print("Hello World") } }
-
클래스와 같은 참조 타입(Reference Type)의 데이터 구조는 상속의 가능성이 존재하기 때문에 Dynamic Dispatch의 대상임
- 실제로 오버라이딩이 되어 있더라도, 이를 따지지 않고 우선은 참조 타입일 경우에는 상속의 가능성이 항상 존재 하기 때문에, 런타임 시점에 정확히 어떤 메소드가 호출되야 하는 지를 확인하는 것
//아래와 같이 상속이 가능하기 때문에 동일한 메소드라도, 어떤 메소드가 호출될 지 확정지어 알 수 없음 class SomeClass { var name: String = "Some Class" func someFuncion() { print("This method can be overwritten!") } } class SomeChildClass: SomeClass { override func someFunction() { print("This method is overwritten by Child Class!") } } //부모 타입 변수지만, 인스턴스는 자식 타입을 할당(업캐스팅) var someInstance: SomeClass = SomeChildClass() //런타임 시점에 이것이 부모 타입 메소드인 지, 오버라이딩 된 자식 타입의 메소드인 지를 결정해야 하는 것 someInstance.someFunction()
-
extension
을 단순한 기능의 확장 용도로 사용하는 경우에도, Static & Dynamic Dispatch의 적용 여부는 기본적으로 위의 기준을 따라간다고 보면 됨- 단, 참조 타입의 경우 컴파일러가 오버라이딩이 없을 것임을 미리 알 수 있는 상황이라면 Static Dispatch가 적용됨
//final 클래스는 상속이 불가능하므로, Static Dispatch의 대상이 됨 final class SomeFinalClass{ func someMethod(){ print("This method cannot be overwritten") } }
-
프로토콜의 경우에는 기본적으로 선언만 제공하고, 구현은 채택의 대상에 따라 달리지기 때문에 기본적으로 Dynamic Dispatch가 적용됨
Protocol Witness Table
- 보통 프로토콜을 채택한 타입마다
protocol witness table
이라는 것을 가지게 됨 - 일반적인 클래스의 메소드가
virtual method table
을 따라가지만, 프로토콜 채택 후 준수한 메소드의 경우에는protocol witness table
을 따라가서 호출할 메소드를 찾게 됨 - 서로 이름과 용도는 다르지만, 결국은 둘 다 런타임 시점에 메소드를 호출할 때 명령의 수행정보를 찾기 위한 과정이라는 점에서 Dynamic Dispatch가 적용되는 것
- 보통 프로토콜을 채택한 타입마다
-
하지만
extension
을 통해 프로토콜을 확장시킬 경우에는, Static Dispatch가 예외적으로 적용될 수 있음- 프로토콜 본체에 있는 메소드를 재정의하는 경우에는 Dynamic Dispatch의 대상이 됨
- 프로토콜의 확장부에만 메소드를 정의할 경우에는 Static Dispatch의 대상이 됨
//여기에 선언되는 것들은 Dynamic Dispatch의 대상 protocol SomeProtocol { func func1() } //위에 선언되지 않고 아래에만 선언되는 것들은 Static Dispatch의 대상 extension SomeProtocol { //Dynamic Dispatch func func1() { print("Hello") } //Static Dispatch - 해당 메소드는 본체에는 정의되있지 않음 func func2(){ print("Hello2") } }
Protocol Extension in Swift
-
바로 위에서 언급한 것을 종합해보면, 프로토콜의 본체에 명시된 것들은 Dynamic Dispatch의 대상이 되고 본체에 명시되지 않고
extension
확장부에만 명시된 것들은 Static Dispatch의 대상이 됨 -
좀 더 나아가서 아래와 같은 상황을 가정해보자
- Apartment라는 빈 프로토콜을 선언한 후 해당 프로토콜의
extension
에 구현해야 할 메소드를 정의하고, Trimage 라는 프로토콜 구현체에서 프로토콜 메소드를 재정의 extension
확장부에는 아래와 같이 디폴트 구현체를 같이 선언
import Foundation protocol Apartment {} extension Apartment { func call() { print("This is Apartment") } } class Trimage: Apartment { func call(){ print("This is Trimage in SeongSu") } }
- 이후 아래와 같이 추상 타입으로 선언한 변수에 구현체 인스턴스를 할당한 후, 메소드를 호출
let building: Apartment = Trimage() building.call()
- 출력을 확인해보면, 구현체에서 정의한 대로 출력될 것 같지만 정작
extension
확장부에 정의한 디폴트 구현체 내용대로 호출됨
This is Apartment
- 만일 Apartment 프로토콜에 해당 메소드를 정의했다면? 위와 달리 클래스에서 재정의한 내용대로 호출되는 것을 확인할 수 있음
protocol Apartment { func call() }
This is Trimage in SeongSu
- Apartment라는 빈 프로토콜을 선언한 후 해당 프로토콜의
-
위와 같이 프로토콜 본체에 메소드가 정의되있는 지의 여부 에 따라 결과적으로, 추상타입 변수에 할당된 프로토콜 구현체 인스턴스가 호출하는 구체적인 메소드가 달라짐을 알 수 있음
-
처음의 상황, 즉 프로토콜 본체에 메소드를 명시한 상황에서는 프로토콜 타입의 구현체가 구현체 내부에서 재정의한 메소드와,
extension
확장부에서 디폴트로 구현한 메소드 중 어느 것을 호출할 지 알아야 하기 때문에 Dynamic Dispatch가 여기에 관여하게 됨 -
하지만 두번 째 상황, 즉 프로토콜 본체 없이
extension
확장부에만 디폴트로 구현한 메소드는 프로토콜 구현체에서 똑같이 메소드를 재정의하더라도 변수가 프로토콜 타입으로 정의된 상황이라면, 무조건 디폴트 메소드가 호출됨 -
이는 위에서 언급한 대로 Static Dispatch가 여기에 관여하기 때문에, 컴파일 시점에 어떤 것을 호출할 지 미리 결정되었기 때문
- 추가로 이러한 상황에서는
Protocol Witness Table
을 런타임 시점에 거치지 않는다는 것을 알 수 있음
- 추가로 이러한 상황에서는
결론
-
extension
은 본체에서 미쳐 정의하지 못한 기능을 다양하게 확장할 수 있음- String, UIColor 등 Swift에서 기본적으로 제공하는 타입에 대해
extension
사용을 통해 개발자가 원하는 방식으로 기능을 추가해서 사용할 수 있음
- String, UIColor 등 Swift에서 기본적으로 제공하는 타입에 대해
-
extension
확장부에서 추가로 정의한 것들은 확장 대상이 값 타입(Value Type), 참조 타입(Reference Type)인 지에 따라 각각 Static Dispatch와 Dynamic Dispath의 대상이 됨 -
단, 프로토콜의 경우는 기본적으로 Dynamic Dispatch의 대상이 되지만
extension
확장부에 디폴트 구현을 정의할 경우 이것의 청사진이 프로토콜 본체에 명시되있지 않다면 Static Dispatch의 대상이 됨protocol SomeProcotol { func someFun1() } extension SomeProcotol { //Dynamic Dispatch의 대상 func someFun1(){ print("This method is target for dynamic dispatching") } //Static Dispatch의 대상 func someFun2(){ print("This method can be overwritten, but is target for static dispatching") } }
-
따라서 어떤 추상 타입을 정의하고자 할 때, 가능하면 Static Dispatch의 대상이 되도록 하는 것이 성능상 유리하기 때문에 위와 같은 특성의 적용을 고려해볼 수 있을 것