[Swift] .self, .Type .Protocol, Self가 뭔디요!

Swift의 메타타입 (Metatype)을 알아보자

naljin
16 min readAug 19, 2020

옛날에 메타 타입 공부한다고 공지도 올려놨는데 역시 까먹어버리고..ㅎㅎ

이후에 상메까지 등록해놨는데 미루고 미루다가 더 이상 미룰 수 없음을 직감한 오늘..!! 쓰고만다. 메타 타입.

4강은 언제 듣지?

참고로 이번 글은

이 포스팅을 번역 + 각색한 후에 좀 더 내용을 추가하는 방식으로 전개될겁니다 ㅎㅂㅎ

허락도 받아왔다구여! 그럼 ㄱㄱ

Metatype 이란?

타입의 타입

뭐? 하다. 지금. 나랑. 장난? 내가 아는 타입은 String 이나 Int같은건데..? 타입의 타입은 도대체 뭐냐고요~~~

자자.. 선생님덜.. 진정하시고 코드를 봅시다

struct Medium {
static let author = "naljin"
func postArticle(name: String) {}
}

let blog: Medium = Medium()

여기서 Medium()instance 이고, Medium 은 instance를 나타내는 type 인거까지는.. 아시죠?!

blog라는 이름으로 생성한 instance에서는 instance method postArticle()호출 할 수 있지만, class property author에는 접근할 수 없답니다.

잠깐! instance method, class property 모두 뭔말인지 모르겠다면?!

👉🏻 [Swift] class func vs static func 을 읽으면 도움이 될걸요?! 😤

자자 그럼 author에는 어떻게 접근할까요??

👩🏻‍💻 Medium.author 을 사용하면 되잖쇼!

맞아요. class property에 접근하는 가장 흔한 방식이죠. 이것 말고도 다른 방법이 있을까요?

👩🏻‍💻 type(of: blog).author 을 사용하쇼!

두둥. 그렇습니다. instance 를 type(of:) 함수 안에 넣는 방법으로도 class property에 접할 수 있습니다.

그러면 대체type(of: blog)의 타입은 뭐길래 이게 가능한걸까요?

let something = type(of: blog) // Medium.Type

Medium type은 Medium.Type 이었네요!! 조금 이상한것 같긴 하지만..이게 바로 Medium의 metatype 입니다요. 타입의 타입!

이렇게 메타 타입을 가지고 아래와 같이 init() 함수클래스 프로퍼티, 메소드를 사용할 수 있어요! type 을 기반으로 한 전반적인 동작을 할 때 유용하겠죠?

let author: String = something.author
let instance: Medium = something.init()

이런 동작들을 generic 한 방식으로도 쉽게 할 수 있어요. metatype을 argument로 넘길 수 있으니까 말이죠!

func createWidget<T: Widget>(ofType: T.Type) -> T {
let widget = T.init()
myWidgets.insert(widget)
return widget
}

Metatype은 아래와 같이 equality check에도 사용될 수 있습니다. 포스팅 저자는 개인적으로는 factory 디자인을 할 때 유용하다고 느꼈다네유 ~.~

func create<T: BlogPost>(blogType: T.Type) -> T {
switch blogType {
case is TutorialBlogPost.Type:
return blogType.init(subject: currentSubject)
case is ArticleBlogPost.Type:
return blogType.init(subject: getLatestFeatures().random())
case is TipBlogPost.Type:
return blogType.init(subject: getKnowledge().random())
default:
fatalError("Unknown blog kind!")
}
}

class, struct, enumprotocol을 포함한 모든 유형(type)의 이름 뒤에 .Type을 붙임으로써 메타 타입을 정의 할 수 있어요. 간단히 말해

- Medium은 인스턴스의 타입 (instance property만 사용 가능)

- Medium.Type은 클래스 자체의 유형(type)을 가리키는 메타 타입 (Medium의 class property사용 가능)

이제 “타입의 타입” 이 좀 말이 되는 것 같나요?

type(of:) Dynamic Metatype vs .self Static Metatype

type(of:) 가 object의 metatype을 리턴하는건 알겠어! 근데 create(blogType: TutorialBlogPost.Type) 이러면 Xcode가 compile error 뱉는다고!

