제네릭타입을 활용한 델리게이트 패턴

델리게이트는 보통 특정한 이벤트처리를 위해서 코코아에서 가장 널리 사용되는 패턴 중 하나이다.  델리게이트를 구현하기 위해서는 보통 다음의 준비를 가져야 한다.

  1. 델리게이트 메소드를 호출하려는 객체(호스트라고 하자)는 델리게이트의 클래스가 무엇인지 알 필요가 없고, 사실 알 수도 없다. 따라서 델리게이트 메소드들은 별도의 프로토콜에 정의한다.
  2. 호스트는 프로토콜을 따르는 익명의 타입의 델리게이트 프로퍼티를 갖는다. 이 때 델리게이트는 존재할 수도, 존재하지 않을수도 있으며 메모리 관리상의 안전을 위해 약한 참조를 사용한다.

따라서 Swift에서는 이 내용을 코드로 표현하면 아래와 같이 나타낼 수 있다. 이 때 몇 가지 디테일에 주목해야 한다.

protocol CustomViewDelegate: class // # 1
{
  func customViewDidFinishEditing(_ view: UIVIew)
}

class MYCustomView: UIView {
  weak var delegate: CustomViewDelegate? ## 2
  
  @IBAction func finishEditing(_ sender: Any?) {
    delegate?.customViewDidFinishEditing(self)
    ...
  }
}
  1. 보통 델리게이트를 weak var 에 옵셔널 타입을 써서 선언하는데, 이렇게하면 델리게이트가 될 수 있는 객체의 타입은 반드시 클래스여야 한다. 값 시멘틱의 타입 객체는 weak 가 될 수 없다.
  2. 따라서 프로토콜 CustomViewDelegate는 오직 클래스 타입에서만 따를 수 있기 때문에 프로토콜 선언 시에 class를 붙여주어야 한다.

그리고 실제로 델리게이트를 사용하기 위해서는 델리게이트 객체를 생성해서 의존성을 주입해주어야 하는 절차를 반드시 거쳐야 한다. 코코아의 많은 내장 클래스에서는 델리게이트가 아웃렛으로 정의되어 있기 때문에 보통 우리는 이 과정을 인터페이스 빌더 내에서 연결하여 수행하게 된다.

조금 다른 스타일로 델리게이트 패턴 구현하기

Swift에서 이러한 델리게이트를 조금 다른 방식으로 구현할 수 있는 방법에 대해서 살펴보자. 일단 기존 방식의 문제는 “델리게이트가 되는 객체 인스턴스”가 필요하다는 점이 그 원인이 된다. 많은 경우에 델리게이트는 델리게이트 메소드의 구현을 제공하는 용도로 제작되며, 델리게이트 자체가 특정한 상태값을 저장하고 있는 경우는 많지 않다. (보통 호스트에 대한 참조를 필요로 하는 경우가 있지만, 이런 경우에는 델리게이트 메소드에서 호스트가 자신을 인자로 넘기는 방식으로 참조한다.) 따라서 프로토콜에서 델리게이트 메소드를 타입 메소드로 지정해버리는 것이다.

protocol CustomViewDelegate {
  static func customViewDidFinishEditing(_ view: CustomView)
}

이렇게 델리게이트 프로토콜을 정의했다. 이제 호스트에서 델리게이트는 인스턴스가 아니라 타입 그 자체가 될 것이기 때문에 다음과 같이 정의를 수정할 수 있다.  그리고 이 타입에 대한 의존성을 주입하기 위해서는 별도의 이니셜라이저를 아래와 같이 정의해야 한다.

class CustomView: UIView {
  var delegate: CustomViewDelegate.Type

  init(frame: CGRect, delegate: CustomViewDelegate.Type) {
    super.init(frame:frame)
    self.delegate = delegate
    ...
  }
  ...
}

뭔가 일이 더 꼬여버리는 느낌이다. 그런데 델리게이트가 타입 그 자체라고 하면, 이런식으로 프로퍼티로 설정하기 보다는 연관 타입으로 대체하는 것도 가능하지 않을까?

class CustomView<S: CustomViewDelegate>{
  typealias Delegate = S
  ...
}

이렇게하면 delegate라는 프로퍼티 없이, Delegate 연관 타입을 사용해서 간단하게 델리게이트 메소드를 호출할 수 있다. 예를 들어 다음과 같은 case를 가지지 않는 enum을 델리게이트 용으로 정의하고…

enum Foo: CustomViewDelegate {
  static func customViewDidFinishEditing(_ sender: CustomView) {
    print(sender.value)
  }
}

CustomView 클래스는 델리게이트 타입에 의존하는 제네릭이 되므로 다음과 같이 사용할 수 있다. 전체적으로 귀찮았던 부분들이 해소되는 것 같다.

let customView = CustomView<Foo>(frame: myRect)
...

한계

물론 프로토콜과 제네릭이 만능은 아니다. 이렇게 제네릭으로 변경한 패턴은 사실 코코아/코코아터치 앱을 사용할 때 적극적으로 사용할 수 없다. 애초에 인터페이스 빌더는 Objective-C 런타임 위에서 돌기 때문에 이렇게 정의한 Swift 클래스와 타입들을 인식하지 못하기 때문이다. 다만, 제네릭을 사용해서 의존성을 활용하는 방법을 이렇게 바꿀수도 있다는 것은 참고삼아 알아두도록 하자.