2023년 1월 4일 • ☕️ 5 min read
프로토콜을 사용해서 개발을 하다보면 자주 만났던 그리고 자주 만날 법한 오류 상황이 있다.
다운로드할 때 필요한 Loadable
프로토콜을 선언하고 associatedtype
으로 반환타입을 선언해보자.
protocol Loadable {
associatedtype Target
func load() -> Target
}
그리고 처리를 위해 loadTarget
메소드를 선언하고 어떤 파일을 다운로드 하기 모르기 때문에 Loadable
로 추상화하였다.
func loadTarget(from loadable: Loadable) {
let target = loadable.load()
...
}
그럼 아래의 컴파일 오류를 만날 수 있다. (Xcode 13 이하)
Protocol 'Loadable' can only be used as a generic constraint because it has
Self or associated type requirements.
이 오류는 Swift 5.6에서 등장한 any
키워드를 통해 해결할 수 있다. (스포 😅)
그리고 Xcode 14부터는 any 키워드를 자동으로 붙여주는 Fix 기능을 아래 사진과 같이 제공한다.
Swift5.7, Xcode14.2를 사용하는 필자가 만난 문구이다.
이 오류가 무엇인지 알고 해결하기 위해서 Existential Type에 대하여 알아야 한다.
Existential type은 실제 타입을 숨겨 모두 같은 타입처럼 보이기 위해 박스로 감싼 데이터 타입이다. 예시를 통해 이해하면 더 쉽다.
Int
, Double
, Float
3가지 타입 모두 준수하는 MyNumber
프로토콜이 있다.
protocol MyNumber {}
extension Int: MyNumber {}
extension Double: MyNumber {}
extension Float: MyNumber {}
이 세가지 타입을 하나의 배열에 담기 위해 MyNumber
타입의 배열을 선언했다.
let intValue: Int = 1
let doubleValue: Double = 1.0
let floatValue: Float = 1
let numbers: [MyNumber] = [intValue, doubleValue, floatValue]
for number in numbers {
print(number)
}
위 numbers
는 existential type이다. Int
, Double
, Float
3가지 타입을 박스로 숨긴다. Swift는 3가지 타입이 모두 같은 타입으로 본다.
Any
키워드가 등장했다.Any
는 개발자에게 해당 타입이 existential type이라는 점 그리고 타입을 준수하는 구현체가 있다는 점을 명시적으로 알려준다.Swift 5.7 이후부터는 any
키워드를 통해 Existential Type을 명시적으로 작성하게 한다. 현재는 any
를 작성하지 않아도 컴파일 오류가 나지 않는다면 Swift 6 부터는 any
를 작성하지 않을 경우 컴파일 오류가 난다. 앞으로 Existential Type을 사용할 때 any
를 붙이는 습관을 가지자.
위 예시에 any
를 적용하면 아래와 같다.
let numbers: [any MyNumber] = [intValue, doubleValue, floatValue]
처음 컴파일 오류 상황으로 돌아가서, loadable
매개변수는 existential type이다. 그러나 아직 Swift의 버전이 6이 아니기 때문에 아래 함수는 컴파일 오류가 나면 안된다.
func loadTarget(from loadable: Loadable) {
let target = loadable.load()
...
}
그러나 컴파일 오류가 생기는 원인은 프로토콜 안에 associatedtype
이나 Self
키워드가 있을 경우 이를 준수하는 concrete type이 필요하기 때문이다.
target 변수는 loadable
이 어떤 타입을 리턴할지 모른다. Target
이 어떤 타입을 가리킬지는 load()
함수가 준수하고 있는 타입에 따라 달려있다. loadable
의 종류에 따라 반환되는 값을 달라지고 결국 컴파일러는 어떤 타입이 로드될 지 추론할 수 없다. 그래서 컴파일 오류가 난다.
만약 Loadable
내부에 associatedtype
이 없을 경우 정상 작동한다. 타입을 쓰는 곳에서 단순 인터페이스라면 접근이 가능하다.
Swift 5.7 이전이라면 generic type을 사용하여 해결할 수 있다.
func loadTarget<T: Loadable>(from loadable: T) {
let target = loadable.load()
...
}
Loadable
프로토콜을 준수하는 구현체를 StringLoader
를 만들고 loadTarget
함수를 호출한다면, 컴파일러는 T
를 Loadable
을 준수하는 타입으로 추론 가능하고 결국 associatedtype 또한 String
타입으로 추론 가능하게 된다.
struct StringLoader: Loadable {
func load() -> String {
...
}
}
let loader = StringLoader()
loadTarget(from: loader)
Swift 5.6에는 any
키워드를 사용할 수 있고, 이제 Swift 5.7에서는 any
를 이용해서 위 오류를 해결할 수 있다.
associatedtype이 존재하는 타입은 무조건 구현체가 있을 것이라고 컴파일러는 판단했지만 이를 찾을 수도 확신할 수도 없기에 오류를 낸다. 그래서 이를 준수하는 concrete type 객체가 있을 것이라고 판단하고 any
키워드를 붙이라고 Xcode가 제안한다.
any
는 명시적으로 컴파일러에게 구현체가 있다는 것을 알려준다. 따라서 컴파일 오류가 나지 않고 런타임에 associatedtype을 찾아낸다.
func loadTarget(from loadable: any Loadable) {
let target = loadable.load()
...
}
프로토콜이 Hashable
과 Equtable
을 준수할 경우도 같은 오류를 만날 수 있다.
아래처럼 작성하면 컴파일 오류가 난다.
protocol Modelable: Hashable {}
class ViewController: UIViewController {
var model: Modelable
}
Hashable
프로토콜은 Equatable
프로토콜을 준수하고 Equatable
프로토콜 내부에 Self
를 포함하는 아래 함수가 있기 때문이다.
public protocol Equatable {
// ...
static func == (lhs: Self, rhs: Self) -> Bool
}
Self
는 타입추론이 필요하기 때문에 associatedtype
과 같은 원리로 컴파일오류가 난다.
기존에는 해결하기 위해서 Model을 enum화하여 Hashable
프로토콜을 채택했다.
enum타입은 concrete type이므로 컴파일러가 오류내지 않는다.
protocol Modelable {}
enum Model: Hashable {
case one
}
class ViewController: UIViewController {
var model: Model
}
Swift 5.7에서부터는 any
키워드를 통해 간단히 해결이 가능하다.
protocol Model: Hashable {}
class ViewController: UIViewController {
var model: any Model
}