에러 퉤

이게 안되는 이유는 myArray.append(String) 이 안되는 이유랑 같아요. String 은 type의 이름이지 값(value)가 아니잖아요? metatype의 값을 얻기 위해선 type의 이름 뒤에 .self를 붙여야 한답니다.

헷갈린다면 이렇게 생각하세용. String 이 type이고 “Hello World” 가 instance의 value인것 처럼, String.Type 은 type이고 String.self 가 metatype의 value 이다!

let intMetatype: Int.Type = Int.self

let widget = createWidget(ofType: MyWidget.self)

.self 는 Apple이 static metatype 이라고 부르는 것인데요..! 컴파일 time에서의 object type을 멋들어지게 부르는 용어랍니다. 사실 여러분들은 이걸 엄청 많이 쓰고 있었을지도 몰라요. 아까 Medium.author로 class property에 접근했던거 기억나나요? 스아실 이건 Medium.self.author와 같다는 점!

Static metatype은 Swift의 어디에나 있습니다. 그리고 타입의 클래스 프로퍼티에 직접 접근할때마다 무의식적으로 써왔던 거예요! table의 register(cellClass:) 에 쓰이는 AnyClass type도 사실 AnyObject.Type 의 alias 입니다.

public typealias AnyClass = AnyObject.Type

그러니까 이런식으로 뒤에 .self를 붙여줘야겠죠?

tableView.register(MyTableViewCell.self, forReuseIdentifier: "myCell")

저는 사실 컴파일 에러 뜨고 저거 붙이라고 추천해주길래 붙여왔음 ㅋㅎ

한편으로 type(of:)dynamic metatype을 반환하는데, 이는 실제 object의 metatype, 즉 runtime type입니다.

let myNum: Any = 1 // Compile time 에 myNum의 type은 Any지만 runtime type 은 Int
type(of: myNum) // Int.type

type(of:) 함수의 signature는 이렇게 생겼어요.

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

뭐 핵심만 말하자면, 만약 object의 subclass가 matter하다면(뭔 말이지?) 반드시 type(of:)를 사용해서 subclass의 메타 타입에 접근할 수 있도록 해야한다네요. 아니면 (원하는 타입의 이름).self 을 이용해서 static metatype에 직접 접근하등가요

메타 타입의 흥미로운 사실은 recursive하다는 점인데, 이는 Medium.Type.Type 와 같은 meta-metatype을 만들 수 있다는걸 의미해요! 하지만 굳이 그럴일은 잘 없지 않을까..

Protocol Metatype

이전에 말한 모든 사항들이 protocol에도 적용되긴 하지만, 짚고 넘어가야할 중요한 차이점도 있어요! 컴파일 되지 않는 아래의 코드를 볼까요?

protocol MyProtocol {}
let metatype: MyProtocol.Type = MyProtocol.self // Cannot convert value of...

이렇게 protocol 에서 오류가 나는 이유는 MyProtocol.Type 이 protocol 자체의 메타 타입을 뜻하지 않기 때문이에요!

대신 protocol을 inherit 하고 있는 타입의 메타 타입을 뜻한답니다. Apple은 이걸 existential metatype 이라고 부른다고 하네요

protocol MyProtocol {}
struct MyType: MyProtocol {}
let metatype: MyProtocol.Type = MyType.self // Now works!

이 경우에 metatype 프로퍼티는 MyProtocol 의 class property 및 method 들만 접근 가능해요 (실제로는 MyType의 구현체가 불리겠죠?) protocol type 자체의 찐 metatype 을 얻기 위해선 .Protocol 를 끝에 붙여주면 됩니다. 다른 타입들에서는 .Type 을 쓴거랑 유사하게 말이죠!

let protMetatype: MyProtocol.Protocol = MyProtocol.self

위의 코드는 protocol 자체를 가리키고 있기 때문에 protMetatype is MyProtocol.Protocol 같은 equality check 외에는 딱히 할 수 있는게 없어요. 아마 protocol 자체의 metatype 목적은 컴파일러 측면에 있지 않을까 싶은데..그러니까 우리가 평소 프로젝트에서 절대 볼 일이 없지 않았을까요?

