WWDC/Swift

[Swift] WWDC22 - Design protocol interface in Swift(디자인 프로토콜 인터페이스)

유정주 2022. 10. 21. 16:49
반응형

서론

저는 프로토콜 활용을 잘 못합니다. 프로토콜 활용 경험이 많이 없어서요...

근데 영원히 못하는 상태로 남을 수는 없으니 프로토콜 관련 WWDC를 보고 실제 프로젝트에도 써먹어보자 결심했어요.

 

Embrace Swift generics에서 언급했던 세션인 Design protocol interface in Swift을 들어보았습니다.

 

이번에 들을 세션인 Design protocol interface in Swift는

라고 써있는 만큼 Embrace Swift generics 세션을 먼저 들으시면 좋을 거 같습니다.

Embrace Swift generics는 저도 정리를 했으니 링크 남겨둘게요! (WWDC22 - Embrace Swift generics)

 

아시다시피 이번 WWDC22부터 한글 자막을 지원해주기 때문에 보다 읽기가 쉬웠습니다 ㅎㅎ

다른 분들도 도전하시길

(물론 내용을 이해하는 건 다른 얘기... ㅠㅠ; 한글이어도 어렵습니다.)

 

Design protocol interface in Swift

이번 세션에서는 세 개의 주제를 다룹니다.

  1. 프로토콜의 associated 타입이 Any 타입과 어떻게 상호작용하는지 "타입 이레이저"를 통해 설명합니다.
  2. Opaque result 타입으로 캡슐화를 개선하는 방법을 설명합니다.
  3. 프로토콜의 동일한 타입의 요구사항을 모델링하는 방법을 설명합니다.

 

Understand type erasure

이번 세션에서 다룰 프로토콜과 구조체입니다.

Animal은 Chicken, Cow에서 채택할 프로토콜이고 Food는 Egg, Milk에서 채택할 프로토콜입니다.

 

Embrace Swift generics에서 다뤘던 것처럼 associatedtype을 이용해 추상화를 쉽게 할 수 있습니다.

 

이를 다이어그램으로 표현하면

이렇게 나타낼 수 있는데요.

Self는 Animal 프로토콜을 채택하는 타입이 될 것이고, CommodityType은 Food를 따르는 타입이 될 것입니다.

Animal을 채택한 타입이 Chicken이라면 Food는 Egg가 될 것이고,

Cow라고 한다면 Animal은 Cow, Food는 Milk가 될 것입니다.

 

근데 농장에서는 한 종류의 동물만 키우지 않죠?

닭도 키우고 말도 키우고 소도 키우고...

그럴 땐 [any Animal]로 모든 Animal 타입을 받을 수 있게 처리할 수 있습니다.

 

타입 이레이저(Type Erasure)는 이렇게 Animal 프로토콜을 따르는 특정 타입이 채워진다고 명시만 하는 것을 말합니다.

 

여기에서 animal은 any Animal 타입이고, produce()의 반환형은 associated 타입입니다.

existential 타입이 associated 타입을 호출하면 컴파일러는 타입 이레이저를 이용해 결과 타입을 정해줍니다.

이런 다이어그램이 컴파일러에 의해

이렇게 변합니다.

 

좀 더 구체적인 예시를 보겠습니다.

any Animal 타입 배열에 Cow를 넣으면 animal이 any Animal 타입일지라도

Cow로 결정되면서 Milk를 반환해준다는 의미입니다.

 

이런 동작이 가능한 이유는 상위 타입과 하위 타입의 개념 때문입니다.

컴파일 시점에 구체적인 타입은 알 수 없지만 Cow가 Animal의 하위 타입이라는 것은 알 수 있습니다.

컴파일러가 Cow가 Animal의 하위 타입인 것을 보고 any Animal을 Cow로 결정하고,

Cow의 produce 리턴 타입인 Milk를 반환할 수 있는 것입니다.

이 동작은 Cow 뿐만 아니라 모든 Animal 프로토콜을 채택하는 타입에 동일하게 적용됩니다.

 

메서드나 생성자의 매개변수에 associated 타입이 들어가면 어떻게 될까요?

Animal 프로토콜의 eat 메서드는 associated 타입인 FeedType을 매개변수로 갖습니다.

eat를 호출하려고 할 때는 이전 예시와 달리 타입 이레이저를 수행할 수 없습니다.

타입 추론 방향이 반대이기 때문입니다.

Animal이 어떤 타입인지 명확하게 결정되지 않았기 때문에 인자로 전달되는 FeedType도 명확하게 결정할 수 없습니다.

 

타입 이레이저는 인스턴스 -> 메서드의 반환 타입으로는 동작되지만,

메서드 파라미터 타입 -> 인스턴스 방향으로는 동작되지 않다는 것을 알았습니다.

 

Hide implementation details

지금부터는 구체적인 타입을 숨기는 것에 대해 알아보겠습니다.

구체 타입을 숨기면 인터페이스를 분리하여 모듈화에 유리합니다.

 

이번에도 Animal을 이용해 알아봅시다.

Animal에 밥을 주는 예시를 만들었습니다.

 

Animal은 isHungry 프로퍼티를 가집니다.

