[Swift] final 파헤치기
개발🧑‍💻/Swift

[Swift] final 파헤치기

블로그 글을 오랜만에 쓰려니 어색,, 어색,,

묵혀놨던 글 소재거리 발견해서 공부하고 포스팅해봅니다,,, 이제부터 말투는 내 쓰고 싶은 대로 쓸 거임,,,

시작!


이전에 부스트 코스 iOS 앱 프로그래밍에서 코드 리뷰를 받았을 때 이러한 리뷰 내용이 있었다.

dynamic...? static...?

리뷰어님 dispatch가 뭔데요...

내가 아는 건 요건디ㅋㅋㅋㅋㅋ

method dispatch라고 검색하니 그제야 내가 찾고 싶었던 것들이 조금씩 보이기 시작했다!

그럼 method dispatch는 뭘까...?

Method Dispatch

간단하게 말하면, Method Dispatch는 어떤 작업을 실행해야 하는지, 더 구체적으로는 어떤 메서드를 호출해야 하는지 결정하는 데에 사용되는 알고리즘 혹은 메커니즘이라고 할 수 있다.

음... 일단 ok.... 그럼 이제 리뷰받은 내용을 다시 보면

더 이상 상속이 필요 없는 class의 경우 final 키워드를 붙여서 최적화할 수 있습니다. Swift에서는 override를 위해 class function 호출 시 dynamic dispatch 테크닉을 사용합니다. final 키워드를 붙이면 dynamic dispatch 대신 static dispatch를 사용하므로 성능 향상에 도움이 됩니다.

그럼 대충 느낌은,,,

dynamic dispatch라니까 뭔가 동적으로... 어떤 메서드를 호출할지 결정한다...

반대로 static dispatch는 정적으로... 어떤 메서드를 호출할지 결정한다... 일까...? 자세히 알아보도록 하자...

Dynamic Dispatch

애플 스위프트 블로그(고대문서급)

Like many other languages, Swift allows a class to override methods and properties declared in its superclasses. This means that the program has to determine at runtime which method or property is being referred to and then perform an indirect call or indirect access. This technique, called _dynamic dispatch_, increases language expressivity at the cost of a constant amount of runtime overhead for each indirect usage. In performance sensitive code such overhead is often undesirable.

이하 개발새발 번역)

다른 많은 언어들처럼, Swift는 클래스가 그 부모 클래스의 메서드와 프로퍼티를 오버라이드(재정의)할 수 있게 합니다. 이는 프로그램이 런타임 도중에 어떤 메서드 혹은 프로퍼티를 참조하고 있는지 결정하고 간접 접근 혹은 간접 호출을 수행해야 한다는 것을 의미합니다. 이 기술은 `dynamic dispatch`라고 불리고, 간접 사용마다 런타임에 일정한 오버헤드를 대가로 하여, 언어의 표현력을 향상합니다. (대충 표현력이 증가하는 대신 런타임에 일정한 오버헤드가 부담된다는 뜻). 성능에 민감한 코드에서는, 이러한 오버헤드는 종종 올바르지 않습니다.

깃허브 스위프트 docs, optimization Tips

In Swift, dynamic dispatch defaults to indirect invocation through a vtable.

스위프트에서, dynamic dispatch는 기본적으로 vtable을 통한 간접 호출입니다.

A virtual method table or 'vtable' is a type specific table referenced by instances that contains the addresses of the type's methods. Dynamic dispatch proceeds by first looking up the table from the object and then looking up the method in the table.

virtual method table 혹은 vtable은 해당 타입(클래스)의 메서드들의 주소를 포함하는 인스턴스에서 참조하는 타입(클래스)의 유형별 테이블입니다. dynamic dispatch는 먼저 객체에서 테이블을 찾은 다음, 그리고 테이블에서 메서드를 찾습니다.

위 글들을 보면 dynamic dispatch는 메서드를 간접 호출 시 vtable(virtual method table,,, 이름이 여러 개 인 듯)을 참조하는는 것을 알 수 있다. 그런데 이 vtable은 함수 포인터들의 배열로 관리되고, 하위 클래스가 메서드를 호출할 때 이 테이블을 참조하여 실제 호출할 함수를 결정한다고 한다. 그런데 런타임 도중에 이걸 한다고? 그럼 속도가 느리지 않음? 요? 그럼 이거 왜 써요 >>> 다형성 (OOP) 때문

 