Conclusion: More uses for Metatype

metatype을 통해 type을 표현함으로써 좀 더 type-safe한 generic system을 만들 수 있다고 합니다! 아래는 포스팅 저자가 string을 직접적으로 다루지 않기 위해 어떻게 이것을 deep link handler에서 사용했는지에 대한 예제예요.

public protocol DeepLinkHandler: class {
var handledDeepLinks: [DeepLink.Type] { get }
func canHandle(deepLink: DeepLink) -> Bool
func handle(deepLink: DeepLink)
}
public extension DeepLinkHandler {
func canHandle(deepLink: DeepLink) -> Bool {
let deepLinkType = type(of: deepLink)
//Unfortunately, metatypes can't be added to Sets as they don't conform to Hashable!
return handledDeepLinks.contains { $0.identifier == deepLinkType.identifier }
}
}
//class MyClass: DeepLinkHandler {
var handledDeepLinks: [DeepLinks.Type] {
return [HomeDeepLink.self, PurchaseDeepLink.self]
}
func handle(deepLink: DeepLink) {
switch deepLink {
case let deepLink as HomeDeepLink:
//
case let deepLink as PurchaseDeepLink:
//
default:
//
}
}
}

아래는 A/B test의 정보를 보여주고 수집하기 위해 메타 타입을 어떻게 사용하는지에 대해 보여준다네유. 좀 더 최신 예제라고 합니다.

if ExperimentManager.get(HomeExperiment.self)?.showNewHomeScreen == true {
//Show new home
} else {
//Show old home
}
// Experiment Managerpublic static func get<T: Experiment>(_ experiment: T.Type) -> T? {
return shared.experimentDictionary[experiment.identifier] as? T
}
public static func activate(_ experiment: Experiment) {
shared.experimentDictionary[type(of: experiment).identifier] = experiment
}

덧붙이는 Self Type

우리 Self 를 보셨나요?! 우리 Self 를 아시나요!? self 말고 Self요!! 쾅쾅

Self type 은 특정한 타입이 아니예요! 그보다는.. type의 이름을 알아내거나 다시 쓸 필요 없이 현재 type을 편하게 지칭할 수 있도록 도와줍니다. 즉, 자기 자신의 타입을 가리킨다고 하면 이해가 쉬울까요?

다만 위에서 설명했듯이 protocol 에서의 Self 타입은 해당 protocol을 conform하고 있는 최종 type을 지칭하겠죠?

class 안의 멤버들에 대해 Self 는 아래와 같은 상황에서만 나타날 수 있답니다.

  • 메소드의 return type
  • 읽기 전용 subscript의 return type
  • 읽기 전용 computed property 의 type
  • method의 body
class Superclass {
func f() -> Self { return self }
}
let x = Superclass()
print(type(of: x.f())) // Prints "Superclass"
class Subclass: Superclass { }
let y = Subclass()
print(type(of: y.f())) // Prints "Subclass"
let z: Superclass = Subclass()
print(type(of: z.f())) // Prints "Subclass"

예제의 끝 부분에서 Selfz의 runtime type인 Subclass 를 가리켰어요. compile-time type인 Superclass 가 아니라 말이죠.

중첩된 type declaration 안에서, Self type은 가장 안쪽의 type declaration 에 해당하는 type을 가리킨답니다.

사실 Self type 은 우리가 위에서 살펴본 type(of:) 함수와 같은 type을 말해요. 따라서 현재 타입의 member에 접근하기 위해 Self.someStaticMember 를 쓰는건 type(of: self).someStaticMember 로 쓰는 것과 똑같겠쥬?

마무리

ㅇㅏ니

나는 엑코에서 .self 붙이라길래 붙여왔을뿐인데..ㅎ 이런거였군요! Self.self 이런 코드봐서 헉 이건 또 뭐야.. 했는데 이젠 좀 알거같은 너낌적인 너낌~~ ㅎㅎ 지금이라도 알았으면 됐죠 뭐 ㅎ 여러분도 그렇게 생각하실거라 믿으며! 그럼 20000! (급하게 마무리)

참고

--

--