Static Dispatch, Dynamic Dispatch (2/2)

2025.02.28 - [◽️ Programming/iOS] - Static Dispatch & Dynamic Dispatch (1/2)

 

Static Dispatch & Dynamic Dispatch (1/2)

오늘은 Static Dispatch & Dynamic Dispatch의 대해 개념적으로 이해를 하고 넘어가보자 이번 면접을 통해 해당 개념을 질문 받았는데 명칭이 생소하다고 느껴서인지 어느정도 개념은 알고 있었지만 제대

dongdida.tistory.com

 

그럼 오늘은 스태틱 디스페치와 다이나믹 디스페치의 두번째 알아보는 시간!! 이전에 내용을 참고하고 추가적으로 해야할 내용이 있는 것 같아서 또 쓰게 됐다

 

이 내용을 쓰기전에 먼저 Overloading과 Overriding의 차이에 대해서 제대로 명확하게 짚고 넘어가도록 하자

 

Overloading VS Overriding

먼저 오버로딩과 오버라이딩의 차이를 제대로 알고 넘어가자! 먼저 오버로딩은 같은 이림의 함수나 메서드를 매개변수의 종류, 개수, 순서 등을 달리해서 여러개 정의할 수 있는 것을 말한다.

 

한 클래스나 모듈 내에서 이름은 같지만 서로 다른 파라미터를 가진 메서드가 존재한다는 점!

 

호출할 함수는 컴파일 시점에 매개변수 타입과 개수를 기반으로 결정되고 함수 이름은 같지만, 전달하는 인자에 따라 서로 다른 함수가 호출된다!

func printValue(value: Int) {
  print("정수: \\(value)")
}

func printValue(value: String) {
  print("문자열: \\(value)")
}

printValue(value: 42)      // "정수: 42"
printValue(value: "Hello") // "문자열: Hello"

오버라이딩은 상속 관계에서 상위 클래스의 메서드를 하위 클래스에서 재정의하는 것을 말한다! 하위 클래스는 상위 클래스가 가지고 있는 메서드를 자신에게 맞게 변경할 수 있다는 특징!!

 

호출할 메서드는 실제 인스턴스의 타입에 따라 결정되므로 런타임 시점에 동적 디스패치를 통해 올바른 메서드가 실행된다!

class Animal {
    func sound() {
        print("동물의 소리")
    }
}

class Dog: Animal {
    override func sound() {
        print("멍멍")
    }
}

let animal: Animal = Dog()
animal.sound() // "멍멍" → 런타임에 Dog 클래스의 sound()가 호출됨

 

오버로딩과 오버라이딩의 주요 차이점

 


이 내용을 바탕으로 이제 다시 스태틱, 다이나믹 디스패치에 대해서 알아보자

 

Value Type에서의 Extension

구조체와 열거형은 상속이 불가능하므로, extension을 통해 추가한 메서드도 오버라이딩의 가능성이 없다!! 따라서 extension으로 추가된 메서드는 컴파일 타임에 결정되는 정적 디스패치로 동작한다.

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

extension Human {
    func sayHo() {
        print("Ho~~")
    }
}

let sodeul: Human = Human()
sodeul.sayHello()  // Static Dispatch
sodeul.sayHo()     // Static Dispatch

위의 예시처럼 구조체의 확장은 언제나 정적으로 호출된다!

 

Reference Type 에서의 Extension

클래스는 상속과 오버라이딩이 가능하기 때문에 클래스 본체에 선언된 메서드는 동적 디스페치를 사용한다. 하지만, extension을 통해 추가된 메서드는 일반적으로 non-@objc인 경우 오버라이딩이 불가능하므로, 정적 디스패치로 처리된다.

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

extension Human {
    func sayHo() {
        print("Ho~~")
    }
}

class Teacher: Human {
    // 아래 코드는 non-@objc 메서드인 sayHo()의 오버라이딩을 허용하지 않습니다.
    // override func sayHo() { } // 에러 발생
}

let sodeul: Human = Human()
sodeul.sayHello()  // Dynamic Dispatch (클래스 내부 구현)
sodeul.sayHo()     // Static Dispatch (extension으로 추가된 메서드)

