image

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
    
  • 위와 같이 프로토콜 본체에 메소드가 정의되있는 지의 여부 에 따라 결과적으로, 추상타입 변수에 할당된 프로토콜 구현체 인스턴스가 호출하는 구체적인 메소드가 달라짐을 알 수 있음

  • 처음의 상황, 즉 프로토콜 본체에 메소드를 명시한 상황에서는 프로토콜 타입의 구현체가 구현체 내부에서 재정의한 메소드와, extension 확장부에서 디폴트로 구현한 메소드 중 어느 것을 호출할 지 알아야 하기 때문에 Dynamic Dispatch가 여기에 관여하게 됨

  • 하지만 두번 째 상황, 즉 프로토콜 본체 없이 extension 확장부에만 디폴트로 구현한 메소드는 프로토콜 구현체에서 똑같이 메소드를 재정의하더라도 변수가 프로토콜 타입으로 정의된 상황이라면, 무조건 디폴트 메소드가 호출됨

  • 이는 위에서 언급한 대로 Static Dispatch가 여기에 관여하기 때문에, 컴파일 시점에 어떤 것을 호출할 지 미리 결정되었기 때문

    • 추가로 이러한 상황에서는 Protocol Witness Table 을 런타임 시점에 거치지 않는다는 것을 알 수 있음

결론

  • extension 은 본체에서 미쳐 정의하지 못한 기능을 다양하게 확장할 수 있음

    • String, UIColor 등 Swift에서 기본적으로 제공하는 타입에 대해 extension 사용을 통해 개발자가 원하는 방식으로 기능을 추가해서 사용할 수 있음
  • 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의 대상이 되도록 하는 것이 성능상 유리하기 때문에 위와 같은 특성의 적용을 고려해볼 수 있을 것


Reference