[iOS/Swift] WMO와 Method Dispatch - class에 final을 붙이는 진짜 이유

일반적으로 더이상 상속되지 않을 class 앞에 `final`이라는 키워드를 붙이곤 한다.

그렇게 하면 이 class는 더이상 상속되지 않을걸 알기에, 시스템의 입장에서 재정의를 위한 일련의 과정을 수행하지 않아도 되기 때문에 성능상에 이점이 있다고 알고 있었다.

 

그런데, 위의 문장으로는 뭔가 설명이 조금 부족해보인다.

이번 글에서는 WMO와 Method Dispatch에 대한 설명과 함께 `final`을 붙이는 진짜 이유에 대해 알아보도록 하겠다.


WMO: 전체 모듈 최적화

xcode에 여러 개의 swift파일이 존재할 때 빌드를 하면, Swift는 각 파일을 개별적으로 컴파일한다.

그런데 사실상 각각의 파일이 개별적으로 존재하는 경우는 잘 없다. 그러니까, 각 파일은 내부적으로 서로 연관이 있는 경우가 일반적이다.

※ 예를 들어 다른 파일에 있는 메서드를 사용한다든지, 다른 파일에 있는 class를 상속받아서 사용한다든지가 그런 경우이다

 

그런데 이러한 연관 관계와 상관없이 일단 개별적으로 컴파일 한 이후에, 런타임에서 앱을 사용할 때 이 연관관계들을 찾아서 연결하는 방식으로 동작한다.

이런 동작의 장점은 컴파일에 시간이 적게 든다는 점이고, 개발자가 앱을 테스트할때 이러한 동작은 유용할 수 있다.

 

그러나, 실제로 앱이 출시된 상황이라면 어떨까?

사용자가 앱을 사용하는 런타임 시점에 내부적으로 각 파일의 연관관계를 찾는 동작이 일어난다면... 좋지 않은 사용자 경험을 일으킬 수 있다.

 

그래서 앱을 만들고, 테스트하는 Debug모드가 아닌, 출시하는 단계인 Release모드에서는 위와는 조금 다른 형태로 동작하게 된다.

런타임 시점이 아니라, 컴파일 시점에 모든 연관된 파일들을 묶어서 컴파일하는 방식인 WMO라는 형태로 동작하게 된다.

 

WMO를 사용하게 되면 컴파일 과정에서 모든 연관 관계를 찾아야 하기 때문에 컴파일 시간은 더 오래 걸릴지 몰라도, 

런타임 과정에서는 연관관계를 찾는 과정이 필요없기 때문에 속도가 더 빨라진다는 이점이 있다.


Swift Optimization Tips

 

사실 WMO의 과정은 내부적으로 일어나기 때문에, 개발자가 크게 관여할 수 있는 부분은 없다.

다만, 코드를 짤때 조금더 최적화에 도움을 줄 수 있는 방법은 있다.

 

위에서 컴파일(또는 런타임)과정에서 연관관계를 찾는 과정이 있다고 했는데, 애초에 코드를 짤 때 연관 관계가 없는 파일들을 직접 명시해줌으로써, 연관관계를 찾는 과정을 줄일 수 있다.

 

이렇게 연관 관계가 없는 파일들은 관계를 끊어주고 필요한 파일들만 엮어서 컴파일하도록 돕는 이 과정을 Swift Optimization Tips 즉, Swift 최적화 팁이라고 한다.

 

Swift Optimization Tips중에 가장 쉽게 접근할 수 있는건 `final`과 `private` 등이다.

쉽게 생각해보면, "상속을 받아서 쓸 이유가 없는 class 앞에 `final`을 붙임으로써 다른 파일들과의 연관관계를 끊는다."

이 문장만으로 이미 최적화에 도움이 되는 이유가 쉽게 이해갈것이다.

 

그런데, 조금 더 들어가서 "왜? 그리고 어떻게?" 최적화에 도움이 되는지 궁금해졌다.

이 이유를 알기 위해서 Method Dispatch(동적 디스패치)에 대해 간단히 살펴보겠다.


Method Dispatch

클래스는 기본적으로 상속이라는 특징을 가지고 있다.

그러다보니 class안에 정의된 메서드가 언제 실행이 될지 컴파일러는 알 수 없다.

예를 들어 `A class`에 있는 메서드가 `A class`에서 실행이 될지, 혹은 `A class`를 상속받은 `B class`에서 재정의(`override`)된 형태로 실행이 될지 컴파일 시점엔 알지 못한다.

이를 Dynamic Dispatch(혹은 Indirect Call)라고 한다.

 

반면 구조체의 경우 상속 개념이 없다.

즉, 구조체에 어떤 메서드가 있다면 해당 구조체 안에서 실행될 것이 확실하다.

이를 Static Dispatch(혹은 Direct Call)라고 한다.

결국 class와 같은 참조타입의 경우 성능상 오버헤드가 발생할 가능성이 있고, 

구조체와 같은 값타입의 경우 성능상 이점이 있다는 것을 알 수 있다.

 

그럼 class중에 상속되지 않는다고 확신하는 경우, 해당 메서드를 Static하게 동작하게 하면 어떨까?


final을 붙이는 진짜 이유

Method Dispatch 개념에서 힌트를 얻었듯이, 상속되지 않는 class에 final을 붙이게 되면, 이제 이 class 안의 메서드들의 실행시점이 확실히 결정되게 된다.

즉, class는 참조타입임에도 불구하고 Static Dispatch로 동작하게 되는 것이다.

 

이제, 누군가 final을 붙이는 이유에 대해 묻는다면 다음과 같이 답할 수 있게 되었다.


 

※참고자료

https://github.com/swiftlang/swift/blob/main/docs/OptimizationTips.rst#enabling-optimizations

 

swift/docs/OptimizationTips.rst at main · swiftlang/swift

The Swift Programming Language. Contribute to swiftlang/swift development by creating an account on GitHub.

github.com