메타타입(MetaType)?
Swift의 메타타입(Metatype)은 타입 자체를 나타내는 타입이다.
일반적으로, 우리는 변수에 값을 저장하거나, 클래스의 인스턴스를 생성할 때 타입을 사용한다.
예를 들어, `Int`, `String`, `Array` 등이 타입이고, 추가로 정의한 클래스나 구조체들도 타입으로 사용할 수 있다.
하지만 Swift에서는 이러한 타입들 자체를 값으로 다룰 수 있고, 이를 위해 메타타입이 사용된다.
메타타입은 명함으로 생각하면 조금 이해가 쉽다.
‘저는 이런 타입이에요’를 실체화, 인스턴스화 한 게 명함과 비슷하다. ▼
메타타입의 존재 의의
"그렇다면 Swift는 메타타입을 어떤 장점 때문에 사용하는 것일까?"
메타타입을 이용하는 큰 이유는 아래와 같다.
동적 생성과 타입 검사
메타타입을 사용하여 런타임에 동적으로 인스턴스를 생성하거나, 특정 타입을 검사할 수 있다.
이는 유연성을 높여주며, 런타임에 발생하는 조건에 따라 다른 타입을 생성하거나 조작하는 것을 가능하게 한다.
추상화 레벨
메타타입을 사용하면, 타입 자체를 변수로 사용할 수 있다.
이는 함수나 메서드의 매개변수로 타입을 전달하는 것을 가능하게 하며, 고차원의 추상화를 달성할 수 있다.
프로토콜 확인
특정 타입이 어떤 프로토콜을 준수하는지 런타임에 확인할 수 있다.
이를 통해, 프로토콜 기반의 설계를 유연하게 관리할 수 있다.
위의 이유들에 대해서는 조금 뒤에 자세하게 확인해볼 것이다.
메타타입의 사용
메타타입은 `.Type`으로 사용할 수 있다.
그리고 메타타입의 인스턴스를 얻기 위해 `.self`를 사용한다. ▼
let intType: Int.Type = Int.self
let stringType: String.Type = String.self
하지만 `.Type`과 `.self`가 완벽히 같은 것은 아니다.
둘이 가리키고자 하는 것은 같지만, 추상화의 정도가 다르다. ▼
`Type`은 우리가 손으로 만질 수 없는 추상적인 존재라고 한다면, `.self`는 우리가 손으로 만질 수 있고 다룰 수 있는 구체적인 존재에 가깝다.
메타타입의 사용 사례
메타타입을 이용하면 클래스의 모든 static 프로퍼티와 메소드들에 접근할 수 있다.
아래와 같이 `Gorani` 구조체가 있다고 해보자. ▼
struct Gorani {
static let name = "noguen"
func sayGoraniself() {
print(self)
}
}
그리고 아래와 같이 `gorani` 인스턴스를 만들었을때, `gorani` 인스턴스는 static 프로퍼티에 접근할 수 없다. ▼
struct Gorani {
static let name = "noguen"
func sayGoraniself() {
print(self)
}
}
let gorani : Gorani = Gorani()
gorani.name = "noguen_noguen" // 접근 불가
"그렇다면 static 프로퍼티에 접근하려면 어떻게 해야할까?"
가장 쉬운 방법은 `Gorani.name`으로 접근하는 것이다.
그리고 이 방법 말고도 하나가 더 있는데, 바로 메타타입을 이용한 접근이다.
`type(of:)` 메소드를 이용하여 `name` 프로퍼티에 접근하는 것이 가능하다. ▼
struct Gorani {
static let name = "noguen"
func sayGoraniself() {
print(self)
}
}
let gorani : Gorani = Gorani()
print(Gorani.name) // noguen
print(type(of: gorani).name) // noguen
print(type(of: gorani)) // Gorani.Type
`type(of:)` 메소드는 인스턴스의 메타타입을 반환해준다.
이렇게 메타타입을 얻으면, 메타타입을 통해 해당 클래스의 `init()`, `클래스 프로퍼티`, `클래스 메소드`를 모두 사용할 수 있다. ▼
"메타타입을 이용하면 해당 클래스의 모든 것에 접근할 수 있는 것은 알겠는데… 이걸로 뭘 할 수 있죠?"
동적 인스턴스 생성과 검사
메타타입을 통해 `init()` 메소드에 접근할 수 있기에, 메타타입 변수가 동적으로 인스턴스를 생성하게 할 수 있다. ▼
struct Gorani {
var name = "noguen"
func sayGoraniself() {
print(self)
}
}
let goraniType : Gorani.Type = Gorani.self
let gorani = goraniType.init()
print(gorani.name) // noguen
또한 이런 식의 타입 검사도 가능하다. ▼
class Elk : Deer { }
class WaterDeer : Deer { }
class RoeDeer : Deer { }
func newDeer<T: Deer>(deerType: T.Type) -> T {
switch deerType {
case is Elk.Type:
return deerType.init()
case is WaterDeer.Type:
return deerType.init()
default:
fatalError("What deer is this...?")
}
}
Static 메타타입(.self)과 Dynamic 메타타입(type(of:))
메타타입에도 종류가 있는데, 하나는 정적(static) 메타타입이고, 다른 하나는 동적(dynamic) 메타타입이다.
Static 메타타입
우선 정적 메타타입부터 보자.
정적 메타타입은 앞에서 봤던 `.self`가 해당된다.
컴파일 시간에서의 객체의 타입이라고 할 수 있다.
.self를 생각보다 많이 사용하지 않을 것 같지만, 우리는 사실 알게 모르게 많이 사용하는 중이다.
위에서 봤던, `Gorani.name` 도 사실은 `Gorani.self.name` 이 축약된 형태다.
table의 register(cellClass:)에 사용되는 AnyClass 타입도 사실은 앞의 `Gorani.self.name`을 `Gorani.name`으로 줄인것과 같은 방식으로, `AnyObject.Type`의 alias다.
Dynamic 메타타입
그렇다면 동적 메타타입은 무엇일까?
동적 메타타입은 앞에서 봤던 `type(of:)`가 해당된다.
`type(of:)`가 반환하는 값은 실행 시간에서의 객체의 타입이라고 할 수 있다.
`type(of:)` 함수의 선언은 아래와 같다. ▼
func type<T, Metatype>(of value: T) -> Metatype {}
객체의 서브클래스가 중요한 경우 해당 서브클래스의 메타타입에 접근하기 위해 `type(of:)`를 사용해야 한다.
그렇지 않은 경우, 정적 메타타입에는 `(원하는 타입의 이름).self`를 통해 직접 접근할 수 있다.
메타타입의 흥미로운 특징 중 하나는 재귀적이라는 것인데, 이는 `Gornai.Type.Type`과 같은 메타-메타타입을 가질 수 있다는 것을 의미한다.
그러나 다행히도 이들에 대해서는 현재 메타타입에 대한 확장을 작성할 수 없기 때문에 큰 문제는 없다.
프로토콜과 메타타입
메타타입은 프로토콜을 다룰 때도 유용하다.
프로토콜의 메타타입은 `.Protocol`으로 표현된다.
예를 들어, `GoraniProtocol` 의 메타타입은 `GoraniProtocol.Protocol`이다.
프로토콜의 메타타입은 주로 타입이 특정 프로토콜을 준수하는지 확인하는데 사용된다.
`GoraniProtocol`이라는 프로토콜을 만들고, 해당 프로토콜의 타입을 받아오기 위해 아래와 같이 코드를 작성했다고 해보자. ▼
protocol GoraniProtocol { }
let protocolType: GoraniProtocol.Type = GoraniProtocol.self
앞에서 했던 대로 했지만, 여기서는 오류가 나게 된다. ▼
앞에서는 이렇게 메타타입을 받아왔던 경험을 생각해보면 이 시도 자체가 잘못된 것은 아니다.
문제는 프로토콜이 특수하게 메타타입을 사용하기 때문이다.
제대로 동작하는 아래의 코드를 보자. ▼
protocol GoraniProtocol { }
struct Goranirani: GoraniProtocol { }
let protocolType: GoraniProtocol.Type = Goranirani.self
이 코드는 문제없이 컴파일된다.
이런 차이가 발생하는 이유는 `**GoraniProtocol.Type`은 `GoraniProtocol` 자체의 메타타입을 말하는 것이 아니라, 해당 프로토콜을 따르는 `Goranirani`의 메타타입을 말하는 것으로 바뀌기 때문이다.**
이를 existential metatype이라고 한다.
프로토콜 자체의 메타타입을 받아오기 위해서는 앞에서 언급한 `.Protocol`을 사용해야한다.
그러나 `.Protocol`을 통해 할 수 있는 것은 Equality 체크 정도이기 때문에 사용할 일이 크게 없을 것이다.
리플렉션
리플렉션은 코드가 런타임에 자기 자신의 구조와 속성을 검사하고 수정할 수 있는 기능이다.
Swift는 제한적인 리플렉션 기능을 제공한다.
Swift의 메타타입은 리플렉션의 한 형태로 볼 수 있다.
메타타입을 사용하여 런타임에 타입 정보를 얻고, 그 타입의 인스턴스를 생성할 수 있다.
또한, Swift는 `Mirror`라는 특별한 타입을 통해 더 깊은 수준의 리플렉션을 제공한다.
`Mirror`를 사용하면, 런타임에 객체의 속성(프로퍼티) 및 값에 접근할 수 있다.
리플렉션에 대한 건 다음에 더 자세하게 보도록 하자.
마치며
메타타입이라는 개념이 조금 생소하지만, 여러 번 더 보다보니 어떤 상황에서 어떻게 사용해야 효율적으로 사용할 수 있는 지 알겠다.
그래도 이름을 조금은 가독성 있게 지어줄 수 있지 않았나… 싶기도 하다….
'Develop > iOS' 카테고리의 다른 글
[iOS][Swift] 클로져 캡쳐에 대한 이해 (0) | 2024.02.11 |
---|---|
[iOS][Swift] Two Phase Initialization (0) | 2024.02.09 |
[iOS][Swift] Swift의 메모리 관리 : ARC 1 (0) | 2024.02.09 |
[iOS][UI] Auto Layout에 대해 (0) | 2024.01.23 |
[iOS] Delegate 패턴 (0) | 2024.01.23 |