안녕하세요.
이번에는 WWDC22에서 소개된 'Embrace Swift generics' 란 세션으로 Swift의 추상화 방법에 대해 자세하게 알아보는 시간을 가져볼게요.
Cow(🐮)라는 구조체가 있다고 해볼게요.
Cow는 Hay(건초)를 매개변수로 가진 eat(먹기) 함수를 가지고 있습니다.
Hay(건초) 구조체는 건초를 생산하는 작물인 알팔파를 재배하기 위한 grow(키우기) 함수를 가지고 있습니다.
Alfalfa(알팔파) 구조체는 알팔파로부터 건초를 harvest(수확)하는 함수를 가지고 있습니다.
마지막으로 Farm(농장) 구조체는 Cow에게 먹이를 주는 feed(먹이기)라는 함수를 가지고 있습니다.
그런데 농장에서 키울 수 있는 동물이 Cow(🐮) 밖에 없는건 아니잖아요??
우린 확장성을 고려해야 합니다.
Cow(🐮), Horse(🐴), Chicken(🐔)도 키우고 싶다면 어떻게 해야 할까요?
동물들은 각기 다른 먹이를 먹지만 먹는 행위 자체는 공통으로 가지고 있습니다. 즉 추상화가 가능하다는 것이죠.
확장성을 고려하여 추상화를 시킨다면, 하나의 추상코드가 구체적인 타입에 따라 각기 다른 함수를 호출할 수 있도록 구현할 수 있습니다.
이를 polymorphism(다형성)이라고 합니다.
Swift에서 polymorphism은 크게 3가지 방법으로 나뉩니다.
- ad-hoc polymorphism
- subtype polymorphism
- parametric polymorphism
## (1) ad-hoc polymorphism
ad-hoc 이란 어려운 말을 썼지만, 결국 함수 overloading을 통한 다형성을 의미합니다.
함수 인자에 의해 어떤 함수가 호출될지 결정되는 것이죠.
조금 전 예시에 ad-hoc polymorphism을 적용해 볼게요.
동물 종류가 많아질수록 overloading 할 함수도 비례해서 많아지기 때문에 확장성을 고려하지 않은 코드라고 할 수 있어요ㅠㅠ
덤으로, feed 함수 내부 구현도 거의 유사하기 때문에 볼수록 속상한 코드가 되었습니다ㅠ
## (2) subtype polymorphism
subtype polymorphism에서 주로 사용되는 방식은 상속을 통한 다형성입니다.
Animal이란 상위 클래스를 만들고, 각 동물들은 Animal 클래스를 상속받고 eat 함수를 override 해줍니다.
참고로 구조체는 상속을 받을 수 없으니, 동물 구조체를 동물 클래스로 바꿔줘야 합니다.
그다음, Animal 클래스의 eat 함수에서는 어떤 먹이를 줄지 알 수 없기 때문에 제네릭(type parameter)으로 먹이 타입을 받아야 합니다.
(type parameter 대신 Any 타입을 함수 인자로 받도록 할 수 있지만, 그러면 모든 하위 클래스에서 타입 검사를 해줘야 합니다.)
이렇게 구현할 경우, 몇 가지 단점이 있습니다.
1) 무조건 클래스로 선언해야 함.
2) 자식 클래스에서 override 함수가 구현되었는지 여부를 런타임 때 알 수 있음.
3) 제네릭 타입 개수가 늘어날 경우, 관련 모든 자식 클래스에서 추가로 선언해줘야 함.
## (3) parametic polymorphism
protocol과 제네릭을 사용한 방법입니다. type parameter(=제네릭)로 하나의 코드에서 여러 타입을 받을 수 있도록 구현하는 방식입니다.
Animal이란 protocol을 정의하고 동물들은 해당 프로토콜을 채택합니다.
이때 associatedtype은 컴파일 타임 때 동물 구조체 안에서 구현한 eat 함수의 매개변수 타입을 보고 타입 추론 됩니다.
Farm 구조체의 feed 함수에선 opaque type을 사용하여 어떤 동물타입이든 매개변수로 전달받을 수 있도록 구현해 주면 됩니다.
[참고] opaque type 이란?
opaque type에 대해 정리한 글이 있으니 참고해 주세요!
이렇게 해서, parametric polymorphism에 대해서 알아봤는데요. 관련해서 좀 더 살펴볼 것이 있습니다.
만약, 농장에 있는 많은 종류의 모든 동물한테 먹이를 주는 feedAll 이란 함수를 추가한다고 해볼게요.
위 코드처럼 매개변수로 [some Animal] 타입을 받게 되면 어떤 종류인진 모르겠지만 같은 종류의 동물들이 있는 배열을 전달받는다는 것을 의미합니다.
(some. 즉, opaque type은 1가지의 underlying type만을 가질 수 있기 때문이죠.)
우린 다양한 종류의 동물 배열을 전달받고 싶은 거죠?
이럴 땐 any 키워드를 사용해야 합니다. [any Animal] 타입의 의미는 "어떤 종류인진 모르겠고 동물이면 ㅆㄱㄴ"라는 의미라고 보면 될 것 같아요.
(boxed protocol type 또는 existential type이라고 하고 underlying type은 런타임 때 달라질 수 있습니다.)
[참고] existential type 이란?
existential type에 대해 정리한 글이 있으니 참고해 주세요!
any Animal(existential type)엔 동적으로 여러 타입이 올 수 있고, 이를 위해 메모리에 특별한 표현(?)을 가진다고 해요.
WWDC 세션에선 이 표현을 상자라고 묘사를 했어요.
어쩔 땐 상자 안에 값 그 자체가 들어갈 때가 있고 어쩔 땐 어떤 값을 가리키는 포인터가 들어간다고 해요.
그리고 서로 다른 타입(위 예시에선 닭과 소를 의미합니다)을 동일한 상자 안에 넣어서 추상화시키는 것을 type erasure라고 합니다.
구체적인 타입(=concrete type)은 컴파일 타임 때 type erase 되며, 런타임 때 구체적인 타입이 뭔지 알 수 있게 됩니다.
(즉, static type은 같지만 dynamic type은 다르다는 것이죠.)
type erasure는 동일한 static type을 가지기 위해 구체적인 타입의 type-level distinction을 제거합니다.
type erasure 덕분에 [any Animal] 안에는 여러 종류의 동물들이 들어갈 수 있는 것이죠.
이제 다시 feedAll 함수로 돌아가서.
매개변수로 [any Animal] 타입을 받고 반복문으로 eat 함수를 호출해 보면
엌... 컴파일 에러가 발생합니다.
그 이유는 type erasure 때문인데요.
type erasure가 tyle-level distintion을 제거한다고 했죠? 제거하면서 구체적인 타입에 포함된 associatedtype 정보까지 같이 지우기 때문이에요ㅠㅠ
이럴 땐 any Animal의 eat 함수를 직접 호출하지 말고, some Animal 타입으로 unboxing을 해줄 필요가 있습니다.
any Animal이랑 some Animal은 서로 다른 타입이지만 컴파일러에서 타입 변환이 가능합니다.
컴파일러가 박스를 열어서 안에 있는 값을 가져오는 개념이고, some Animal 이기 때문에 associatedtype을 포함해 underlying type이 뭔지 알 수 있게 되는 흐름인 것 같아요.
컴파일러의 unboxing을 활용한 feedAll의 최종 코드는 이렇게가 되겠죠?
## some vs any
some과 any 키워드 기능 차이점은 아래와 같습니다.
그리고 개발할 땐 default로 some 키워드를 사용하는 것을 권장하고 특정 상황일 때 any로 바꾸라고 합니다.
any 키워드를 사용하면 type erasure 같은 비용을 지불해야 하기 때문입니다.
[참고] some View?
SwiftUI 경험이 있으신 분들은 some을 보자마자 var body: some View가 떠올랐을 것 같은데요.(저만 그런가요..?)
맞습니다. body property는 opaque type입니다.
some View 타입인 경우도 마찬가지로 함수 내부에서 다른 타입을 반환할 수 없습니다.
만약 경우에 따라 다른 타입을 반환하고 싶으면 @ViewBuilder annotation을 붙여주세요!
오잉? 근데 body property에는 @ViewBuilder annotation을 따로 안넣었는데?? 라고 생각하실 수 있지만 body property는 이미 @ViewBuilder 타입이었답니다ㅎㅎ..
# 요약
- Swift에서의 다형성은 크게 3가지가 있다. (ad-hoc, subtype, parametic)
- any 키워드를 사용하면 컴파일 타임 때 type erasure가 동작한다.
- 컴파일러는 any -> some 타입으로 변환이 가능하다.
# 참고
- https://developer.apple.com/videos/play/wwdc2022/110352
이번 글은 여기서 마무리.
'WWDC' 카테고리의 다른 글
[WWDC23] Verify app dependencies with digital signatures (0) | 2023.08.16 |
---|---|
[WWDC22] Explore App Tracking Transparency (0) | 2023.08.05 |
[WWDC22] Create your Privacy Nutrition Label (0) | 2023.07.31 |
[WWDC22] What's new in WKWebView (0) | 2023.07.29 |
[WWDC22] Use SwiftUI with UIKit (feat. UIHostingConfiguration) (0) | 2022.08.08 |