Target Action
우리는 흔히 UIKit를 이용하여 개발할 때 버튼이나 여러 요소들의 사용자 이벤트를 처리하기 위해 Target Action 디자인패턴의 원리를 이용해 다음과 같은 코드를 사용한다.
let button = UIButton()
button.addTarget(self, action: #selector(touchUpButton(_:)), event: .touchUpInside)
@objc func touchUpButton(_ sender: UIButton) {
//...
}
헌데 실제로 어떻게 프로그램의 런타임에서 action이 실행될 수 있는 것일까? 그 원리를 알아보고자 한다.
Dispatch
우선 Dispatch의 개념을 알아야 한다.
- 많은 객체지향 언어들이 상속을 통해 메소드와 프로퍼티들을 오버라이드 할 수 있도록 한다. 오버라이드를 할 경우, 프로그램은 실제 호출할 함수가 어떤 것인지 결정하는 과정이 필요하다.
- Dispatch란 이때 어떤 함수를 호출할지 결정하는 것을 의미한다.
class Parent {
func someMethod() {
//...
}
}
class Child: Parent {
override func someMethod() {
// ...
}
}
let object: Parent = Child()
object.someMethod() // Parent의 someMethod를 호출할 것인가, Child의 someMethod를 호출할 것인가?
위 예제를 해결하는 방법은 두가지가 있다.
- Static Dispatch
- 변수의 명목상 타입에 맞추어 메서드를 호출한다. 이 경우 Parent의 SomeMethod()를 호출하게 된다.
- 컴파일 타임에 결정된다.
- 컴파일 타임에 결정되기 때문에 성능이 좋지만 자식 클래스의 요소 호출하고 싶으면 명시적인 타입 캐스팅으로 변수를 자식 타입으로 만들어줘야 한다. 따라서 다형성 활용이 어려워진다.
- Dynamic Dispatch
- 변수의 실제 타입의 맞춰서 메소드와 프로퍼티를 호출한다. 이 경우 Child의 SomeMethod()를 호출하게 된다.
- 런타임에 결정된다.
- 어떤 클래스가 들어와도 실제 타입에 맞는 요소를 참조하기 때문에 다형성 활용에 유리하다.
- 런타임에 실제 참조할 요소를 찾는 과정이 있기 때문에 Static Dispatch보다 성능상에서 손해를 보게 된다.
- 이 과정은 O(1)의 시간복잡도를 가지도록 구현되어 있다는데... 어떻게 확인하지? ㅋㅋ
참고 링크에 따르면 Swift는 Dynamic Dispatch를 채택했다고 한다. 일반적으로는 Dynamic Dispatch가 편리하지만, 성능을 신경써야 하는 코드에서는 이 Dynamic Dispatch의 오버헤드에 신경써야 한다고 한다. 그래서 final, private 등의 접근 지정자를 Swift가 제공하는 이유는 컴파일러가 이를 통해 최적화가 가능해지기 때문이라고 한다.
그래서 Target Action의 원리가 뭐냐? 바로 Message Dispatch
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 런타임을 사용해야 한다.
그렇다면 어떻게 Message Dispatch를 사용할 수 있을까?
- 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는 최대한 최적화되어 구현되어 있지만, 아무래도 런타임에 하는 일이 비교적 많다보니 많아지면 느릴 수 밖에 없다. 하지만 반드시 사용할 수 밖에 없는 경우가 있다....
그럼 언제 Message Dispatch를 사용해야 하나요?
- extension에서의 override
- 기본적으로 Swift는 extension에서의 함수 오버라이드를 금지하고 있다.
class someClass {
@objc dynamic func action() -> Int { 1 }
}
class derived: someClass {}
extension derived {
override func action() -> Int { 2 } // OK
}
- 이렇게 하면 가능하다. 반대로 extension에서 처음 선언한 메소드를 오버라이드 가능하게 하기 위해서는 반드시 extension에서 @objc dynamic을 붙인다.
- Selector, KVO 등의 기능을 사용할 때.
- 이러한 기능들은 Objective-C 런타임을 통해서만 제공되는 기능이기 때문에 어쩔 수 없다.
- NSObject와 그 서브클래스들을 이용할 때.
- NSObject 자체가 뭔가 특별한 건 아니지만 그 근본적인 특성상 NSObject 자신과 그 구현이 대부분 Objective-C 런타임에 의존하고 있다.
- 이는 코드를 한꺼번에 수정하지 않고도 Swift에서 기존 프레임워크를 이용할 수 있도록 하는 장점이 있지만, 최적화 문제와 Swift로 처음 진입한 개발자에게는 혼란과 진입 장벽이 되기도 한다고 한다.
참고링크: https://jcsoohwancho.github.io/2019-10-11-Dynamic-Dispatch와-성능-최적화/
Uploaded by N2T