[새싹 iOS] 8주차_Method Dispatch

임승섭·2023년 9월 6일
0

새싹 iOS

목록 보기
20/45

Method Dispatch


  • 하위 클래스에서 상위 클래스의 메서드나 프로퍼티를 오버라이드 할 때,
    프로그램에서 실제 호출할 함수가 어떤 것인지 결정하는 과정에서 사용되는 메커니즘
    • 어떤 연산을 실행해야 하는지
    • 어떤 메서드 구현이 사용되어야 하는지

Static Dispatch (Direct Call)

  • 컴파일 타임에 호출될 함수를 결정한다
  • 그대로 런타임에서 실행한다
  • 컴파일 타임에 결정나는 것으로 성능상 이점을 가진다
  • 모든 value type(구조체, 열거형)에 적용된다
    • 실행할 메서드가 명확하다

Dynamic Dispatch (Indirect call)

  • 런타임에 호출될 함수를 결정한다
  • 런타임에 결정나는 것으로 성능상 손해를 보게 된다
  • 클래스마다 vTable(Virtual Method Table)을 갖는다
    • vTable : 함수 포인터들의 배열
  • 인스턴스마다 vTable을 참조하는 포인터가 하나씩 존재하고,
    어떤 인스턴스에서 메서드가 호출되면,
    런타임에 vTable에서 해당 메서드를 구현하고 있는 코드를 찾아 실행한다
  • 요약 (클래스 인스턴스의 메서드 실행)
    1. 해당 인스턴스의 메타 타입 얻는다
    2. 그 타입의 vTable에서 호출하고자 하는 메서드 주소 찾는다
    3. 해당 주소 찾아가서 메서드 실행한다
  • 런타임에 vTable에서 함수 찾아서 주소 읽고 찾아가야 하기 때문에 성능상 손해
  • 클래스는 따로 최적화를 해주지 않으면 Dynamic Dispatch로 동작한다
    • 상속의 여지가 있어서
      동일한 인터페이스에 대한 메서드가 여러 개 정의될 수 있다 (override)
    • 실제로 오버라이딩이 되었는지 여부에 상관없이
      무조건 vTable을 확인한다


Dispatch 종류


1. value type

  • struct, enum : 상속 x. 오버라이딩 가능성 x
    • Static Dispatch 사용

2. reference type

  • class : 상속의 가능성. 오버라이딩의 가능성
    • sub class에서 함수 호출할 수 있기 때문에 Dynamic Dispatch 사용
/* 오버라이딩 하지 않았을 때 */
class Person {
	func say() {
    	print("hi")
    }
}

class Noah: Person { }

let a: Person = Noah()
a.say()		// hi


/* 오버라이딩 했을 때 */
class Noah: Person {
	override func say() {
    	print("hi Noah")
    }
}

let b: Person = Noah()
b.say()		// hi Noah

3. protocol type

  • Dynamic Dispatch 사용
    • 기본적으로 메서드의 선언부만 제공하기 때문에
      런타임에 어떤 곳에서 채택하고 있는지 확인해야 한다
  • vTable -> Witness Table

프로토콜을 채택한 구조체

  • 클래스처럼 상속의 개념이 적용된다

  • 컴파일 타임에는 메서드의 선언만 알고,
    역시 런타임에 메서드의 정의를 찾아야 한다

  • 그 인스턴스에 맞는 메서드를 찾아서 호출해야 한다

    protocol Person {
        func say()
    }
    
    struct A: Person {
        func say() {
            print("a a a")
        }
    }
    struct B: Person {
        func say() {
            print("b b b")
        }
    }
    
    let a: Person = A()
    let b: Person = B()
    a.say()		// a a a
    b.say()		// b b b
  • PWT (Protocol Witness Table)
    • 프로토콜을 채택하는 각 구조체 인스턴스가 하나씩 가지고,
      메서드에 대한 실제 구현을 이 테이블에 연결해둔다


Extension


1. value type

  • 여전히 상속 가능성 없기 때문에 extension 해도 Static Dispatch

2. reference type

  • class를 확장해서 메서드를 추가하면,
    sub-class에서 오버라이딩이 불가능하다 (non-@objc)
  • 오버라이딩 자체가 불가능하기 때문에,
    extension에서 선언한 메서드가 불리는 게 보장된다

  • 즉, Static Dispatch

    class Person {
        func say() {
            print("hi")
        }
    }
    
    extension Person {
        func sayYes() {
            print("yes!")
        }
    }
    
    class Man: Person {
        override func sayYes() {	// Error!
        }
    }

3. Protocol

3 - 1. 기존 메서드의 default 메서드 구현

  • protocol에서 선언한 메서드에 대한 default 구현이 있으면,
    해당 프로토콜을 채택하는 클래스에서 필수로 메서드에 대한 구현을 할 필요가 없다
  • 구현하지 않은 경우
    • extension에 작성된 default 구현 메서드가 불린다
  • 직접 구현한 경우
    • 직접 구현한 메서드가 extension의 default보다 우선순위가 더 높기 때문에 직접 구현한 메서드가 불린다
  • 따라서 어느 한 타입에서만 불린다고 보장되지 않기 때문에 Dynamic Dispatch

    protocol Person {
        func say()
    }
    
    extension Person {
        func say() {
            print("this is default hi")
        }
    }
    
    class Man: Person { }
    class Woman: Person {
        func say() {
            print("this is woman hi")
        }
    }
    
    let a: Person = Man.init()
    let b: Person = Woman.init()
    
    a.say()		// this is default hi
    b.say()		// this is woman hi
    

3 - 2. 추가 메서드 구현

  • 인스턴스의 타입이 프로토콜일 때, 무조건 extension에서 구현한 메서드가 실행된다

  • 즉, 해당 클래스에서 아무리 같은 메서드를 작성했더라도 extension에서 구현한 메서드가 불리는 것이 보장되기 때문에 Static Dispatch이다

  • (주의) 변수의 타입이 클래스이면, 클래스에서 구현한 메서드가 실행된다

protocol Person { }

extension Person {
	func say() {
    	print("newly say")
    }
}

class Man: Person { }
class Woman: Person {
	func say() {
    	print("woman say")
    }
}

let a: Person = Man.init()
let b: Person = Woman.init()
let c: Woman = Woman.init()

a.say()		// newly say
b.say()		// newly say
c.say()		// woman say

0개의 댓글