iOS/Swift Grammar

[Swift] final을 사용해야 하는 이유 with Dispatch

kyxxn 2024. 5. 22. 00:54
728x90

학습 목표

  • Dispatch에 대해 알아보기
  • Static Dispatch & Dynamic Dispatch 차이
  • Reference Type | Value Type | Protocol에서의 Dispatch 동작
  • Extension 했을 때의 Dispatch 동작

학습 내용

본 주제에 대한 개요

🧑🏻‍💻 프로젝트에서 final 키워드를 모든 class에 다 붙여 두셨는데 이유가 있나요?

🧑🏻‍💼 더이상의 상속을 하지 않겠다는 의미로 final 키워드를 모두 붙여주었습니다!

🧑🏻‍💻 그렇다면 final 키워드를 사용함으로써 상속을 막는것 이외에 다른 장점은 무엇이 있을까요?

🧑🏻‍💼 ?

출처: https://itllbegone.tistory.com/10

Dispatch에 대한 개념을 알아야 final을 써야 하는 이유에 대해 알 수 있음

  1. Dispatch 개념을 보고,
  2. final을 써야하는 이유를 보겠음

Dispatch가 뭔데 ?

내가 아는 Dispatch는 CollectionView에서 셀 생성할 때 DispatchQueue에서 사용

Dispatch

어떤 메소드를 호출할 것인지 결정하고,
해당 메소드를 실행하는 매커니즘이다.

상속 관계에서, 프로토콜 채택 관계.. 등에서

메소드 호출을 ‘컴파일’ 때 정하는지, ‘런타임’ 때 정하는지에 따른 방식

Static Dispatch

‘컴파일' 시점에 호출될 함수를 결정

이 때 정해진 함수로, 런타임 때 위 함수가 실행됨

컴파일 시점에서 결정이 나기 때문에,

런타임에서 결정 안 해도 되는 성능상의 이점을 지님

Dynamic Dispatch

‘런타임'에 호출될 함수를 결정함

따라서 Swift는 클래스마다 함수 포인터들의 배열인 vTable을 유지함

하위 클래스가 메소드를 호출할 때, 해당 클래스의 vTable을 참조하여

어떤 함수를 호출할 지에 대한 결정을 함

위와 같은 과정이 런타임에 발생하므로 성능을 손해보게 됨

한 줄 요약

  • Static Dispatch - 컴파일 시점에 실행할 함수들이 결정되어 있음
  • Dynamic Dispatch - 런타임 시점에 함수를 결정해야 하므로, 성능상 손해봄

Swift에서 Dispatch 동작 방식

Reference Type, Value Type, Protocol 세 가지 경우에서 보겠음

Reference Type(클래스)에서의 Dispatch 동작

레퍼런스 타입의 클래스에는 상속이 있음
즉, 자식 클래스에서 함수를 호출할 수 있기에 Dynamic Dispatch를 사용

클래스에는 Dynamic Dispatch가 사용되는데

상속을 했을 때, 자식 클래스에서 부모의 메소드를 오버라이딩 했다면

둘 중에 뭐 호출해야할 지에 대한 vTable 읽는 추가 명령어 연산이 필요함

class Human {
    func sayHello() {
        print("Hello Human!")
    }
}

class Teacher: Human { }

let gywns: Human = Teacher()
gywns.sayHello()               // Hello Teacher!

위 예제는 오버라이딩 안 했음 → Human의 sayHello()가 호출되는게 명확함

그러면 Dynamic Dispatch가 아닌 Static Dispatch일까 ?

→ No, 오버라이딩의 가능성이 있으므로 무조건 vTable을 확인해서 참조함

각 클래스마다 가지고 있는 vTable에 함수 포인터를 저장해두고,

실제 런타임 시점에 이 vTable을 사용하여 어떤 메소드가 불릴지를 결정됨

class Human {
    func sayHello() {
        print("Hello Human!")
    }
}

class Teacher: Human {
   override func sayHello() {
        print("Hello Teacher!")
    }
}

let gywns: Human = Teacher()
gywns.sayHello()               // Hello Teacher!

런타임 시점에 Teacher이란 클래스의 vTable을 탐색하여

실제 불릴 sayHello의 함수 포인터를 찾아 실행시킴

그럼 Dynamic Dispatch 왜 쓰나요 ?

→ 객체지향의 오버라이딩을 위해서 해당 Dispatch는 필수

→ 상속 안 할 건데 Reference Type(클래스)라는 이유로 Dynamic Dispatch 쓰는게 싫다 ?

그러면 final을 통해 상속 관계를 없앰을 명시해줘서 성능 향상시키는 것

상속이 없는 단일 클래스도 Final을 안 쓰면 동적 디스패치가 동작

Value Type(구조체/열거형)에서의 Dispatch 동작

Value Type인 구조체, 열거형은 상속을 할 수 없음
-> 오버라이딩 가능성이 없으므로 Static Dispatch 사용

Reference Type인 클래스는 오버라이딩 개념에 의해 Dynamic Dispatch였음

그러나 Value Type은 상속이 안되므로 Static Dispatch가 가능

struct Human {
        func sayHello() {
                print("Hello Human")
        }
}

Human의 인스턴스의 sayHello 메소드는 상속이 없으니

무조건 확정으로 Human의 sayHello가 호출된다

그러므로 런타임에 vTable 추적할 필요 없이 컴파일 타임에 결정남

Protocol에서의 Dispatch

프로토콜은 기본적으로 메소드의 선언부만 제공
실제 사용할 때 프로토콜 타입을 참조로만 사용할 경우,
해당 인스턴스 타입에 맞는 메소드를 호출해야 해서 Dynamic Dispatch를 사용

protocol Human {
    func description()
}

