Final
iOS 개발을 하다보면 Final 키워드를 붙여 클래스를 정의하는 경우를 볼 수 있습니다.
final class NetworkManager {
static let shared = NetworkManager()
private init() {}
...
}
따라서 Final 키워드의 특징은 무엇인지, 어떠한 경우에 사용해야 할 지에 대해 학습하였습니다.
문법적 특징
- Final 키워드를 선언하게 되면 상속이 불가능해집니다. 만약 Final 키워드가 선언된 것을 상속하려고 하면 컴파일 에러가 발생합니다.
- Final 키워드를 사용하면 메서드, 프로퍼티, 서브스크립트의 오버라이드가 불가능해진다는 특징을 지닙니다. 만약 final 키워드가 선언된 곳에서 오버라이드를 진행하려고 하면 컴파일 에러가 발생합니다.
문법적인 의미에서 다음과 같은 특징을 지닌다는 것은 이해했는데, 성능적인 관점에서 바라보았을 때의 이점은 없는지 궁금하여 학습하였습니다.
성능의 관점
- final 키워드를 선언하면 직접 호출(Direct call)방식으로 구현되기 때문에 컴파일 시점에 실제 호출이 결정되어 성능 개선을 기대할 수 있습니다.
final 키워드를 선언하면 클래스, 메서드, 프로퍼티를 오버라이드 할 수 없다고 하였는데, 이는 컴파일러가 indirect call이 아닌 direct call로 호출할 수 있게 됨을 의미합니다. 스위프트에서 클래스는 기본적으로 dynamic dispatch 방법을 사용합니다. Dynamic dispatch는 vtable을 이용한 간접 호출(indirect call) 방식으로, 실제 호출이 유동적(사용자의 인터렉션에 의해 일어나는 등)으로 일어나기 때문에 런타임에 결정됩니다.
하지만 직접 호출(direct call) 방식은 static dispatch 방법으로 컴파일 타임에 실제 호출이 결정되기 때문에 컴파일때 결정됩니다.
Dynamic Dispatch는 vtable을 통해 컴파일러 최적화를 진행해야 하지만, Static Dispatch는 이러한 최적화 과정이 필요없기 때문에 성능적인 개선을 기대할 수 있습니다.
실습 진행
swift docs에 있는 코드를 기반으로 차이점이 있는지 직접 확인해보았습니다.
final class C {
var array1: [Int] = [] // final class라서 정적 디스패치됨
func doSomething() {
print("C: doSomething")
}
}
class D {
final var array1: [Int] = [] // final이라 정적 디스패치
var array2: [Int] = [] // 오버라이드 가능 -> 동적 디스패치
func doSomething() {
print("D: doSomething")
}
}
func usingC(_ c: C) {
c.array1.append(1) // 정적 디스패치
c.doSomething() // 정적 디스패치
}
func usingD(_ d: D) {
d.array1.append(2) // 정적 디스패치 (final var)
d.array2.append(3) // 동적 디스패치 (var -> 오버라이드 가능)
d.doSomething() // 동적 디스패치 (오버라이드 가능)
}
// 실행 예제
let c = C()
let d = D()
usingC(c)
usingD(d)
아래의 명령어를 실행하여 swift 파일을 바이너리로 컴파일하였습니다.
swiftc -g CompareFinal.swift -o CompareFinal.out
그럼 다음과 같은 파일들이 나타납니다. dSYM 파일과 info.plist가 나타난 것이 신기했습니다.
다음 .out 파일을 lldb를 통해 디버깅을 진행해주었습니다.
lldb CompareFinal.out
각 파일들을 분석하였고, 모든 언어를 분석하면 매우 길어져 해당 함수를 사용한 usingC와 usingD를 분석해보았습니다.
disassemble --name usingC
여기서 중요한 명령어는 bl입니다.
CompareFinal.out[0x100003afc] <+112>: bl 0x1000033b0 ; CompareFinal.C.doSomething() -> () at CompareFinal.swift:4
bl(Branch with Link)은 정적 디스패치에서 사용되는 명령어로 직접 호출을 의미합니다.
다음은 usingD입니다.
disassemble --name usingD
여기서 중요한 명령어는 ldr, blr이 있습니다.
CompareFinal.out[0x100003bdc] <+204>: ldr x8, [x20]
CompareFinal.out[0x100003be0] <+208>: ldr x8, [x8, #0x78]
CompareFinal.out[0x100003be4] <+212>: blr x8
ldr x8, [x20] → 객체의 vtable 주소 로드
blr x8 → vtable을 통해 간접 호출 (동적 디스패치)
실습까지 진행해보니 실제로 다른 방식으로 동작함을 쉽게 알 수 있었습니다.
[참고]
'iOS > UIKit' 카테고리의 다른 글
[UIKit] PinLayout이란 (2) | 2025.07.30 |
---|---|
[UIKit] 메모리 관점에서의 CollectionView, PageView, ScrollView의 동작 방식 (2) | 2025.01.29 |
[UIKit] 문자열 보간법의 실행 구조 (feat. UI*.text) (0) | 2025.01.06 |
[UIKit-Storyboard] 스토리보드 컴파일 과정 (0) | 2024.12.31 |
[UIKit-Code] UIKit 코드 기반 프로젝트 세팅하기 (스토리보드 없애기) (0) | 2023.12.27 |