WWDC

[WWDC] Design protocol interfaces in Swift

차코.. 2025. 2. 14. 13:57

https://developer.apple.com/videos/play/wwdc2022/110353

 

Design protocol interfaces in Swift - WWDC22 - Videos - Apple Developer

Learn how you can use Swift 5.7 to design advanced abstractions using protocols. We'll show you how to use existential types, explore how...

developer.apple.com

 

 

 

 

(연관 글: https://dlwogus0128.tistory.com/27)

 

이번 글에서는 구체 타입을 추상화하고, 프로토콜로 타입 간의 관계를 모델링하는 방법에 대해서 다룹니다.

 


Understand type erasure

 

associatedtype를 가진 프로토콜과 existential type은 어떻게 상호작용할까요?

 

 

예시 코드를 살펴봅시다.

 

동물(Animal)에는 닭(Chicken)과 소(Cow)가 있고,

이들이 생산하는 식품(Food)에는 달걀(Egg)과 우유(Milk)가 있습니다.

 

닭은 달걀을 낳고, 소는 우유를 생산하는데요.

이 관계를 추상화하기 위해 Animal 프로토콜에 produce() 메서드를 추가합니다.

 

 

 

이 때, 서로 다른 타입의 produce() 메서드를 추상화하기 위해서

연관 타입(associated type)을 활용할 수 있습니다.

 

 

 

이제 associatedtype을 이용해서 Animal 프로토콜을 구현했으니,

produce()를 호출하면 특정한 구체 타입의 Food가 반환되는 방식으로 코드를 설계합니다.

 

Chicken은 CommodityType이 Egg이고,

Cow은 CommodityType이 Milk인 Animal 프로토콜을 따릅니다.

 

 

 

이러한 동물들이 모여있는 농장을 떠올려볼까요?

 

 

농장에는 다양한 동물들이 존재하므로,

농장에 존재하는 동물들을 모두 담은 animals 배열의 속성은 any Animal이 됩니다.

 

 

 

(이전 강연에서 언급되었던 type eraser과 연관됩니다.)

 

 

 

 

any로 타입이 소거되었기 때문에, 정적 타입 관계 또한 제거되어 있는데요.

 

여기에서 produceCommodities() 메서드 내에서는

해당 동물 배열을 순회하면서 각 동물의 produce()를 호출해주어야 합니다.

 

map 클로저에서 들고 있는 animal 매개변수 any Animal인데

produce의 반환 타입associatedtype입니다.

 

이 경우, 특정 Animal의 CommodityType이 무엇인지 명확하지 않으므로,

produce()의 반환타입 또한 any Food로 변환됩니다.

 

any Food는 CommodityType의 upper bound 타입으로,

각각의 요소들의 구체 타입이 명확하지 않기 때문에 컴파일러가 결과 타입을 any로 지정하는 것입니다.

 

 

 

 

(Swift 5.7부터 associated type eraser가 더 명확해졌다고 합니다.)

 

해당 프로토콜 내의 메서드에서 결과 타입에 사용된 연관 타입(CommodityType)은

producing position에 있다고 합니다.

 

즉, 메서드를 호출하면 해당 타입의 값이 생산된다는 의미인데요.

 

 

 

예를 들어, any Animal 타입인 Cow 객체에서

produce()를 호출하면 Milk가 반환됩니다.

 

Milk는 CommodityType의 upper인 any Food으로 저장될 수 있으므로

항상 타입 안정성을 보장합니다.

 

 

 

반면에, 메서드의 매개변수 중 연관타입이 나타나는 경우는 어떨까요.

 

 

 

위처럼 Animal 프로토콜의 eat() 메서드가

연관 타입인 FeedType를 매개변수로 받아야 할 경우가 있습니다.

 

 

 

이 때, 타입 소거를 통해 any Animal이 될 경우

FeedType의 구체 타입을 알 수 없기 때문에 해당 함수를 사용할 수 없습니다.

 

타입 변환이 반대로 진행되기 때문에 (any FeedType -> 구체 타입으로 변환 불가능),

구체 타입을 정적으로 보장할 방법이 없습니다.

 

때문에 some으로 existential type을 언박싱(구체 타입 추론)해야 합니다.

 

 

 

 

clone() 메서드를 가지는 any Cloneable 프로토콜에서도 비슷하게 적용되는데요.

 

clone() 메서드가 Self로 반환하도록 선언되어 있는데,

any Cloneable 타입의 객체에서 clone()를 호출하면 반환 타입이 any Cloneable로 소거됩니다.

 

이와 마찬가지로, associated type이 있는 프로토콜에서도

생산하는 위치에서의 타입 소거는 허용되지만, 소비하는 위치에서는 불가능합니다.

 

 


 

Hide implementation details

 

이제, 불분명한 결과 타입(opaque result type)을 활용하여

구현을 감추고, 필요한 정보만 노출하는 방법을 살펴보겠습니다.

 

 

예를 들어, Animal에 배고픔 여부의 상태를 나타내는 bool 변수가 있다고 가정합니다.

Farm에는 이 동물들 중, 위 속성을 통해 배고픈 동물들만 hungryAnimals()로 분류합니다.

 

filter()를 활용하면 any Animal 배열이 반환되는데,

이는 임시 배열을 생성하기 때문에 비효율적입니다.

 

 

 

이러한 임시 할당을 피하기 위해,

lazy.filter를 사용하면 지연 평가(lazy evaluation)를 활용할 수 있지만,

 

이 경우에는 반환 타입이  LazyFilterSequence<[any Animal]> 처럼 복잡해집니다.

(불필요한 구현 세부 사항을 노출)

 

 

우리는 이 타입에 대해 알 필요가 없고, 반복할 수 있는 컬렉션이다! 만 알면 되는데요.

 

 

 

위와 같이 some Collection으로 반환 타입을 설정하면

특정 컬렉션 타입을 감추면서도 요소 타입에 대한 정보를 유지할 수 있습니다.

 

 

하지만 아래 feedAnimal()에서 해당 컬렉션의 요소(animal)의 타입

(some Collection).Element인데요.

 

이는 우리가 해당 요소 타입이 any Animal이라는 지식이 없다면,

Animal 프로토콜의 어떤 메소들도 호출할 수 없게 만듭니다.

 

 

 

그래서, 숨길 건 숨기고 나타낼 것은 나타내게,

인터페이스 정보를 노출하는 정도를

opaque result type(제한된 불분명한 결과 타입)으로 조절합니다.

 

 

 

위와 같이 프로토콜 이름 뒤에 꺾쇠 괄호 안에 타입 인수를 넣는 형식입니다.

 

 

 

우리 입장에서는

any Animal 배열의 LazyFilterSequence라는 사실은 알 수 없게 되지만,

any Animal과 같은 Element 연관 타입을 가진 Collection을 따르는

구체 타입이라는 것은 알 수 있습니다.

 

때문에 위와 같이 작성하면,

feedAnimal의 animalany Animal 타입으로 표현됩니다.

 

 

 

 

이것이 가능한 이유는

Collection 프로토콜이 Element 연관 타입을 기본 연관 타입으로 갖고 있기 때문입니다.

 

 

위와 같이 any 키워드와도 사용할 수 있습니다.

 

 

some 키워드를 사용하면, 반환 타입이 여러 개인 경우 오류가 발생하는데

 

 

any를 사용하면 되겠죠

 

 


Identify type relationships

 

마지막으로, 동일 타입 요구 사항(same-type constraint)을 활용하여

여러 구체 타입 간의 관계를 보장하는 방법을 살펴보겠습니다.

 

 

 

예를 들어, 동물이 먹는 사료(AnimalFeed)와 작물(Crop)의 관계입니다.

 

소(Cow)는 건초(Hay)를 먹으며, 건초는 알팔파(Alfalfa) 작물에서 만들어집니다.

닭(Chicken)은 스크래치(Scratch)를 먹으며, 이는 기장(Millet) 작물에서 생산됩니다.

 

 

이 관계를 유지하려면,

AnimalFeed 프로토콜과 Crop 프로토콜의

associated type 간의 관계를 정확하게 설정해야 합니다.

 

 

 

 

그러나 단순히 연관 타입만 선언하면, 프로토콜 간의 중첩이 발생합니다.

 

AnimalFeed의 CropType: Crop -> Crop의 FeedType: AnimalFeed

이 관계가 무한대로 번갈아가며 중첩되는데요.

 

 

위와 같이 작성했을 때,

두 프로토콜이 구체 타입 간의 관계를 정확하게 표현하고 있을까요?

 

 

동물들에게 밥을 주기 전에 작물을 재배해야겠죠.

(AnimalFeed의 인스턴스가 없어도 호출될 수 있어야 하기 때문에, static 메서드)

 

grow()는 AnimalFeed의 정적 메소드인데,

다시 말하자면 grow()가 타입에 대해 직접 호출되어야 한다는 뜻입니다.

 

 

그래서 feedAnimal()의 매개변수로 특정 타입의 이름을 넣어야 하는데,

우리는 다른 프로토콜인 Animal을 따르는 some 타입의 특정 값인 것 밖에는 모릅니다.

 

하지만 animal이 따르는 타입(type(of: animal))이 Animal임을 알기에,

some Animal.FeedType.grow()가 가능하고 crop의 반환타입은 some Animal.FeedType.CropType입니다.

 

그렇다면 feed의 반환 타입은 some Animal.FeedType.CropType.FeedType이 되겠죠?

 

하지만 animal.eat()에서 기대한 매개변수 타입은 some Animal.FeedType인데요.

 

위와 같은 모습

 

 

(에러 발생)

 

또 다른 문제점은, 프로토콜 정의가 너무 일반적이라서

구체 타입 간의 요구되는 관계를 정확하게 나타내지 못하는 것입니다.

 

 

예를 들어서, 실수로 알팔파에서 건초가 아닌 스크래치가 생산되도록 설정되어도 

구체 타입이 AnimalFeed과 Crop의 요구사항을 충족하기 때문에

논리적 오류가 생깁니다.

 

 

이를 방지하기 위해서는 associatedtype들이 사실상 같다는 걸 명시해야되는데요.

where문으로 적용할 수 있습니다.

 

 

 

즉, AnimalFeed의 CropType.FeedType

원래 AnimalFeed 타입과 같아야 한다는 제약을 추가하는 것입니다.

 

 

 

 

이렇게 연관 타입을 단일 쌍으로 만들어, 무한 연쇄를 방지할 수 있습니다.

 

 

(Crop 또한 마찬가지로)

 

위와 같이 기대 충족

 

 

위와 같은 다이어그램으로 올바른 관계를 가지게 됩니다.