struct Teacher: Human {
    func description() {
        print("I'm a teacher")
    }
}

struct Student: Human {
    func description() {
        print("I'm a student")
    }
}

위 코드는 Human 프로토콜을 채택한 Teacher, Student가 있음

let teacher: Teacher = .init()
teacher.description()           // I'm a teacher

let student: Student = .init()
student.description()           // I'm a student

위처럼 쓰면 Human 프로토콜을 채택한 각각의 구조체에 한해서

프로토콜의 프로퍼티가 호출되므로 Static Dispatch로 작동

그러나 프로토콜 타입을 사용한다면 ?

var human: Human = Teacher()
human.description()             // I'm a teacher

human = Student()
human.description()             // I'm a student

프로토콜 타입으로 해당 인스턴스 타입에 맞는 메소드를 확인해서 호출하므로

Static Dispatch처럼 description이 호출되는 타입을 지정해둘 수 없기에

Dynamic Dispatch 사용

위 3가지 동작을 Extension 했을 때 Dispatch 동작

Reference Type에서 Extension의 Dispatch 동작

클래스에서 확장했을 때 Dispatch 동작

class Human {
        func sayHello() {
                print("Hello Human")
        }
}

extension Human {
        func asd() {
                print("asd")
        }
}

class Teacher: Human {
        override func asd() {
                print("zxc")
        }
}

Class를 확장해서 메소드를 추가한 경우,

서브 클래스에서 오버라이딩이 안되므로 위 코드의 Teacher는 문법 상 에러

그러나, Human에 대한 인스턴스는 Static Dispatch로 동작함

let gywns: Human = .init()
gywns.sayHello()  ->  Dynamic Dispatch
gywns.asd()       ->  Static Disptach

Value Type에서 Extension의 Dispatch 동작

값 타입에서 확장했을 때 Dispatch 동작

상속(오버라이딩) 가능성이 전혀 없기에 확장을 해도 Static Dispatch로 동작

struct Human {
    func sayHello() {
        print("Hello Human!")
    }
}

extension Human {
    func asd() {
        print("asd")
    }
}

let gywns: Human = .init()
gywns.sayHello()           -> Static Dispatch
gywns.sayHo()              -> Static Dispatch

Protocol에서 Extension의 Dispatch 동작

Protocol에 선언만 되어 있는 메소드를

Extension으로 Default 메소드를 구현한 경우

protocol Human {
    func sayHello()
}

extension Human {
    func sayHello() {
        print("Hello Human!")
    }
}

프로토콜에 선언만 되어 있고, Extension에서 Default 메소드를 구현한 경우에 대한 코드

위처럼 작성 시, 프로토콜을 채택해도

해당 sayHello에 대한 메소드 구현이 필수적이지 않는다.

class Student: Human { }

class Teacher: Human {
    func sayHello() {
        print("Hello Teacher!")
    }
}
var gywns: Human = Student.init()
gywns.sayHello()       // Hello Human!

gywns = Teacher.init()
gywns.sayHello()       // Hello Teacher!

다음과 같이 프로토콜을 채택한 두 클래스가 있다면,

  • Student의 경우 Default 메소드가 호출
  • Teacher의 경우 직접 구현한 메소드가 호출

즉, 프로토콜의 메소드를 Extension으로 디폴트화 시켜뒀다면,

확정할 수 없기 때문에 Dynamic Dispatch가 동작하게 된다.

단, Virtual Method Table을 갖는 건 아니라고 함 !

(G선생이 말한 거라 확실치는 않을지도..)

Protocol에 선언되어 있지 않은 메소드를 추가로 구현한 경우

protocol Human { }

extension Human {
    func sayHello() {
        print("Hello Human!")
    }
}

프로토콜에서 처음에 선언하지 않은 메소드를 Extension에서 구현한다면 ?

class Student: Human { }

class Teacher: Human {
    func sayHello() {
        print("Hello Teacher!")
    }
}
var gywns: Human = Student.init()
gywns.sayHello()       // Hello Human!

gywns = Teacher.init()
gywns.sayHello()       // Hello Human!

Student 클래스는 sayHello() 호출 시, Human의 Extension에서 구현한 메소드가 호출

Teacher의 경우 본인 클래스에서 직접 sayHello를 구현했음에도 불구하고,

gywns 변수가 Human 프로토콜 타입이기에 extension의 sayHello()가 호출됨

즉, 프로토콜에 선언하지 않은 메소드를 Extension으로 구현한 경우,

본 프로토콜을 채택한 클래스에서 재구현 하더라도

본 프로토콜 타입의 변수에선 무조건 Extension으로 구현한 메소드가 호출됨

→ Static Dispatch

프로토콜 타입이 아닌 Teacher의 변수라면 정상 작동

var gywns2: Teacher = .init()
gywns2.sayHello()       // Hello Teacher!

배운 점

  • 두 가지 Dispatch에 대해 알게 됨
  • final을 사용했을 때의 얻을 수 있는 이점을 알게 됨
  • Reference Type, 클래스를 확장해서 메소드를 작성하면 (프로퍼티는 당연 X)
    자식 클래스에서 오버라이딩이 불가능하다..!
    → @objc 붙이면 옵씨의 특성을 빌려 가능
  • Extension 했을 때의 Dispatch 동작을 알게 됨
    외우기 보단, 많이 써보면서 어떤 메소드가 호출되는지 동작을 이해하면
    Static/Dynamic 특성으로 분류할 수 있을듯
  • 클래스/프로토콜/타입캐스팅 복습

참조 링크

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

Swift) Static Dispatch & Dynamic Dispatch (1/2)

Swift) Static Dispatch & Dynamic Dispatch (2/2)

Swift의 Dispatch 규칙