델리게이트 패턴에서 제네릭으로 – Swift

(제목이 스포일러이긴한데…) 간단한 클래스를 하나 작성해보자. 0으로 시작하는 값에서 메소드를 하나를 호출하면 그 값을 1씩 증가시켜나가는 것이다.

class Counter {
  var value: Int = 0
  func increase() {
    value += 1
    print("value: \(value)")
  }
}

현실적으로는 별 쓸 데 없는 이 클래스를 사용하려 할 때, 값이 변할 때 수행하는 동작을 입맛에 맞게 커스터마이징하고 싶은 경우가 있을 수 있다. 물론 increase() 메소드를 그 때 그 때마다 변경하면 되지만, 소스를 직접 수정할 수 없는 서드파티가 이 클래스를 사용한다면, 델리게이트를 만들어서 값이 변하는 이벤트의 처리를 맡길 수 있다. 예를 들어 변경된 새 값이 짝수인 경우에만 출력한다던가, 화면 출력이 아닌 파일로 저장한다던가 혹은 네트워크를 통해 서버에 값을 업로드하려고 할 수도 있을텐데, 델리게이트가 해당 동작을 수행하도록 한다면 우리는 델리게이트가 지정된 메소드를 호출하게끔만 해주면 될 일이다.

델리게이트를 구현하기 위해서는 보통 델리게이트에 필요한 프로토콜을 정의하고, 해당 프로토콜을 따르는 델리게이트 클래스를 작성해야 한다. 예를 들어 이런 식이다.

class Counter {
  weak var delegate: CounterDelegate?
  func increase() {
    value += 1
    delegate?.valueDidChange(value)
  }
}

protocol CounterDelegate {
  func valueDidChange(_ value: Int)
}

class Foo: CunterDelegate {
  func valueDidChange(_ value: Int) {
    if value % 2 == 0 { print("value: \(value)") }
  }
}

물론, 위 코드는 정상적으로 컴파일 될 수 없다. Swift에서 weak로 정의되는 프로퍼티는 반드시 class 타입이어야 한다. 따라서 프로토콜 CounterDelegateclass를 상속받아 정의되어야 할 것이다.

