Search
Duplicate
🛠️

Opaque Return Type

Created
2019/07/26
Tags
Programming
Swift

Generic Protocol의 한계

저번 포스트에서 Generic Protocol의 용도와 한계에 대해서 다뤘습니다. 간단히 복습해 보자면 다음과 같이 정리 할 수 있겠습니다.
정의 : 내부에서 사용되는 프로퍼티등의 타입을 외부에서 지정해주는 프로토콜
한계 : 리턴 타입으로 쓸 수 없음
코드로 보면 아래와 같습니다.
protocol Identifiable { associatedtype ID_TYPE: Equtable var id: ID_TYPE { get set } init() } class Student: Identifiable { var id: Int required init() { id = 0 } } class AppleDevice: Identifiable { var id:String required init() { id = "asdf-1234" } } func getSomeIdentifiable() -> Identifiable { return AppleDevice() // Compile Error!! }
Swift
복사
이 때 컴파일 에러의 내용은 다음과 같았습니다.
Protocol 'Identifiable' can only be used as a generic constraint because it has Self or associated type requirements

기존의 에러 대처 방법

Generic Protocol은 “Generic Constraint”로만 사용될 수 있다고 합니다. 이게 무슨 말일까요? 말로 설명하는 것 보다, 아래 코드로 보여 드리는 편이 이해가 쉬울 것 같습니다.
func getSomeIdentifiable<T:Identifiable>() -> T { return T() // Identifiable이 T에 대한 제약(Constraint)로 사용되고 있습니다. } var iPhone:AppleDevice = getSomeIdentifiable() print(iPhone.id) // id타입은 String var bob:Student = getSomeIdentifiable() print(bob.id) // id타입은 Int
Swift
복사
이렇게
getSomeIdentifiable 이 사용되는 곳에
“어떤 Identifiable을 사용하려 하는지”를 명시해주면
컴파일러는 어렵지 않게 iPhone의 id 타입을 추론할 수 있고, 불평 없이 컴파일을 할 수 있게 됩니다.

기존 방법의 문제점

자, 그러면 아래와 같은 상황은 어떨까요?
var someIdentifiable = getSomeIdentifiable() // id타입을 추론 할 수가 없습니다 ㅠㅠㅠ
Swift
복사
어떤 Identifiable을 원하는지를 사용하는 측에서 명시해 주질 않으니까, 당연히 컴파일러는 불만을 표시 할 수밖에 없습니다. 그러면 이렇게 생각 할 수도 있습니다. “그러면 언제나 사용하는 측에서 Identifiable의 타입을 명시해 주면 되는 거 아냐?
var iPhone:AppleDevice = getSomeIdentifiable() // 그냥 계속 이렇게 사용하면 안 되나?
Swift
복사
라고요.
아주 틀린 말은 아닙니다. 하지만 이렇게 하면 getSomeIdentifiable의 리턴타입들에 대해 사용자들이 “알아야” 합니다. 즉 은닉화 원칙에 위배됩니다.
이런 단점을 보완하기 위하여 등장한 것이 Swift 5.1에서 등장한 Opaque Return Type 입니다. 결론부터 말하면, some키워드를 추가하면, Generic Protocol도 리턴타입으로 사용 될 수 있습니다.
func getSomeIdentifiable() -> some Identifiable { return AppleDevice() } let iPhone = getSomeIdentifiable() let iPad = getSomeIdentifiable() iPad.id == iPhone.id // 가능!! iPhone.id.isEmpty // 불가능!!
Swift
복사

Opaque Return Type

지금부터는 SwiftUI의 View 타입을 예시로 설명해 보겠습니다. 제 생각으로는 Opaque Return Type 자체가 SwiftUI의 여러 API를 만들기 위해 도입된(적어도 도입의 우선순위에 영향을 받은) 측면이 강한 듯 하니까요.
SwiftUI에서 가장 기초가 되는 녀석은 View라는 프로토콜입니다. 그것도 아주 간단한 녀석이에요. 구현해야 할 요소가 딱 하나밖에 없지요.
protocol View { associatedtype Body : View var body: Self.Body { get } }
Swift
복사
따라서 이런 식으로 쓸 수 있습니다.
public struct ContentView: View { var body: some View { Text("Hi") } } let contentView = ContentView() print(type(of:contentView.body) // 뭐가 나올까요?
Swift
복사
이렇게 하면 ContentView를 사용하는 “프로그래머 입장”에서는, contentView 의 body에 대해 어떤 추측도 할 수 없게 됩니다 contentView가 body를 가진다는 것 말곤 아무것도 확신 할 수 있는게 없죠. 실제로 다음과 같은 코드는 동작하지 않습니다.
contentView.body.bold() // 에러!! // bold()는 Text객체가 쓸 수 있는 메소드
Swift
복사
여기까지는 그냥 프로토콜과 다를 게 없습니다. 다르다고 하면 이 부분입니다.
print(type(of:contentView.body)) // prints "Text"
Swift
복사
그렇습니다. “프로그래머 입장”에서는 contentView.body가 그저 View로 보이지만 “컴파일러 입장”에서는 body의 타입이 훤히 보이는 것입니다. 이게 핵심입니다. 즉, Opaque Return Type은 Generic Protocol의 associatedtype을, “컴파일 타임에” 정하게 합니다.
struct ContentView: View { var body: some View { Text("바로 지금 컴파일러에게 body의 타입을 알려주고 있습니다") } }
Swift
복사
컴파일러는 위 코드에서 제가 Text를 리턴하는 시점에 body의 타입을 알게 되지만, 외부 API사용자에게는 일부러 그 사실을 가리는 것이지요. 컴파일러는 컴파일러대로 타입을 알고 있으니까 컴파일 할 수 있고, 프로그래머에게는 실제 리턴타입이 가려져 보이니(Opaque하니) 은닉화 원칙을 지킬 수 있게 되는 것입니다.
컴파일러는 사실 뭘 리턴하는지 알고 있지만, 프로그래머에게는 그 부분을 모자이크해서 흐리게(Opaque하게)보여줍니다.

마치며

이제 API를 작성하는 사람은, Generic Protocol을 사용하면서도 API사용자로부터 리턴타입을 숨길 수 있게 됩니다. 이게 과연 어떤 의미일까요?
예컨대 SwiftUI에서 우리는 View에게 픽셀단위로 이래라 저래라 하지 않습니다. “저 뷰가 저 뷰 옆에 있었으면 좋겠어” 정도의 로직을 말해주면 “옆”의 정의는 SwiftUI가 각 플랫폼별로 알아서 판단하는 방식입니다. 즉 “내부 구현”은 플랫폼별로도 달라질 수도 있고, 애플의 향후 여러 개발정책에 따라서도 달라질 수 있습니다. 현재는 SwiftUI의 내부는 아무래도 UIKit이나 CoreAnimation등을 사용하고 있겠지만, 나중에는 완전히 다른 프레임워크를 사용하게 될 수도 있는 것이지요. 실제로 그렇게 될 지는 모르지만, 적어도 애플의 개발자들은 그럴 수 있는 자유를 얻은 것입니다!
좀 더 나아가서 행복회로를 돌려보면, SwiftUI의 이런 특성이, 나중에 Swift를 통한 Android개발로 이어질 수도 있다고 생각합니다. ContentView의 내용이 UIView가 아니라 Fragment가 되면 어떤가요! 어차피 우리는 알 수도 없을텐데! 이곳에서 진행되는 대화내용들을 살펴보면, 아주 가망이 없는 얘기는 아닐수도 있다고 생각합니다. 생각만으로도 행복해지네요!