본문 바로가기

Swift

[Swift] Metatype 이란? (.Type, .self, .Protocol) (번역)

  이번 포스팅에서는 Metatype에 대해 정확히 알아보고자 합니다. 실제로 자주 사용해왔지만 이것이 무엇이냐 묻는다면 정확히 답변하기는 어려웠을 것입니다. 각 접미사들의 차이는 정확히 무엇인지 알아보겠습니다.

 

Metatype 이란?

 

  애플 문서를 살펴보면 Metatype은 타입의 타입이라고 정의되어 있습니다. String 타입의 타입? 이미 타입인데 이 타입의 타입이 있다는 것이라고 합니다. 이론적으로는 이상하게 들리지만 Swift의 쉬운 사용성을 위해 이러한 세세한 사항들을 숨기는 Swift 문법에 익숙해 졌기 때문일 수 있습니다.

 

struct Device {
    static let name = "iPhone"
    func turnOn(name: String) {}
}

let myPhone: Device = Device()

 

  위 코드에서 myPhone의 타입은 무엇일까요? myPhone은 객체(object)이고 Device가 이 객체의 타입이라고 할 것입니다. 그러나 myPhone을 인스턴스로 바라보고 Device를 이 인스턴스의 타입이라고 바라봐야 합니다. 결국 myPhone의 인스턴스 메소드인 turnOn 에 접근할 수 있지만 스태틱 프로퍼티인 name에는 접근할 수 없을 것 입니다.

 

  그렇다면 name에는 어떻게 접근할 수 있을까요? 가장 일반적인 방법은 Device.name을 사용하는 것입니다. 이 방법 외에는 없을까요?

 

let myPhoneType = type(of: myPhone) // Device.Type

 

  type(of:)는 어떤 객체를 모든 스태틱 프로퍼티에 액세스할 수 있는 것으로 변환합니다. myPhoneType을 살펴보면 Device.Type을 나타내고 있음을 알 수 있습니다. Device의 타입인 Device.Type이 존재하고 있다는 것을 알 수 있습니다. Device.Type은 Device의 Metatype인 것입니다.

 

let name: String = myPhoneType.name
let newDevice: Device = myPhoneType.init()

 

  Metatype을 사용하면 Device의 스태틱 프로퍼티와 메소드 모두 접근할 수 있습니다. 이 방식을 알아두면 인스턴스화를 하거나 스태틱 프로퍼티에 접근하고 각 타입에 따라 작업을 하는 메소드를 만들 때 유용하게 사용할 수 있습니다. Metatype을 인자로도 전달할 수 있으므로 일반적인 방법에서 유용하게 사용할 수 있습니다.

 

var devices: [Devicable] = []

func createDevice<T: Devicable>(ofType: T.Type) -> T {
    let device = T.init()
    devices.append(device)
    return device
}

 

  Metatype은 Equality 체크에도 사용될 수 있습니다.

 

protocol Devicable {
    var name: String { get }
    init(name: String)
}

struct iOS: Devicable {
    let name: String
}

struct Android: Devicable {
    let name: String
}

func create<T: Devicable>(deviceType: T.Type) -> T {
    switch deviceType {
    case is iOS.Type:
        return deviceType.init(name: "iPhone")
    case is Android.Type:
        return deviceType.init(name: "Galaxy")
    default:
        fatalError("Unknown device")
    }
}

 

  클래스, 구조체, 열거형, 프로토콜을 포함한 모든 유형의 Metatype을 해당 타입의 이름 뒤에 .Type으로 정의할 수 있습니다. 간단하게 정리하자면, Device는 인스턴스의 타입을 나타내지만 Metatype인 Device.Type은 Device 타입의 타입을 나타내는 것입니다.

 

Dynamic Metatype과 Static Metatype

  type(of:)는 객체의 Metatype을 반환한다는 것을 알아보았습니다. 그런데 실제로 사용할 때 객체가 없으면 어떻게 될까요? 아마도 create(deviceType: iOS.Type)을 호출하면 컴파일 에러를 내뱉을 것입니다. 이유는 간단합니다. array.append(String) 이라고 한다면 마찬가지로 컴파일 에러가 발생합니다. String은 값이 아니라 타입의 이름이기 때문입니다. Metatype을 값으로써 얻어내려면 해당 타입 뒤에 .self를 입력해야 합니다.

 

  다시 정리하자면 String은 타입이고 "Hello world" 가 String 타입의 값이고, String.Type 또한 타입이고 String.self가 Metatype의 값인 것 입니다.

 

let iPhone12 = create(deviceType: iOS.Type) // 컴파일 에러
let iPhone13 = create(deviceType: iOS.self) // 컴파일 성공

 

  .self는 static metatype이라고 부릅니다. 컴파일 타임에 타입이 정해진다는 의미로 받아들일 수 있습니다. 위에서 Device.name으로 스태틱 프로퍼티에 접근했던 방법이 기억나시나요? 이 방법외에도 Device.self.name 또한 가능한 것입니다. Static metatype은 타입의 스태틱 프로퍼티에 직접 접근할 때마다 사용할 수 있습니다. AnyClass는 AnyObject.Type 이라는 점도 흥미로운 부분입니다. 왜냐하면 테이블 뷰에 Cell을 등록할 때 자주 보았기 때문입니다.

 

// func register(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: String)

let tableView = UITableView()
tableView.register(MyTableViewCell.self, forReuseIdentifier: "MyTableViewCell")

 

  반면에 type(of:)는 dynamic metatype을 반환합니다. 그렇기 때문에 런타임에 타입이 정해진다는 걸 알 수 있습니다.

 

let number: Any = 1
let numberType = type(of: number)
print(numberType) // Int

 

  type(of:) 함수는 다음과 같이 정의되어져 있습니다.

 

func type<T, Metatype>(of value: T) -> Metatype

 

  다시 정리하자면, 객체의 하위 클래스의 Metatype에 접근하려면 type(of:)를 사용해야 합니다. 그렇지 않다면 .self 를 통해 static metatype에 접근하면 됩니다.

 

Protocol Metatype

  위에서 말한 모든 것이 프로토콜에도 적용되지만 중요한 차이점이 있습니다. 다음 코드는 컴파일 되지 않습니다.

 

protocol Devicable {}
let deviceType: Devicable.Type = Devicable.self // 컴파일 에러

 

  그 이유는 Devicable.Type은 프로토콜의 자체 Metatype이 아니고 해당 프로토콜을 따르는 모든 타입의 Metatype이기 때문입니다. 애플은 이를 existential metatype이라고 부릅니다.

 

protocol Devicable {}
struct MyDevice: Devicable {}
let deviceType: Devicable.Type = MyDevice.self // 컴파일 성공

 

  이 경우에, deviceType은 Devicable의 스태틱 프로퍼티 및 메소드에만 접근이 가능하지만 MyDevice의 구현부가 호출됩니다. 프로토콜 타입 자체의 Metatype을 얻으려면 .Protocol 접미사를 사용할 수 있습니다. 이는 기본적으로 다른 타입에서 .Type을 사용하는 방식과 동일합니다.

 

let protocolType: Devicable.Protocol = Devicable.self
print(protocolType) // Devicable

 

  그런데 실제로 .Protocol을 사용하는 경우는 보기 힘들 것 입니다. 왜냐하면 프로토콜 자체를 나타내고 있기 때문에 Equality 체크외에는 실제로 할 수 있는 작업이 없기 때문입니다.

 

원글

What Are .self, .Type, and .Protocol? - Bruno Rocha