[Swift] 연관 타입(Associated Type)

연관타입(Associated Type)

Objective-C의 Associated Object와 비슷한 명칭이라 좀 헷갈릴 수 있는데, 연관타입은 프로토콜 등에서 현재 타입과 관련이 있는 타입을 의미한다.

연관타입은 프로토콜의 일부에 쓰이는 어떤 타입에 대한 플레이스홀더같은 것으로 프로토콜이 실제로 적용되기 전에는 사용되지 않는 타입1을 말한다. 연관타입은 typealias 키워드를 통해 정의한다. 다음 예는 Container라는 프로토콜의 정의이다.

protocol Container {
    typealias ItemType
    mutating func append(item: ItemType)
    var count: Int { get }
    subscript(i:Int) -> ItemType { get }
}

위 프로토콜은 세 가지 특정을 정의하고 있다.

  1. append 메소드를 정의하여, 새로운 아이템을 추가할 수 있다.
  2. count 프로퍼티를 정의하여 아이템의 개수를 알 수 있다.
  3. 정수 인덱스를 사용하는 서브스크립션을 쓸 수 있다.

이 때 Container 프로토콜은 단지 일종의 집합처럼 동작하는 조건을 정의하고 있고, 그 원소의 타입이나 저장방식에 대해서는 제약을 두지 않는다. 즉 여러 개의 정수를 배열로 저장하는 대신에 추가, 개수세기, 서브스크립션을 지원할 수 있다면 이 프로토콜을 따를 수 있고, 문자열을 연결리스트로 저장한다든지 하는 여러 variation에 저장할 수 있다.

프로토콜을 따르는 타입은 그 프로토콜이 정의한 프로퍼티와 메소드를 따르기만 하면 된다. 단 연관타입을 사용하는 프로토콜은 대상 원소 타입이 명확하지 않으므로 어떤 타입을 대상으로 할 것인지를 구체화할 필요는 있다. 프로토콜을 작성하는 시점에서는 해당 프로토콜을 따라야 하는 타입도 결정되지 않았고, 해당 프로토콜에서 사용하는 대상 타입(Container의 경우 Element 타입)도 정의될 수 없다. 따라서 typealias 구문을 통해서 이런 류의 타입이 있다라고 가정해둔 다음, 그 타입을 쓰는 것이다. 이렇게 연관타입을 포함하는 프로토콜을 실제 적용할 때에는 연관타입이 무엇인지 명시해준다.

struct IntStack: Container {
    var items = [Int]()
    mutating func push(item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return item.removeLast()
    }
    //protocol Container
    typealias ItemType = Int
    mutating func append(item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}

위 예제는 정수를 담을 수 있는 스택 구조를 구현하면서 Container 프로토콜을 따르도록 했다. 이 때 프로토콜의 구성요소인 append()의 정의에 쓰인 타입이 Int이기 때문에 ItemType은 꼭 명시적으로 지정해줄 필요는 없다. 타입시스템이 이 정도는 알아서 추론해줄 것이다.

보통은 이렇게 연관타입으로 정의된 프로토콜을 적용할 때는 제네릭을 사용한다. 위 스택 구조의 원소를 정수로 제한하지 않고 아무 타입이나 쓸 수 있도록 제네릭을 적용하면 다음과 같다.

struct AnyStack<T> : Container{
    var items = [T]()
    mutating func push(item: T) {
        items.append(item)
    }
    mutating func pop() -> T {
        return items.removeLast()
    }
    mutating func append(item: T) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> T {
        return items[i]
    }
}

이 때 타입 T는 곧 ItemType의 역할을 하며, 마찬가지로 프로토콜 메소드의 타입을 보면 연관타입을 추론할 수 있다. 이처럼 프로토콜의 연관타입은 프로코톨 정의시에만 쓰여도 충분하며, 대체로 실제 적용되는 타입 내에서는 타입 시스템이 이를 추론할 수 있는 경우가 많다. 일례로 Array 타입은 그 원소의 타입이 무엇이든 간에 위에서 예로든 Container의 세 요소(append, subscript, count)를 모두 지원하므로 다음과 같이 확장하는 것만으로 Container 프로토콜을 따르도록 할 수 있다.

extension Array:Container {}

  1. 즉 프로토콜은 특정 타입에 종속되지 않으므로, 프로토콜을 적용받는 타입에서 어떤 타입을 사용할지 정해지지 않은 경우에 해당 타입을 연관타입으로 정의한다고 이해하면 된다.