Class 상속을 공부하다가 상속은 어떻게 동작할까? 라는 급 의문이 생겨 야크쉐이빙 하게 되었고, 정리로 남겨야겠다라고 생각해 쓰게 되었습니다 허허
목차
- Dispatch
- Static Dispatch
- Dynamic Dispatch
- 타입 별 Dispatch의 차이
- Reference Type에서의 Dispatch
- Value Type에서의 Dispatch
- Protocol에서의 Dispatch
Dispatch
디스패치(Dispatch)라고 하면 GCD가 먼저 떠오르는데요.
이름은 같지만 오늘 알아볼 Dispatch는 어떤 메소드를 호출할 것인지를 결정하고 실행하는 메커니즘입니다.
Swift에선 Static Dispatch, Dynamic Dispatch 2가지 방식이 있습니다.
이러한 Dispatch는 내가 호출할 함수를 컴파일 타임에 결정할 것이냐, 런타임에 결정할 것이냐에 따른 방식입니다.
Static Dispatch (Direrct Call)
컴파일 타임에 호출될 함수를 결정하고, 런타임에 그대로 실행하기 때문에 Dynamic Dispatch에 비교해 성능상 이점을 가질 수 있습니다.
Dynamic Dispatch (Indirect Call)
런타임에 호출될 함수를 결정합니다.
Swift에서는 클래스마다 함수 포인터들의 배열인 vTable(Virtual Method Table)이 존재하는데,
하위 클래스가 메소드를 호출할 때, 이 vTable을 참조해 실제 호출할 함수를 찾아갑니다.
이 과정들이 런타임에 일어나기 때문에 Static Dispatch와 비교했을 때 오버헤드가 발생하게 됩니다.
타입 별 Dispatch의 차이
Swift에서 사용되는 대표적인 타입들이 있습니다.
ValueType, ReferenceType 그리고 Protocol 에서 어떤 Dispatch 방식이 사용되는지를 알아보겠습니다.
Reference Type에서의 Dispatch
Reference Type인 Class가 Struct와 가장 다른 부분이 뭘까요?
Class는 다형성을 위해 상속을 지원한다는 점입니다.
따라서, 서브 클래스에서 함수를 호출할 때 부모 클래스의 함수를 호출할 지 오버라이딩한 함수를 호출할 지 결정해야 하기 때문에 Dynamic Dispatch 방식을 사용하게 됩니다.
class Animal {
func speak() {
print("Animal")
}
}
class Cat: Animal { }
위와 같은 예시가 있습니다.
Animal 클래스를 만들고, Animal을 상속받아 Cat이라는 서브 클래스를 만들어 주었습니다.
let cat = Cat()
cat.speak() // Animal
Cat이 Animal 클래스를 상속받고 있긴 하지만, speak 메소드를 override하지 않았기 때문에 어떤 메소드를 호출해야할 지 문제가 되지 않습니다.
class Animal {
func speak() {
print("Animal")
}
}
class Cat: Animal {
override func speak() {
print("meow~")
}
}
그런데 만약 Cat클래스에서 speak 메소드를 override하면
let cat: Animal = Cat()
cat.speak() // meow~
cat 변수의 타입은 Animal이지만, Cat 인스턴스를 업캐스팅해서 가리키고 있기 때문에 Animal.speak() 을 참조하는 것이 아니라 Cat.speak()을 참조해야 합니다.
이처럼 컴파일러는 클래스의 메소드가 하위 클래스에서 override될 경우를 대비하여 상위 클래스의 speak()을 참조해야 하는지, 하위 클래스의 speak()을 참조해야하는지를 확인하는 작업을 런타임에 하게 됩니다.
speak() 라는 함수는 메모리의 코드영역에 탑재가 되어있을 것이고, 각 클래스마다 가지고 있는 vTable이란 곳안에 포인터로 각 클래스의 speak()의 주소를 가리키고 있게 됩니다.
class Animal {
func speak() {
print("Animal")
}
}
class Cat: Animal {
override func speak() {
print("meow~")
}
}
let animal: Animal = Animal()
let cat: Animal = Cat()
이 코드를 그림으로 표현하면 다음과 같습니다.
Animal과 Cat은 referenceType이므로 heap에 저장될 것이고, 각각 vTable의 주소를 포인터로 갖고 있을 것이고, vTable에선 실제 코드의 주소를 가리키고 있게 됩니다.
실제론 코드들이 Code Segment에 기계어로 번역되어 저장되어 있습니다.
만약 오버라이드를 하지 않았다면, 아래와 같이 Cat vTable에선 상위 클래스의 speak 함수를 가리킬 것입니다.
해당 부분은 소들님 블로그 를 보고 이해한 것으로 실제와 다를 수 있고, 틀린 부분이 있다면 댓글로 지적해주시면 감사하겠습니다.
class Animal {
func speak() {
print("Animal")
}
}
class Cat: Animal { }
let animal: Animal = Animal()
let cat: Animal = Cat()
만약 Cat 클래스에서 새로운 메소드를 추가하면 해당 메소드는 기계어로 번역되어 코드 영역에 추가될 것이고, 해당 메소드의 위치를 가리키는 포인터가 Cat vTable의 끝에 추가되게 됩니다.
아래 예시입니다.
class Animal {
func speak() {
print("Animal")
}
}
class Cat: Animal {
func punch() {
print("냥냥 펀치")
}
}
let animal: Animal = Animal()
let cat: Animal = Cat()
결론
이처럼, 런타임 과정에 해당 클래스의 vTable을 찾고, vTable에서 함수를 찾아 메모리 주소를 읽고 그 주소로 점프해야 하기 때문에 2개의 명령어가 추가로 필요하게 되어 성능상 손해를 보는 것입니다.
그렇다면 성능이 구린 Dynamic Dispatch는 왜 쓰지? 라는 생각이 들 수 있는데, OOP의 다형성을 만족하기 위해선 상속과 오버라이딩이 필요했고, 오버라이딩을 하기 위해선 Dynamic Dispatch가 필수적이었던 것입니다.
그러면 나는 상속을 할 필요가 없는데, class로 만들면 Dynamic Dispatch로 동작해 성능 손해를 보니까 Static Dispatch로 동작하는 Value Type의 Struct를 사용해야겠구나! 라고 생각이 도달했다면 오케이입니다!
Value Type에서의 Dispatch
Value Type인 구조체, 열거형은 상속을 할 수 없다는 특징 때문에 오버라이딩이 될 가능성이 없고,
따라서 Static Dispatch를 사용한다.
Reference Type은 상속과 오버라이딩 때문에 Dynamic Dispatch로 동작했지만, Value Type은 상속이 안되기 때문에 Static Dispatch가 가능합니다.
struct Tiger {
func speak() {
print("어흥")
}
}
Struct의 경우 어디서 speak() 함수를 호출해도, 늘 Tiger의 speak() 함수가 호출될 것이 보장되기 때문에 런타임에 추적할 필요가 없습니다.
따라서 컴파일 타임에 결정이 되어 Dynamic Dispatch에서 추가로 사용되던 명령어가 필요가 없기 때문에, 성능상 이점을 갖게 됩니다.
Protocol에서의 Dispatch
프로토콜은 기본적으로 메소드의 선언부만 제공하기 때문에,
실제 사용 시 프로토콜 타입을 참조로만 사용할 경우,
해당 인스턴스 타입에 맞는 메소드를 호출해야 해서 Dynamic Dispatch를 사용한다.
protocol Animal {
func speak()
}
struct Cat: Animal {
func speak() {
print("야옹~")
}
}
struct Tiger: Animal {
func speak() {
print("어흥~")
}
}
let cat: Cat = .init()
cat.speak() // 야옹~
let tiger: Tiger = .init()
tiger.speak() // 어흥~
이런 코드가 있을 때, Cat 과 Tiger는 Animal 프로토콜을 준수하고 있고,
구조체는 상속이 안되니까 각각 Cat.speak()와 Tiger.speak() 가 호출될 것이 보장되어 Protocol 또한 Static Dispatch로 동작해도 될 것 같습니다?
하지만 Protocol은 Reference Type과 동일하게 Dynamic Dispatch를 사용한다고 합니다.
이유는 저도 잘 몰루?
아시는 분 있다면 댓글로 달아주시면 감사하겠습니다.
참고 자료
'부스트캠프' 카테고리의 다른 글
함수형 프로그래밍 with. Swift (0) | 2024.07.24 |
---|---|
Swift 정규표현식 (0) | 2024.07.23 |
네이버 부스트캠프 웹・모바일 9기 챌린지 과정 1주차 회고 (0) | 2024.07.19 |
메모리의 구조 (0) | 2024.07.18 |
토크나이저, 렉서, 파서 (Tokenizer, Lexer, Parser) (0) | 2024.07.17 |