우리는 흔히 UIKit를 이용하여 개발할 때 버튼이나 여러 요소들의 사용자 이벤트를 처리하기 위해 Target Action 디자인패턴의 원리를 이용해 다음과 같은 코드를 사용한다.
let button = UIButton()
button.addTarget(self, action: #selector(touchUpButton(_:)), event: .touchUpInside)
@objc func touchUpButton(_ sender: UIButton) {
//...
}
헌데 실제로 어떻게 프로그램의 런타임에서 action이 실행될 수 있는 것일까?
그 원리를 알아보고자 한다.
우선 Dispatch의 개념을 알아야 한다.
class Parent {
func someMethod() {
//...
}
}
class Child: Parent {
override func someMethod() {
// ...
}
}
let object: Parent = Child()
object.someMethod() // Parent의 someMethod를 호출할 것인가, Child의 someMethod를 호출할 것인가?
위 예제를 해결하는 방법은 두가지가 있다.
참고 링크에 따르면 Swift는 Dynamic Dispatch를 채택했다고 한다. 일반적으로는 Dynamic Dispatch가 편리하지만, 성능을 신경써야 하는 코드에서는 이 Dynamic Dispatch의 오버헤드에 신경써야 한다고 한다. 그래서 final, private 등의 접근 지정자를 Swift가 제공하는 이유는 컴파일러가 이를 통해 최적화가 가능해지기 때문이라고 한다.
Swift는 또다른 Dispatch 방법인 Message Dispatch 라는걸 지원한다고 한다. Message Dispatch는 Dynamic Dispatch의 종류 중 하나라고 한다. 이 때 Dynamic Dispatch는 Message Dispatch와 구분하기 위해 Table Dispatch라고도 한다.
TableDispatch
Message Dispatch
Message Dispatch는 자기 자신이 오버라이드 하거나 새로 정의한 메소드들만 테이블에 유지한다.
대신 부모 타입으로의 포인터를 가지고 있어서, 부모 타입의 메소드들은 부모 타입에서 찾아서 실행한다. 이러한 방식은 굉장히 유연해서, 아예 런타임에 메소드의 동작을 수정하는 것부터 새로운 메소드나 프로퍼티를 수정하는 등, 아예 클래스를 동적으로 만드는 것도 가능한다.
다만 스위프트가 이걸 직접적으로 제공하지는 않는다. Objective-C 런타임이 이걸 제공하는데 스위프트는 Objective-C 런타임을 사용하도록 지원한다. 즉 Message Dispatch를 사용하려면 Objective-C 런타임을 사용해야 한다.
Swift의 클래스는 Objective-C의 클래스에서 Message Dispatch 능력을 뺀 것이다.
따라서 원한다면 Objective-C 런타임과 연결해서 Message Dispatch 기능을 다시 붙일 수 있다.
Swift에서 Message Dispatch를 사용하기 위해서는 특정 멤버가 Objective-C의 런타임을 사용하겠다는 것을 명시적으로 알려줘야 한다. 이를 위해 다음과 같은 과정을 거칩니다.
@objc 어노테이션을 선언 앞에 추가하기. 이 어노테이션은 해당 요소가 Objective-C 런타임에 의해 접근 될 수 있게 합니다. 하지만 Objective-C 방식(#selector)등으로 접근하지 않는 경우는, 원래의 Dispatch가 적용.
dynamic 변경자(modifier)를 선언 앞에 추가한다. 이 변경자의 경우는 해당 요소가 Dynamic dispatch를 사용하도록 유도한다.
Swift 4 이전에는 dynamic을 쓰면 @objc 를 자동으로 추론해서 추가시켜 줬는데, 이 Proposal이 Swift4 이후에 적용되어 dynamic만 써서는 @objc를 자동으로 추가해주지 않는다.(단, @objc 어노테이션이 적용된 프로토콜의 메소드를 구현하거나 클래스 전체에 @objcMembers 어노테이션이 적용되어 있다면 자동 추론)
하지만 Message Dispatch가 강제되는 상황이 아니면 컴파일 오류를 내지 않는다. 하지만 이상태로 Objective-C의 기능(Selector, KVO 등)을 사용하면 런타임 에러가 발생한다고 한다~~~ 그렇군요...
Message Dispatch는 최대한 최적화되어 구현되어 있지만, 아무래도 런타임에 하는 일이 비교적 많다보니 많아지면 느릴 수 밖에 없다. 하지만 반드시 사용할 수 밖에 없는 경우가 있다....
extension에서의 override
class someClass {
@objc dynamic func action() -> Int { 1 }
}
class derived: someClass {}
extension derived {
override func action() -> Int { 2 } // OK
}
Selector, KVO 등의 기능을 사용할 때.
NSObject와 그 서브클래스들을 이용할 때.
마지막을 보고 스위프트도 완벽한 언어는 아니구나.... iOS가 Objective-C의 그늘에서 완전히 벗어난 건 아니구나 라는 생각이 들었다. 레거시 프레임워크로 인해... 언젠가 스위프트도 자체적으로 Message Dispatch가 되지 않을까아~~?
그리고 저번학기에 수강한 프로그래밍 언어론이 이렇게 도움될줄...ㅋㅋ 학교 수업 열심히 들어야겠다