정리: dynamic dispatch는 프로그램 런타임 도중 (<<<중요) vtable을 통해 어떤 메서드 혹은 어떤 프로퍼티를 참조하고 있는지 결정한다.

Static Dispatch

반면 Static Dispatch는 컴파일 타임 (<<<중요)에 실제 호출할 메서드나 프로퍼티를 결정하기 때문에 호출 과정이 간단(직접 호출)하고 속도가 빠르다. (vtable? 그게 몬데 ㅋ)

컴파일러는 함수가 호출되면 직접 메모리 주소에 덤프 하여 연산을 수행한다.

그래서 우리는 더 이상 상속이 필요하지 않은 클래스에 final 키워드를 붙여 Dynamic Dispatch가 아닌, Static Dispatch가 일어나게 하여 최적화를 진행할 수 있다!

 


 

근데... Swift의 클래스는 Reference Type(참조 타입)이고, 상속이 가능하다.

위에서 더이상 상속이 필요하지 않은 클래스에

final 키워드를 붙여 컴파일러에게 더이상 오버 라이딩이 일어나지 않는다고 알려주고,

Dynamic Dispatch가 아닌, Static Dispatch가 일어나게 할 수 있다고 했다.

 

그렇다면 상속이 불가한 Value Type(값 타입), 그리고 Protocol, Extension은 어떨까?

 

먼저 Value Type에는

Enum과 Struct가 있다. 근데 이들은 상속이 불가능하다. 즉, Static Dispatch가 적용된다.

 

이번에는 Protocol을 보자,

 

1. 프로토콜 본체에 기본 정의된 함수의 경우

protocol SomeProtocol {
  func action() -> Int
  }

  class SomeClass: SomeProtocol {
  func action() -> Int { 2 }
  }

  class DerivedClass: SomeClass {
      override func action() -> Int { 3 }
  }

  var c:SomeProtocol = SomeClass()
  print(c.action()) // 2

  c = DerivedClass()
  print(c.action()) // 3

SomeProtocol을 채택한 SomeClass는 action()이란 메서드를 구현했고 2를 리턴한다.

SomeClass를 상속받은 DerivedClass는 action을 재정의하여 3을 리턴한다.

이렇게 Protocol을 통해서도 override 할 수 있으므로, 이 경우 Protocol은 dynamic dispatch를 사용한다.

 

2. 프로토콜 본체에 없는 함수를 extension으로 추가한 경우

 protocol SomeProtocol { }

   extension SomeProtocol {
       func action() -> Int { 4 }
   }

   class SomeClass: SomeProtocol {
       func action() -> Int { 2 }
   }

   class DerivedClass: SomeClass {
       override func action() -> Int { 3 }
   }  

   var c:SomeProtocol = SomeClass() 
   print(c.action()) // 4

   c = DerivedClass()
   print(c.action()) // 4

프로토콜 본체에 선언하지 않고 extension으로 추가한 메서드의 경우 vtable을 가질 수 없다고 한다. 그러므로 static dispatch가 사용된다.

 

마지막으로,

Extension에서는 override가 불가능하다. 따라서 static dispatch가 사용된다.

마무리

final 키워드를 쫓아 멀리멀리까지 와버렸다... 뜻하지 않게 글이 고봉밥🍚이 되어버렸다... 너무 배부르다 

아무튼 우리는 더 이상 상속이 필요하지 않은 클래스에 final 키워드를 붙여 Dynamic Dispatch가 아닌, Static Dispatch가 일어나게 하여 최적화를 진행할 수 있다!

 

그리고 이는 실제로 스위프트 공식 문서(Inheritance - Preventing Overrides)에도 나와있는 이야기이다.

역시... 공식 문서에는 없는 것이 없다... 사실 원피스는 공식문서가 아닐까?

Reference

Static vs Dynamic Dispatch in Swift: A decisive choice

의 번역본

Swift의 Dispatch 규칙

Dynamic Dispatch와 성능 최적화

깃허브 스위프트 docs

스위프트 공식문서

apple developer swift blog