동적 디스패치 줄이기

디스패치(Dispatch)란 어떤 메서드를 호출할 것인가를 결정하여 그것을 실행하는 매커니즘을 뜻합니다.
디스패치의 종류에는 정적(Static) 디스패치동적(Dynamic) 디스패치가 있습니다.
정적 디스패치 방식은 어떤 메서드를 호출해야 하는지 컴파일 시점에 명확히 알 수 있는 방식이고, 동적 디스패치 방식은 어떤 메서드를 호출해야 하는지 런타임 시점에 알 수 있는 방식입니다.

class Person {
    func walk() {  }
}

class Student: Person {
    override func walk() {  }
}

class Programmer: Person {  }

let joo: Person = Person()
let miji: Student = Student()
let mai: Programmer = Programmer()

let people: [Person] = [joo, miji, mai]

for person in people { 
    person.walk() 
}

위의 코드에서 joo, miji, mai는 모두 Person입니다. Person이거나 Person을 상속받았기 때문이죠.
맨 아랫줄의 for 구문에서 [Person] Array에 속한 인스턴스 각각에게 walk() 메서드를 호출한다면, 컴파일타임에는 Personwalk()를 호출해야 할지, 오버라이드 된 Studentwalk()를 호출해야할지 결정할 수 없습니다. 런타임에서야 비로소 for 구문을 실행하면서 person 인스턴스가 진짜 어떤 타입인지 확인한 후에 적절한 walk()를 호출할 수 있기 때문입니다. 이렇게 런타임에 호출할 메서드를 결정하는 방식을 동적 디스패치라고 합니다. 런타임에 호출할 메서드를 결정해야 하기 때문에 실행속도에 불리합니다. 하지만 인스턴스의 다형성 구현을 위해서는 꼭 필요한 방식이기도 하죠.

struct Robot {
    func walk() {  }
}

let pica: Robot = Robot()
let chu: Robot = Robot()

for robot in [pica, chu] {
    robot.walk()
}

반면에 구조체는 상속기능이 없기 때문에 오버라이드 가능성이 없으므로, 항상 Robotwalk()를 호출해도 된다는 사실이 자명합니다. 그렇기 때문에 컴파일타임에 어떤 메서드를 호출할지 명확히 알 수 있습니다. 이렇게 컴파일타임에 어떤 메서드를 호출할지 결정하는 방식을 정적 디스패치라고 합니다. 컴파일타임에 호출할 메서드가 정해지고 컴파일러가 런타임 코드를 최적화 할 수 있기 때문에 런타임 성능이 더욱 뛰어납니다.

그렇기 때문에 런타임 성능을 최적화하려면 동적 디스패치를 줄이고, 정적 디스패치를 최대한 활용하는 것이 좋습니다.

1. 오버라이드가 필요하지 않은 요소에는 final을 꼭 명시하기

오버라이드 하지 않을 요소에 final을 명시하면 런타임에 호출할 메서드를 찾지 않아도 되기 때문에 정적 디스패치가 됩니다.

class Person {
    func walk() {  }
    final func run() {  }
}

class Student: Person {
    override func walk() {  }
    final func study() {  }
}

class Programmer: Person {  }

let joo: Person = Person()
let miji: Student = Student()
let mai: Programmer = Programmer()

let people: [Person] = [joo, miji, mai]

for person in people {
    person.walk()  // 동적 디스패치
    person.run()   // 정적 디스패치
}

miji.study()  // 정적 디스패치

final class Dog {
    func sleep() {  }
    func eat() {  }
}

let puppy: Dog = Dog()
puppy.sleep()  // 정적 디스패치
puppy.eat()  // 정적 디스패치

2. 파일 외부에서 접근할 필요가 없는 요소에 privatefileprivate 사용하기

private 또는 fileprivate 키워드를 통해 파일 외부 또는 자식 클래스로 구현부 요소가 드러나지 않으면 오버라이드의 가능성을 배제하기 떄문에 컴파일러가 정적 디스패치할 수 있습니다. private 또는 fileprivate를 붙여 선언하면 컴파일러가 알아서 final 키워드를 붙여주기 때문에 정적 디스패치가 가능합니다.

private class Cook {
    func cook() {  }
}

class Magician {
    fileprivate func magic() {  }
}

let cook: Cook = Cook()
let magician: Magician = Magician()

cook.cook()  // 정적 디스패치
magician.magic()  // 정적 디스패치

위 코드에서는 CookMagician의 각각의 메서드에 오버라이드 구현이 없으므로 정적 디스패치로 동작합니다.

3. WMO를 사용하는 경우, 외부 모듈에서 접근할 필요가 없는 요소는 internal 사용하기

WMO를 사용하면 컴파일러가 모듈의 소스코드를 한 번에 컴파일합니다(WMO를 잘 모르신다면 앞의 WMO 레슨을 참고해주세요). 그렇기 때문에 개별 요소가 어디에 쓰이는지 모듈 전체에서 파악할 수 있고 그를 통해 최적화 할 수 있는 가능성이 높아집니다. 개별 요소를 internal 선언을 하면 모듈 밖으로 드러나지 않으므로, 컴파일러는 모듈 밖에서 오버라이드 가능성이 없는 요소로 판단하고 final로 최적화 할 수 있습니다. 최적화가 이루어지면 정적 디스패치를 할 수 있죠.

스위프트 언어의 기본 접근수준(access level)은 internal이기 때문에 사실 크게 신경쓸 부분은 아닙니다. 런타임 성능을 최적화하고 싶다면 WMO를 활성화하는 것이 좋겠습니다.