배고픈 동물들에게만 밥을 줄거에요.

 

Farm은 hungryAnimal을 찾아 먹이를 주는 역할입니다.

hungryAnimal의 filter는 동물이 적을 때는 유용할 수 있지만, 동물이 많아지면 연산이 불필요하게 증가할 것입니다.

 

lazy를 이용하면 불필요한 임시 할당을 피할 수 있습니다.

LazyFilterSequence는 lazy.filter로 filter 연산을 하면 반환되는 타입입니다.

LazyFilterSequence로 변경하면 결과는 같지만 임시 할당을 피할 수 있습니다.

 

hungryAnimals 타입은 LazyFilterSequence라는 복잡한 타입으로 선언되어야 하는데, 호출하는 쪽은 이걸 몰라도 됩니다.

호출하는 입장에서는 "LazyFilterSequence 같은 복잡한 건 모르겠고! 아무튼 반복할 수 있는 Collection이기만 하면 돼!" 인거죠.

 

이럴 때 some을 이용하면 LazyFilterSequence를 숨길 수 있습니다.

LazyFilterSequence는 Collection 프로토콜을 따르고 있기 때문에 some Collection으로 LazyFilterSequence를 숨길 수 있습니다.

 

하지만 이 방법은 animal이 어떤 타입인지 특정하기 어렵게 됩니다.

hungryAnimals가 Collection이라는 것은 알지만, Collection의 Element 정보는 없기 때문입니다.

 

Swift 5.7은 "Constrained opaque result type"이라는 기능을 도입했습니다.

(이전에는 명확한 데이터 타입을 작성해야 했어요.)

Collection 뒤에 < >를 넣어 Element 타입 정보를 알려줄 수 있죠.

이제 호출하는 쪽이 Collection의 Element가 any Animal 타입이라는 것을 알 수 있습니다!

 

복잡한 타입은 숨기고 Element 정보는 줄 수 있는, 우리가 원하는 인터페이스 형태에요.

 

항상 Lazy하게 동작하는게 아니라 필요에 따라 선택하고 싶을 때도 있습니다.

그럼 조건문을 이용해 분기 처리를 할 수 있을텐데요.

안타깝게도 이 코드는 컴파일 에러가 발생합니다.

반환 타입이 1개가 아니기 때문입니다.

 

이럴 때는 some Collection을 any Collection으로 변경하면 됩니다.

이 프로퍼티가 호출이 되면 다른 타입으로 반환할 수 있다는 것을 알려주는 것이죠.

 

Identify type relationships

이제는 여러 추상화된 프로토콜의 관계를 식별하고 보장하는 방법을 알아보겠습니다.

 

이번에도 Animal을 이용해 알아볼텐데요.

이제부터는 Animal에게 Feed를 주기 위해서는 작물을 재배하고, 수확해야 합니다.

 

각각 동물에게 먹이를 주기 전에 grow와 harvest 단계가 추가되었습니다.

동물마다 grow할 작물과 수확한 먹이 구조체를 따로 만드는 것은 비효율적일 겁니다.

 

이를 추상화해서 feedAnimal 메서드를 만들건데요.

파라미터로 some Animal 타입을 받아서 동물에 따라 다른 작물을 생산하려고 합니다.

그럼 동물마다 따로 정의할 필요 없이 하나로 퉁칠 수 있습니다.

 

먼저 잘못된 프로토콜 추상화를 볼까요?

AnimalFeed에서는 Crop을 associated 타입으로 사용하고, 

CropType에서는 AnimalFeed 타입을 associated 타입으로 사용합니다.

이런 구조는 서로가 서로를 연관 타입으로 사용하면서 무한 중첩이 되버립니다.

AnimalFeed가 Crop을 사용하는데 Crop에서는 AnimalFeed를 사용하고, 그럼 다시 AnimalFeed가 Crop을 사용하고 Crop이 또 AnimalFeed를 사용하고...

끊임 없이 무한 중첩이 돼요.

 

이 모델들은 타입에 안전하지도 않습니다.

소에게는 Hay를 길러서 Alfalfa를 수확해야 하는데 Hay가 Scratch로 바뀌어도 알 방법이 없죠.

Hay도 AnimalFeed이고 Scratch도 AnimalFeed니까요.

 

그럼 어떻게 해야할까요?

where을 이용하면 이 문제를 해결할 수 있습니다.

AnimalFeed와 Crop의 associated 타입에 where절로 제약을 추가했습니다.

AnimalFeed의 Crop은 CropType의 FeedType와 현재의 AnimalFeed가 동일해야 합니다.

또, Crop의 AnimalFeed의 CropType은 현재의 Crop과 동일해야 하죠.

즉, AnimalFeed(1-1) -> Crop(1-2) -> AnimalFeed(2-1) -> Crop(2-2) ... 로 무한 중첩되었던 구조가

AnimalFeed(1-1) <-> Crop(1-2)로 서로를 명확히 알 수 있게 된 것입니다.

 

전체 다이어그램을 봐볼까요?

Cow, Hay, Alfalfa가 한 세트라는 것과 Chicken, Scratch, Millet이 한 세트인 것을 명확히 알 수 있고,

이 관계가 정확히 모델링 되었습니다.


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형