반응형

 

Hacking with Swift 사이트의 강좌 번역본입니다.

[원문 : https://www.hackingwithswift.com/articles/187/how-to-use-opaque-return-types-in-swift-5-1]

How to use opaque return types in Swift 5.1

Swift 5.1은 불분명한 타입(opaque types)(어떤 종류의 객체인지 구체적으로 알지 못하고 객체의 기능에 대해서 알려주는 타입)이라는 언어에 중요한 새로운 기능을 도입했습니다 . Swift에서 제네릭 기능을 개선하기 위한 더 큰 로드맵의 일부로 SE-0244에서 왔습니다. 

우선 얼핏 보기에 불분명한 타입은 프로토콜과 많이 비슷하지만, associated type으로 작업할 수 있고, 내부에서 매번 같은 타입으로 사용되어야 하고, 구체적인 세부사항을 숨길수 있기 때문에 프로토콜의 개념을 훨씬 발전시킨것입니다.

예를들어, Rebel 기지에서 다른 종류의 전투기를 발진시키길 원하는 경우 다음과 같이 작성할 수 있습니다.

protocol Fighter { }
struct XWing: Fighter { }

func launchFighter() -> Fighter {
    XWing()
}

let red5 = launchFighter()

 

주의: Swift 5.1은 더 이상 단일 표현 함수에서 return 키워드를 요구하지 않으므로, XWing() return XWing()과 같습니다 - 자세한 것은 What’s new in Swift 5.1에서 읽어보세요.

launchFighter() 함수를 호출하는 누구든지 Fighter의 종류를 반환할 것이라는 것을 알고 있지만, 정확히 무엇인지는 알지 못합니다. 결과적으로, struct YWing: Fighter { } 또는 다른 타입을 추가할 수 있고, 그것들을 반환합니다.

launchFighter() 함수 호출을 아는 이는 일종의 Fighter를 반환할 것이지만, 정확히 무엇인지는 알수 없습니다. 결과적으로, struct YWing: Fighter { } 또는 다른 타입을 추가하고 그것들 중 하나를 반환할 수 있습니다.

하지만 문제가 하나 있습니다: 특정 전투기가 Red 5인지 확인하기 위해 어떻게 해야 하나요? 아마도 해결책으로 Equatable 프로토콜을 준수하는 Fighter을 만드는 것을 생각할수 있으므로, ==를 사용할 수 있습니다. 하지만, Swift는 곧바로 launchFighter 함수에 대해 특히 치명적인 오류가 발생할 것입니다: 프로토콜 ‘Fighter'는 Self 또는 연관된 타입이 요구되기 때문에, 제네릭 제약조건(constraint)으로만 사용될수 있습니다.

여기에서 오류가 있는 Self 부분이 문제입니다. Equatable프로토콜은 2개의 인스턴스 자체(Self)가 같은지 확인하기 위해 비교하지만, Swift는 해당 equatable한 것들이 같다라는 보장이 없습니다. - 예를들어, Fighter를 정수 배열로 비교할 수 있습니다.

내부적으로 Swift 컴파일러는 어떤 프로토콜이 실제 사용되는 것을 정확히 알고 있기 때문에, 불분명한(opaque) 타입은 해당 문제를 해결합니다 - XWing, 문자열 배열 또는 무엇이든 알고 있습니다.

불분명한 타입을 다시 보내기 위해서, 프로토콜 이름 앞에 some 키워드를 사용하세요.

func launchOpaqueFighter() -> some Fighter {
    XWing()
}

 

호출자의 관점에서는 여전히 XWing, YWing이거나 Fighter 프로토콜을 준수하는 다른 것일 수도 있는 Fighter을 얻게 됩니다. 하지만 컴파일러의 관점에서는 정확히 무엇이 반환되는지 알고 있으므로, 모든 규칙을 올바르게 따를수 있습니다.

예를들어, 다음과 같이 some Equatable를 반환하는 함수를 고려해보세요.

func makeInt() -> some Equatable {
    Int.random(in: 1...10)
}

 

그것을 호출할때, Equatable값 종류인 것이라는 것을 알고 있고, Swift는 동일한 기본 타입이 될 것임을 알고 있기 때문에, 2번 호출하고나서 2번 호출한 결과를 비교할 수 있습니다.

let int1 = makeInt()
let int2 = makeInt()
print(int1 == int2)

 

다음과 같이 some Equatable를 반환하는 2번째 함수가 있는 경우에는 같지 않습니다.

func makeString() -> some Equatable {
    "Red"
}

 

비록 우리의 관점에서 볼때, Equatable 타입을 돌려주지만, makeString() 에 대한 2번 호출하거나 makeInt() 2번 호출한 결과를 비교할 수 있고, Swift는 문자열과 정수형을 비교하는게 의미가 없다는 것을 알고 있기 때문에, makeString()의 반환 값과 makeInt()의 반환 값을 비교할 수 없습니다. 

여기에서 중요한 단서(proviso)는 불분명한 반환 타입으로 된 함수는 반드시 하나의 특정 타입을 반환해야 하는 하는 것입니다. 예를들어 Bool.random()을 사용해서 XWing 또는 YWing을 무작위로 시작하려고하면 컴파일러가 더 이상 무엇을 돌려보낼지 알수 없기 때문에, Swift는 코드 빌드하는 것을 거부합니다. 

항상 같은 타입을 반환해야 하는 경우에, 왜 func launchFighter() -> XWing 같은 함수를 작성하지 않는 것일까?를 당연히 생각할 것입니다. 때로는 동작할지 모르지만, 다음과 같은 새로운 문제가 발생합니다:

  • 정말로 드러내고 싶지 않은 타입으로 끝이납니다. 예를들어, someArray.lazy.drop { … }을 사용하면 LazyDropWhileSequence를 돌려보냅니다 - Swift 표준 라이브러리에서 전용이며 매우 구체적인 타입입니다. 실제 걱정하는 모든 것은 그것이 시퀀스(sequence)라는 것입니다; Swift의 내부 동작을 알 필요가 없습니다.
  • 나중에 바꾸는 능력을 잃어버렸습니다. launchFighter() XWing만을 반환하도록 하면 나중에 다른 타입으로 교체할 수 없고, Disney가 Star Wars 장난감 판매에 얼마나 많이 의존하고 있는지가 문제가 될 것입니다! 불분명한 타입을 반환함으로써 오늘 X-Wings를 반환할 수 있고, 올해 B-Wings로 바꿀수 있습니다 - 코드 빌드로 제공되는것 중에 오직 하나만을 반환하지만, 여전히 마음대로 변경할 수 있는 유연성을 가질수 있습니다.

어떤 측면에서는  Self 또는 연관된 타입(associated type) 요구사항 문제를 해결하는 제네릭과 비슷하게 들릴수 있습니다. 제네릭은 다음과 같이 작성할 수 있습니다.

protocol ImperialFighter {
    init()
}

struct TIEFighter: ImperialFighter { }
struct TIEAdvanced: ImperialFighter { }

func launchImperialFighter<T: ImperialFighter>() -> T {
    T()
}

준수하는 타입이 매개변수 없이 초기화되야 하는 새로운 프로토콜을 정의하고, 해당 프로토콜을 준수하는 2개의 구조체를 정의하고나서 이를 사용하는 제네릭 함수를 만듭니다. 하지만, 여기에서 다른점은 launchImperialFighter()의 호출자가 다음과 같이, 어떤 종류의 전투기중 하나를 고릅니다. 

let fighter1: TIEFighter = launchImperialFighter()
let fighter2: TIEAdvanced = launchImperialFighter()

 

호출자가 데이터 타입을 선택할수 있도록 하는 경우에 제네릭이 잘 동작하지만, 함수가 반환 타입을 결정하도록 하면 아래로 내려갑니다(fall down). 

따라서, 불분명한 결과 타입은 다음 몇가지 일을 할 수 있습니다:

  • 해당 함수의 호출자가 아니라, 함수들이 어떤 데이터 타입을 반환하는지 결정합니다. 
  • 컴파일러가 내부적으로 무슨 타입인지 정확히 알고 있기 때문에, Self 또는 연관된 타입(associated type) 요구사항을 걱정할 필요가 없습니다.
  • 필요할때마다 나중에 마음을 바꿀수 있습니다.
  • 외부에 비공개 내부 타입을 드러내지 않습니다.

프로토콜과 불분명한 타입간의 차이점을 잊은 경우에, 이것을 생각하세요: Fighter를 반환하는 것은 Fighter타입 종류이지만 무엇인지는 알지 못함을 의미하고, some Fighter를 반환하는 것은 특정 Fighter타입이지만 여전히 무엇인지 알지 못함를 의미합니다. 후자의 경우에, 그 차이점은 기본 타입이 컴파일러가 알고 있는 특별한 무언가이고, 전자의 경우에는 해당 프로토콜을 준수하는 모든 것이 될 수 있습니다 - 메소드를 호출할때마다 달라집니다.

Swift 5.1에는 무엇이 또 있나요?(What else is coming in Swift 5.1?)

Swift 5.1은 모듈 안정성(module stability), 멤버단위 초기화 개선, 단일 표현식 함수의 암시적인 반환, 등과 같은, 다른 새로운 기능들을 소개합니다 - 자세한 내용은 What’s new in Swift 5.1을 살펴보세요.

Swift 5.0에서의 모든 새로운 기능들을 놓친 경우에, 따라잡기에 너무 늦지 않았습니다 - 자세한 정보는 What’s new in Swift 5.0를 보세요.

 

반응형

'Swift > Tip' 카테고리의 다른 글

What’s new in Swift 5.5  (0) 2022.01.25
What’s new in Swift 5.4  (0) 2021.04.15
What’s new in Swift 5.3  (0) 2021.04.15
What’s new in Swift 5.2  (0) 2021.04.15
What’s new in Swift 5.1  (0) 2019.12.17
What’s New in Swift 5.0  (0) 2019.03.07
What’s New in Swift 4.2?  (0) 2018.06.14
Alamofire Tutorial: Getting Started  (2) 2018.05.05
Posted by 까칠코더
,