protocol CounterDelegate: class { ...

타입 메소드로 변경하기

많은 경우 델리게이트 패턴에서, 델리게이트 그 자체는 특정한 상태값을 유지할 필요가 없는 경우가 많다. 대신 델리게이트는 델리게이트 메소드를 제공하는 호스트의 역할을 담당하기 때문에 인스턴스로 만들어지는 경우가 많다. 만약, 델리게이트가 특정한 상태값을 유지하고 있을 필요가 없다면 델리게이트 메소드가 반드시 인스턴스 메소드여야 할 이유는 없을 것이다. 그렇다면 다음과 같이 델리게이트 구현을 바꾸는 것은 어떨까?

protocol CounterDelegate {
  static func valueDidChange(_ value: Int)
}

이 프로토콜은 클래스가 아닌 struct나 enum 타입들도 따를 수 있게 된다. 특히 인스턴스를 만들 필요가 없으니 enum 으로 다음과 같이 만들어볼 수 있겠다.

enum Foo: CounterDelegate {
  static func valueDidChange(_ value: Int) {
    if value % 2 == 0 {
      print("value: \(value)")
    }
  }
}

그렇다면 Counter는 어떻게 수정되어야 할까? delegate 프로퍼티는 특정한 타입의 객체가 아닌, 타입 그 자체를 가리켜야 하고, 따라서 delegate의 타입은 CounterDelegate가 아닌 CounterDelegate.Type을 사용하게 된다.

class Counter {
  var delegate: CounterDelegate.Type?
  var value: Int = 0 
  func increase() {
    value += 1
    delegate?.valueDidChange(value)
  }
}

다시 제네릭으로 옮겨가기

그렇게 따지고보면 delegate는 Counter 클래스의 연관 타입(associated type)으로 볼 수 있다. 그렇다면 굳이 프로퍼티로 정의할 필요 없이, Counter 자체가 제네릭 타입이면 되는 것이다. 즉 이를 테면 Delegate라는 연관 타입을 가지는데, 이 연관 타입은 CounterDelegate 프로토콜을 따라야 한다. 이는 클래스 선언 부분에서 제네릭에 대한 where 절을 통해서  델리게이트 타입이 해당 프로토콜을 따르는 것을 명시하면 된다.

class Counter<Delegate> where Delegate: CounterDelegate {
  var value: Int = 0 
  func increase() {
    value += 1
    Delegate.valueDidChange(value)
  }
}

이렇게 되면 델리게이트에 대한 프로토콜과, 그 프로토콜을 따르는 타입만이 존재한다. 델리게이트의 인스턴스는 필요하지 않으며, 오직 Counter를 생성할 때 연관되는 델리게이트의 타입만을 명시하면 된다. 이렇게 의존성을 주입하는 코드가 간결해지고, 다음과 같이 사용될 수 있다.

do {
  let c = Counter<Foo>()
  for _ in (0...20) {
    c.increase()
  }
}

한계

여기서 보인 예시는 델리게이트를 다른 객체 인스턴스가 아닌 타입 자체로 옮기면서 연관 타입이 위임의 대상이 되는 것을 통해서 제네릭으로 델리게이트 구현을 바꿀 수 있다는 개념을 보인 것이다. 즉 “이런게 된다”는 것이지 이것이 기존의 방법보다 반드시 좋다고는 볼 수 없다. 특히 현재 Swift 및 Cocoa의 기능에서는 몇 가지 한계가 있는데 우선 델리게이트 관계는 통상 인터페이스 빌더에서 많이 연결된다. 인터페이스 빌더에서 연결된다는 의미는 결국 Objective-C 런타임을 통해서 구성된다는 것이고, Objective-C에서는 제네릭을 지원하지 않기 때문에 IB에서는 제네릭을 통한 위임을 사용할 수 없다. 또한 현재의 Swift 타입 시스템은 concrete type만을 타입으로 인식한다. 즉 Counter<Foo>는 어떤 타입으로 특정됨에 비해서 타입 파라미터가 생략된 Counter는 아직까지는 그 자체로는 “타입을 만들기 위한 틀”, 즉 메타 타입이며 이는 타입으로 취급되지 않는다. (이 부분은 향후 Swift의 제네릭 시스템에서 higher-kinded 타입을 지원하게 되면 해결될 수 있을 것이다.) 따라서 실제로는 그럴 필요가 전혀 없겠지만, 델리게이트 타입이 Counter 스스로가 되도록 정의할 수 있는 직접적인 방법은 없다.

 

프로퍼티 리스트 타입

프로퍼티 리스트는 코코아의 표준 데이터 타입 클래스들을 직렬화/역직렬화하는데 사용되는 구조화된 데이터 포맷이다. 프로퍼티 리스트는 .plist 라는 확장자의 파일로 기록되어 코코아 앱에서 광범위하게 쓰인다. 특히 모든 코코아/코코아터치 앱의 번들에는 기본적으로 Info.plist라는 파일이 있고, 이 파일은 앱의 기본적인 론칭에 필요한 필수적인 정보들(메인 nib파일 및 스토리보드 경로등)이 지정되어 있다.

프로퍼티 리스트는 구조화된 트리이며, 파일로 저장될 때에는 XML의 형식이나 바이너리 파일로 저장될 수 있다. 인터페이스 빌더에서 작성한 UI 구성 정보는 모두 프로퍼티 리스트로 만들어진다. 이 정보는 XML 형식을 써서 .xib 포맷으로 기록되었다가, 프로젝트를 빌드하는 시점에 컴파일되어 .nib파일로 변환된다.

프로퍼티 리스트 타입이란, 프로퍼티 리스트에 “들어갈 수 있는” 데이터 타입을 말한다. 보통 클립보드에 복사하기 위한 pasteboardPropertyList(ofType:)의 리턴값들은 프로퍼티 리스트에 들어갈 수 있는 다음의 타입중 하나여야 한다.

추상타입 XML 요소 코코아 클래스
array <array> NSArray
dictionary <dictionary> NSDictionary
string <string> NSString
data <data> NSData
date <date> NSDate
integer <integer> NSNumber
floating <real> NSNumber
Boolean <true/> , <false/> NSNumber

프로퍼티 리스트 타입에 속하는 클래스들은 보통 프로퍼티 리스트로부터 바로 객체화될 수 있고, 프로퍼티 리스트로는 별다른 변환없이 그대로 넘겨져서 시스템에 의해 자동으로 직렬화된다. 예를 들어서 NSArray<NSString *> * 타입의 배열이 있다면 이 배열의 전체 그래프는 하나의 프로퍼티 리스트 객체인 셈이다. 이 데이터를 만약 어딘가 저장하거나 할 때에는 여러 방법을 사용할 수 있다.

