(Swift) 프로토콜 그 자체가 자신을 따르지 않는다

Swift 5.1에 추가된 some 키워드 (불투명 리턴 타입)에 관한 Swift 공식 문서를 살펴보다가 이상한 구절을 발견했다.

Another problem with this approach is that the shape transformations don’t nest. The result of flipping a triangle is a value of type Shape, and the protoFlip(_:) function takes an argument of some type that conforms to the Shape protocol. However, a value of a protocol type doesn’t conform to that protocol; the value returned by protoFlip(_:) doesn’t conform to Shape. This means code like protoFlip(protoFlip(smallTriange)) that applies multiple transformations is invalid because the flipped shape isn’t a valid argument to protoFlip(_:).

프로토콜 타입의 값이 그 프로토콜을 따르지 않는다는 것이다. 왱? 이게 뭔말이지? 특정한 프로토콜을 따르는 객체들은 그 실제 타입에 상관없이 해당 프로토콜을 타입처럼 사용할 수 있다고 했는데, 이번에는 프로토콜 타입의 값이 그 프로토콜을 따르지 않는다라니?

(Swift) 프로토콜 그 자체가 자신을 따르지 않는다 더보기

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

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

  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 클래스와 타입들을 인식하지 못하기 때문이다. 다만, 제네릭을 사용해서 의존성을 활용하는 방법을 이렇게 바꿀수도 있다는 것은 참고삼아 알아두도록 하자.

required 가 붙은 이니셜라이저

UIView, UIViewController를 서브클래싱하는 코드를 작성하면서 init(frame:)을 오버라이딩하는 코드를 작성하면, Xcode는 매번 init?(coder:)가 정의되지 않았다는 컴파일 에러를 낸다. 일단 이 에러를 해결하려면 간단하게 부모 클래스의 이니셜라이저를 그대로 호출해주기만 하면 된다.

required override init?(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
}

왜 이 init?(coder:)는 자동으로 상속이 안되는 것일까?

잠깐, 이니셜라이저 관련한 프로그래밍 가이드에서는 required 이니셜라이저는 서브클래스에서 오버라이딩할 때 override를 붙이지 않는다고 했는데? Xcode는 override를 붙여야 한다고 한다. 이 부분은 재확인이 필요하다.

(https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html#//apple_ref/doc/uid/TP40014097-CH18-ID203)

required 가 붙은 이니셜라이저 더보기

(Swift) Array 완전정복 – 02. Sequence 프로토콜

Sequence

시퀀스(Sequence)는 직역하면 연속열이 될 수 있으며, 문자 그대로 개개의 원소들을 순서대로 하나씩 순회할 수 있는 타입을 의미한다. (Swift 기본 타입에 대해서는 사실상 모든 집합 타입이 이에 해당한다.) 시퀀스는 사실 Swift 문법과 밀접한 관련이 있는데, 바로 for - in 구문에 사용된다는 점이다.1 (Swift) Array 완전정복 – 02. Sequence 프로토콜 더보기

What Happened to NSMethodSignature?

NSInvocation에 대해 찾아보다가 Swift 공식 블로그에서 찾은 글을 간단히 번역해본다.

https://developer.apple.com/swift/blog/?id=19

What Happened to NSMethodSignature?

코코아 프레임워크를 Swift로 옮기는 것은 우리 스스로가 우리의 API를 새로운 관점에서 볼 수 있는 좋은 기회가 되었습니다. 우리는 Swift의 목표에 맞지 않는다고 생각되는 클래스들을 찾았고, 우리의 우선순위는 주로 안전성에 맞췄습니다. 예를 들어 동적인 메소드 호출(dynamic method invocateion)과 관련된 클래스들은 Swift에 반입되지 않습니다. 이러한 클래스에는 NSInvocationNSMethodSignature가 있지요.

우리는 최근에 이 클래스들이 빠져있음을 발견한 한 개발자로부터 버그 리포팅을 받았습니다. 이 개발자는 Objctive-C에서 메소드 인자들의 타입을 검사하는데 NSMethodSignature를 사용하고 있었고, Swift로 마이그레이션하는 과정에서 이 클래스를 사용할 수 없다는 것을 알았습니다. 실제 그 코드는 인자 타입이 정해지지 않은 HTTP 핸들러릘 받도록 되어 있었습니다. 예를 들면 이런 것들이죠. What Happened to NSMethodSignature? 더보기