@objc를 사용한 경우에는 Objective-C 런타임의 다이나믹 디스페치 메커니즘을 사용할 수 있으므로 오버라이딩이 가능해진다.

하지만 대부분의 Swift 개발에서는 이제 @objc를 사용하지 않도록 하는 방향인 것 같다

 

Protocol 에서의 Extension

프로토콜에 extension을 적용하는 경우, 두 가지 중요한 상황이 존재한다.

 

먼저 첫번째로 프로토콜 요구사항에 대한 Default 구현에 대한 내용인데

프로토콜 요구사항으로 선언된 메서드에 대해 extension에서 기본 구현을 제공할 수 있다.

 

동적 디스패치가 발생하는 경우는 다음과 같다

  1. 만약 프로토콜을 채택한 타입이 해당 메서드를 자체적으로 구현했다면, 그 구현이 우선순위를 갖게 된다.
  2. 변수의 타입이 프로토콜인 경우, 호출 시 런타임에 구체적인 타입의 구현이 결정되므로 다이나믹 디스패치가 발생한다.
protocol Human {
    func sayHello()
}

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

class Student: Human { } // default 구현 사용

class Teacher: Human {
    func sayHello() { // 직접 구현 → 오버라이딩
        print("Hello Teacher!")
    }
}

var sodeul: Human = Student()
sodeul.sayHello() // "Hello Human!" (default 구현, 하지만 동적 디스패치)

sodeul = Teacher()
sodeul.sayHello() // "Hello Teacher!" (Teacher의 구현이 호출됨, 동적 디스패치)

 

두번째로 프로토콜에 선언되어 있지 않은 메서드를 Extension으로 추가한 경우엔

  1. 프로토콜에 선언되지 않은 메서드는 extension에서만 정의된 것이므로, 해당 메서드는 프로토콜의 witness table에 등록되지 않는다.
  2. 결과적으로 프로토콜 타입으로 선언된 변수에서 호출할 경우에는 스태택 디스패치가 발생한다.
  3. 즉, 타입이 프로토콜로 선언되면 extension에 추가된 메서드가 무조건 호출되며, 채택한 클래스에서 동일 이름의 메서드를 구현해도 프로토콜 타입으로 호출 시에는 extension구현이 우선시 된다.
protocol Human { }

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

class Student: Human { }

class Teacher: Human {
    func sayHello() { // Teacher 내부 구현
        print("Hello Teacher!")
    }
}

var sodeul: Human = Student()
sodeul.sayHello() // "Hello Human!" → extension의 메서드가 호출됨

sodeul = Teacher()
sodeul.sayHello() // "Hello Human!" → Teacher의 구현은 무시되고, extension 구현이 호출됨

// 단, 아래처럼 타입을 Teacher로 명시하면, Teacher의 구현이 호출됨.
let teacher: Teacher = Teacher()
teacher.sayHello() // "Hello Teacher!"

프로토콜 변수로 메서드를 호출할 경우, extension에 추가된 메서드는 정적 디스패치로 호출된다. 구체 타입 변수로 호출할 경우에는 해당 타입 내에 직접 구현된 메서드가 호출된다.

 


정리하자면 다음과 같다!!!

 

Value Type의 Extension

상속이 불가능하므로, extension 메서드는 항상 스태틱 디스패치로 동작한다.

 

Reference Type의 Extension

클래스 내부의 메서드는 Dynamic Dispatch를 사용하지만, extension으로 추가된 non-@objc 메서드는 오버라이딩이 불가능하여 Static Dispatch가 된다.

 

Protocol의 Extension

프로토콜 요구사항에 대한 Default 구현의 경우, 채택한 타입이 직접 구현하면 동적 디스패치가 발생한다.

 

프로토콜에 선언되지 않은 메서드를 extension으로 추가하면 프로토콜 타입 변수에서는 정적 디스패치가 발생하며, 채택한 클래스 내의 동일 이름 메서드 구현은 무시된다!

 

오늘 이렇게 Static Dispatch와 Dynamic Dispatch에 대해서 알아봤는데 아직 좀 어려운 개념인 것 같다.. 반복 숙지를 통해 꼭 알고 넘어가도록 하자!!Static Dispatch, Dynamic Dispatch (2)