  1. 이들 클래스는 모두 NSCoding을 따르기 때문에 NSKeyedArchiver를 사용해서 이진 데이털 직렬화할 수 있고, 이 데이터를 그대로 기록할 수 있다.
  2. 프로퍼티 리스트 타입을 위한 NSPropertyListSerialization 클래스가 있다. +dataWithPropertyList:format:options:error:을 사용하여 직렬화할 수 있다.
  3. 위의 말은 곧 NSArray는 곧바로 파일에 기록될 수 있다는 것을 의미한다. 모든 프로퍼티 리스트 타입의 객체들은 -writeToURL:atomically:를 쓸 수 있다. (단, 이 메소드는 Xcode9 기준으로 deprecated되었다.)

3번을 생각한다면 프로퍼티 리스트 XML 요소를 사용해서 잘 포맷팅된 XML 파일이 있다면, 이로부터 NSArray, NSDictionary를 바로 읽어들이는 것도 가능하다는 이야기이다. (-initWithContentsOfURL:이 있다.)

다만 현재 Xcode 9 버전을 기준으로 NSArray, NSDictionary를 다른 클래스 도움 없이 파일에 쓰고, 읽는 메소드들은 deprecated된 상태이다. (물론 다른 대안들이 존재하긴 하지만) 이렇게 간편한 방법에 대한 새로운 API가 아직까지는 정식으로 공개되지는 않은 상태이다.

 

이중옵셔널에 대하여 – Swift

Swift의 캐스팅 연산자인 as? 는 T 타입으로 캐스팅에 실행할 가능성을 내포하기 때문에 T? 타입을 만들게 된다. 이 때 변환하려는 값이 이미 옵셔널타입인 경우에는 nil이 아니면 T 타입으로 간주하여 최종 결과는 T??가 아닌 T?가 된다.

var a: Any? = 8
if let x = a as? Int { 
// a는 Any?이면서 그 값이 Int 형으로 변환될 수 있으므로 
// Int 형으로 변환된 후에 옵셔널로 마크되어 Int? 타입이 된다.
// 다시 이 값은 let x = 에 의해서 언래핑되고 if 체크를 통과한다. 
  print(x)
}

// -> 8이 출력됨

위 예에서 aAny? 타입인데, as? 를 통해서 Int로 변환하려고 한다. a의 값은 8이므로 Int로 변환이 가능하니 그 명시적인 타입이  Any, Any?이든 상관없이 그 캐스팅의 결과는 그 타입이 Int? 가 되어 언래핑된 결과는 Int 타입으로 표시된다.

그런데 다음의 경우는 조금 이상하다. try? 를 통한 예외를 던질 수 있는 함수의 호출은 그 결과가 옵셔널로 변환된다. 따라서 Any?를 다시 as? Int로 적용하려 하는데 그 결과는 Int가 아닌 Int?가 된다.

enum SomeError: Error {
 case error
}

func divide(x:Int, y:Int) throws -> Any {
  guard y > 0 else { throw SomeError.error }
  return x / y
}

if let x = try? divide(x:8, y:4) as? Int {
// 예상 : try? divide(x:8, y:4) -> (2 : Any?)
// (2 : Any?) as? Int -> (2: Int?)
// 하지만 실제로는 Int?? 타입이며
// if 문 내로 들어갔을 때 x는 Optional(2)가 된다.
  print(x)
}
// -> Optional(2)

이건 사실 위 표현식에서 try?의 우선순위가 as? 보다 낮기 때문에 divide()가 실행된 결과가 Any 타입에서 Int?로 바뀌고 이것이 try? 때문에 Int?? 타입이 된 것이다. (이건 좀 이상하다. 단일 값에 대해서 try? 를 적용하게 된다는 의미가 아니던가) 이 문제는 사실 다음과 같이 괄호로 우선순위 문제를 정리해주면 해결된다.

if let x = (try? divide(x:8, y:4)) as? Int {
  print(x) // 2
}

만약 이렇게 이중으로 옵셔널이 만들어질 수 있는 경우에는 어떤식으로 언래핑해야 할까?

두 번 언래핑하기

다행히, if 절은 컴마를 통해서 여러 조건을 나열할 수 있다. (이 때 and로 평가된다.) 그래서 이중 옵셔널을 두 개의 이름을 사용해서 두 번 벗겨내면 된다.

if let x = try? divide(x:8, y:4) as? Int, let y = x
{ print(y) }

다만 이 방법에서는 중간 변수인 x는 실제로 블럭 내에서 사용하지 않는 이름이 되기 때문에 괜히 변수 이름 하나를 낭비하는 느낌도 있다.

이중 옵셔널 캐스팅 패턴 사용하기

널리 알려지지 않았지만 이렇게 이중, 삼중으로 옵셔널이 적용된 타입의 값을 한 번에 언래핑하는 방법이 있다. 사실 옵셔널 바인딩 패턴 (if let x = 옵셔널값)은 일종의 문법적인 장식이며 이는 case let x? =  을 let x = 으로 바꿔서 표현하게 해주는 것이다. 따라서 원래의 패턴을 사용하면 Int?? 타입을 이중 옵셔널 캐스팅을 사용해서 한 번에 언래핑할 수 있다. case let x?? =  패턴을 사용하는 것이다.

if case let x?? = try? divide(x:2, y:1) as? Int {
  print(x)
}

플랫맵 사용하기

(지금은 널리 알려져 있는 사실인데) 옵셔널 타입도 flatMap()을 가지고 있고, 이것은 nil 이 아닐 때 자신을 벗겨내는 것이다.

let a = try? divie(x:2, y:1) as? Int // Int??
if let x = a.flatMap{$0} {
  print(x)
}

가장 깔끔한 방법은 이중 옵셔널 타입매칭이긴 한데, 가독성을 위해서는 (try? divide(x:4, y:2)) as? Int 를 쓰는 것이 가장 현명하겠다. 사실 무엇보다 이 문법에서는 try? .. as? 를 왜 이중 옵셔널로 평가하는지가 가장 애매한 상황인 듯 하다.

참고 자료 – 이중 옵셔널을 만드는 try?

Swift 메일링에 Tim Vermeulen이 같은 이슈를 제기한 바 있고, 이에 대해 Erica Sadun이 관련한 포스팅을 쓴 적이 있다. 이 글에서도 언급하듯이 try?as? 보다는 더 낮은 우선순위를 가지고 있지만, 이 문제는 본질적으로 연산자의 우선순위와 관련된 내용은 아니라는 것이다.

예를 들어 하나의 함수가 예외를 던질 수 있으면서 동시에 옵셔널 값을 리턴할 수 있다고 해보자. 간단히 위 divide() 함수가 Any가 아닌 Int?를 리턴한다고 해보자. 이 경우에는 as? 캐스팅 없이도 이중 옵셔널이 만들어진다. 즉 try?는 함수 호출에 성공하면 그 값을 옵셔널로 감싸며, 예외가 발생하면 해당 예외를 무시하고 nil로 평가하게끔 한다.  즉 아래의 코드에서는 as? 캐스팅이 없지만 그 결과는 이중 옵셔널이 된다.

func divide(x: Int, y: Int) throws -> Int? { ... }

if let x = try? divide(x: 8, y: 4) {
  print(x) // Optional(2)
}

또 이 경우에 ‘스마트하게 옵셔널 값이 성공적으로 리턴되었다면 이중 옵셔널을 만들지 않고 우아하게 축약하면 되지?’라고 생각할 수는 있겠지만 언어 레벨에서의 이러한 축약은 리턴되는 데이터의 문맥을 임의로 제거한다는 문제를 내포할 수 있다. 결국 try?에 의한 이중 옵셔널 발생 문제는 함수 디자인의 문제이지, as와의 연산 우선순위 문제는 아닌 셈이다. 그리고 이에 대해 Erica Sadun은 다음과 같은 코멘트를 붙였다.

옵셔널을 리턴하면서 예외를 발생시키는 케이스는 “너무 나간” 경우입니다. 파일 시스템에서 읽을 수 없는 디렉토리를 액세스하는 경우 예외를 발생시키면서 파일이 존재하지 않는 경우 nil을 리턴하는 함수를 생각해볼 수는 있겠지요. 불명확한 구석에도 불구하고 두 접근법을 아예 생각할 수 없는 것은 아닙니다.

하지만 개인적으로는 이와 같은 경우에는 두 개의 실패 케이스 모두 nil로 리턴하는 함수를 만들거나, 아니면 파일이 없는 경우와 디렉토리를 읽을 수 없는 경우 모두를 각각의 다른 사항의 예외로 던지도록 하는 경우가 맞다고 보여진다. 즉 애초에 throws 함수를 디자인 한다면 그 값이 옵셔널이 되지 않게끔 하는 디자인하는 센스가 우선해야 한다고 생각된다.

오일러 프로젝트 69

오일러 프로젝트 69 번 문제는 오일러의 피(phi)함수에 관한 내용이다. 사실 소인수분해를 빠르게 할 수 있는 방법만 있다면, 오일러 피함수 역시 간단하게 구현할 수 있으나, 여기서는 범위가 1,000,000까지이므로 만만한 문제가 아닐 수 있다. 그런데 문제를 잘 파악해보면 의외로 쉬운 문제이기도 하다.

오일러 프로젝트 69 더보기