💻 Programming 개발/🍎 iOS 개발, Swift

[WWDC 2022] Embrace Swift Generics

킴디 kimdee 2023. 2. 14. 17:34
반응형
💡 용어 정리
concrete type 구체타입
opaque type 불투명타입
underlying type 기반타입
existential type 실존타입
type erasure 타입 이레이서
boilerplate code 보일러플레이트 코드

 

 


Swift 5.7에서의 제네릭

제네릭은 Swift에서 추상 코드를 작성하는 기본적인 툴. 코드가 발전함에 따라 복잡성을 관리하는데 매우 중요.

<T> where T: Idea 
  • 제네릭은 특정 타입의 세부사항을 추상화하는데 사용
  • 중복되는 구현이 있는 오버로드 코드를 작성하고 있다면 일반화해야할 때
  • 구체 타입으로 시작해서 필요할 때 일반화 하기

다형성 Polymorphism

다형성은 다른 구체 타입에서 하나의 코드가 다르게 동작하도록 코드를 추상화하는 것

image by codegym

다형성을 구현하는 방법

  • 애드혹 다형성 ad-hoc polymorphism
    • 인자 타입(&갯수)에 따라 같은 함수 호출이어도 다르게 동작하는 것
    • 함수 오버로딩
    • 이게 일반적인 해결책은 아님. 새로운 타입이 추가될 때마다 추가적인 보일러플레이트 코드가 필요. 결국 repetitive implementation 이 됨.
  • 서브타입 다형성 subtype polymorphism
    • 수퍼타입 위에서 동작하는 코드는 런타임 때 사용하는 특정 서브타입에 따라 다르게 동작할 수 있음.
    • 즉, 서브클래싱. 클래스가 수퍼클래스의 메서드를 override
  • 매개변수 다형성 parametric polymorphism - 제네릭을 사용
    • 제네릭 코드는 타입 파라미터를 이용하여 하나의 코드가 다른 타입들과 동작할 수 있게 하며 구체타입 자체가 인자로서 사용됨.

프로토콜 protocol

  • 기능을 표현하는 인터페이스
  • 구현되는 세부사항에서 아이디어를 분리한 것
  • 프로토콜을 준수하는 타입의 기능을 나타내는 추상화 도구
  • 각 기능은 프로토콜 요구사항을 나타냄

연관타입 associatedtype

  • 구체타입의 플레이스홀더
  • 연관타입은 프로토콜을 준수하는 특정타입에 따라 달라짐

불투명 타입opaque type vs. 기반 타입underlying type

불투명타입

  • some Tea
  • <T: Tea>
  • 추상타입으로 특정 구체 타입의 플레이스홀더를 나타냄
  • 인풋과 아웃에 모두 사용할 수 있음

기반타입

  • 특정 구체 타입으로 불투명타입으로 대체됨
  • 값을 사용하는 제네릭 코드는 값에 접근할 때마다 동일한 기반 타입을 받는 것이 보장됨.

제네릭 인터페이스 작성 팁 (protocols or not)

some

~ Swift 5.6 (AS - IS)

func feed<A>(_ cat: C) where C: Cat

// 또는 func feed<C: Cat>(_ cat: C)

Swift 5.7 ~ (TO - BE)

func feed(_ cat: some Cat)
  • 포로토콜을 준수하는 추상타입 Cat을 이렇게 작성할 수 있다.  some Cat
  • 위에 있는 정의들은 모두 상동하지만, 5.7 이후의 새 버전이 좀 더 가독성과 이독성이 좋다.
  • some 키워드는 매개변수, result 타입으로 사용할 수 있다. 5.6 이전에는 프로퍼티 타입과 리턴 타입으로만 사용할 수 있었다.
  • some 은 기반타입이 변경되지 않음.

any

 

any는 Swift 5.7 이상에서 사용할 수 있다.

  • 프로토콜 타입 배열과 같이 프로토콜의 임의 타입을 표현해야 할 경우,  any 를 사용한다.
  • any 는 런타입에서 기반 타입이 변경될 수 있음.
  • any X 는 단일 스태틱 타입으로 X 를 준수하는 구체타입을 동적으로 저장하는 기능을 가지고 있으며, 값 타입과 함께 서브타입 다형성을 사용할 수 있다.
  • any 에 필요한 유연한 스토리지를 허용하기 위해서  any X 타입은 메모리 상에서 특별한 표현방식을 가지고 있음.
    • 이 표현방식을 고정된 상자로 생각했을때,
       
      • 구체 타입은 상자 안에 들어갈 수 있는 경우, 상자 안에 저장이 된다.
      • 들어갈 수 없는 경우 인스턴스의 포인터를 저장한다.
  • 구체타입  X 를 동적으로 저장하는 스태틱 타입 any X 는 실존타입이라고 부른다.
  • 다른 콘크리트 타입을 동일한 표현방식으로 사용하는 전략을 타입 이레이서 (type erasure) 라고 한다.
  • 구체타입은 컴파일 타임에 지워지며, 오직 런타임에서만 알 수 있음.
  • 타입 이레이서는 X 를 준수하는 서로 다른 인스턴스 간에 타입 레벨 구분을 제거하고, 이를 통해서 다른 동적 타입을 동일한 정적 타입으로 상호 교환하는 식으로 값을 사용할 수 있게 함.

some vs. any

  • 컴파일러는 기반 값을 언박싱해서 some X 파라미터에 바로 넘겨줌으로써 any X 의 인스턴스를 some X 로 변환할 수 있다.
    • Swift 5.7~some any
      고정된 구체타입을 가지고 있다 임의의 구체타입을 가지고 있다.
      타입 관계를 보장 타입 관계를 삭제

🦮 요약

  • 기본적으로 some 사용하기
  • 임의 값을 저장해야하는 경우 any 로 변경. 여기서 임의의 값은 임의의 구체타입 인스턴스를 뜻함.

위와 같은 방식을 사용하면 타입 이레이서와 맥락적 한계(semantic limitations) 에 드는 비용을, 오직 저장공간의 유연성이 필요할 때만 지불할 수 있음. 기본적으로 변수를 선언할 때 let 을 쓰다가 값 변경이 필요할 때 var 로 바꾸는 것과 동일한 접근 방식.


🥞 부록

타입 이레이서란

제네릭 유형을 지원하는 프로그래밍 언어에서 타입 이레이서는 자유롭게 전달될 수 있는, 제약되지 않은 논-제네릭 타입 안에 제약된 제네릭 타입을 추상화하는 프로세스.


만약 타입을 알 수 없는 객체의 특정 메서드를 호출해야된다면 (aka. 델리게이트 패턴)

타입을 알 수 없다면 객체가 공개하는 인터페이스를 알 수 ❌

하지만, 서로 다른 타입들이 같은 이름의 메소드를 구현해 둘 것을 약속해둔다면 → 프로토콜

왜 타입 정보를 지워야할까?

스위프트는 멀티 패러타임 언어로 개발자가 객체지향/함수지향/제네릭지향/프로토콜 지향 프로그래밍 스타일 내에서 구조를 짜도록함. 프로토콜 지향 프로그램의 주춧돌은, 상속과 같이 높은 결합도를 지양한다. 그러나 이 패턴을 따르면 아래와 같은 에러를 만나게 될 것.

이 컴파일러 에러는 스위프트의 프로토콜 연관 타입과 제네릭 제약조건을 모두 참조한다. 이 에러를 다루기 위해 최종적으로는 타입 이레이서 패턴을 적용할 것.

이 에러는 연관값이 있는 프로토콜을 함수의 인자로서 사용하거나 개체의 콜렉션으로서 사용할 때 발생.

protocol Cat {
  associatedtype CatType
  var power: String { get }
  func eat() -> Int
  func weight(of cat: CatType) -> Double
}

var cats: [Cat] = [cat1, cat2, ... ]
var totalWeight: Int = 0
for c in cats {
  totalWeight += c.weight(of: < ??? >)
}

연관타입을 가진 프로토콜을 구체타입으로 허용한다손 쳐도 이 연관 타입이 구체적으로 결정되어야 할 때 컴파일러가 처리할 수 없게 됨. 스위프트는 type-safe한 언어이기 때문에 컴파일러가 프로토콜의 기반타입을 알고 있어도 컴파일 타임에 결정할 수 없다면 동작하지 않음.

Swift 5.6 이전에서는

5.6 이전에서는 이 문제를 해결하기 위해서 이 개체들을 제약없고 안전하지 않은 “Any” 바리에이션 안에 넣어둠.

class CatSequence {
    let value: Any

    init<T: Sequence>(_ sequence: T) {
        self.value = sequence
    }
}

이 프로세스를 ‘타입 이레이서’라고 한다.

스위프트 표준 라이브러리 안에 이러한 객체들이 많이 포함이 되어 있다. 타입 이레이서를 이용해 의존성 주입도 이루어짐. (참고로 이는 Swift 5.6 이전의 이야기)

Swift 5.7 이후에서는

any 실존 타입의 추가로 타입 이레이서가 Swift 컴파일러의 feature가 됨. 이제 타입 앞에 any 를 붙임으로써 자동으로 스위프트 컴파일러가 타입을 추상화함. 타입이 지워진 값 ⇒ 이것을 실존 타입이라고 부르기로 함.

let erased: any Cat = KoreanShortHairCatType()

이제 위와 같은 오류 대신에, 제약조건 없이 프로토콜을 사용하게 되면 실존타입으로 참조하라는 메시지가 뜨게 될 것.

any 타입의 타입 안전성

수동적인 타입 이레이서와 달리 안전하다. Any 키워드를 사용한 타입 이레이서는 결국 값을 언래핑 해야되는 문제가 있었음

 

🔗 참고링크

https://developer.apple.com/wwdc22/110352

https://www.swiftbysundell.com/articles/referencing-generic-protocols-with-some-and-any-keywords/

https://academy.realm.io/posts/tryswift-gwendolyn-weston-type-erasure/

https://soooprmx.com/타입-지우기-type-erasure-swift/

https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html

https://swiftrocks.com/whats-any-understanding-type-erasure-in-swift

 


 

 

 

오늘도 읽어주셔서 감사합니다. 

 

궁금하거나 나누고 싶은 얘기가 있으시면 댓글로 알려주세요!

재밌게 읽으셨다면 공감과 구독은 큰 힘이 됩니다. 

 

항상 감사합니다.

 

이 글은 doy.oopy.io 에도 발행되어 있습니다. 

 

 

 

 

반응형