ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Opaque Type
    Swift 2022. 9. 21. 17:44

    some 키워드

    struct CircleImage: View {
        var body: some View {
            return Image("turtlerock")           
        }
    }
    
    struct CircleImage_PreView: PreviewProvider {
    		static var preview: some View {
    		CircleImage()
    }
    
    

    기본으로 import 되어있는 코드에서 타입 앞에 ‘some’이라는 키워드를 볼 수 있다.

    swiftui가 아닌 swift 5.1 이상에 구현되어 있는 기능이다.

    some은 Opaque type으로써 불투명한 타입이다.

    불투명 타입?

    불투명 타입은 도대체 뭐고 어떻게 왜 사용하는지에 대해 알아보자.

    Opaque type이란?

    아래는 opaque type에 대한 overview이다.

    opaque 타입을 리턴하는 함수나 메소드는 return 값의 타입 정보를 숨긴다.

    함수의 return 타입으로 구체적인 타입을 제공하는 대신 return 값은 프로토콜이 지원하는 구제척인 용어로 설명된다.

    타입 정보를 숨기는 것은 기본 타입의 반환값이 비공개로 유지 될수 있기 때문에 모듈과 모듈에서 호출되는 코드 사이의 경계에 유리하다.

    타입이 프로토콜 타입 값을 반환하는 것과 다르게 opaque 타입은 타입 identity를 보호한다.

    컴파일러는 타입 정보에 접근할 수 있지만 모듈의 클라이언트는 그럴 수 없다.

    타입 정보를 숨긴다는 것은 protocol을 채택하고 있는 구체적인 struct나 class의 정보를 숨긴다는 것이다,

    즉, 다형성을 보장한다.

    타입 정보를 숨긴다고 했기에 타입의 반환값이 비공개로 유지 될 수 있는 것 같긴한데…

    타입 정보를 숨긴다? 이는 캡슐화 개념이다.

    어떤 protocol을 채택하고 있는 모든 타입들은 구체화 되어 있지 않기 때문에 외부에서 접근 할 수 없어 안좋은 side effect를 방지하는 역할을 해준다.

    또한 구체적인 타입을 리턴 해주는게 아니기 때문에 protocol을 채택하고 있는 어떠한 타입이 변경 된다고 하더라도 외부에서 사용하는 경우에 문제가 없다.

    때문에 특정 타입의 protocol을 제공하여 이를 통해 기능을 수행한다면, 이는 함수형 프로그래밍, 선언형 프로그래밍을 도와주는데 안성맞춤이란 생각이 든다.

    사실 불투명 타입 아니더라도 타입을 반환 하는건 가능하다.

    우리는 반환 타입으로 프로토콜이나 클래스, 구조체를 항상 넘겨왔다.

    타입 정보를 숨긴다는게 구체적인 타입이 아닌 따르는 상위 타입이라면 굳이 불투명 타입을 쓰지 않아도 되는게 아닐까?

    예제를 보자.

    Opaque Type 예제

    해당 예제는 공식문서에서 발췌함.

    Opaque Types - The Swift Programming Language (Swift 5.7)

    protocol Shape {
        func draw() -> String
    }
    
    struct Triangle: Shape {
        var size: Int
        func draw() -> String {
            var result: [String] = []
            for length in 1...size {
                result.append(String(repeating: "*", count: length))
            }
            return result.joined(separator: "\\n")
        }
    }
    
    let triangle = Triangle(size: 3)
    triangle.draw()
    //*
    //**
    //***
    

    Shape 프로토콜을 채택한 Triangle 이라는 구조체가 있다.

    struct FlippedShape: Shape {
        var shape: Shape
        func draw() -> String {
            let lines = shape.draw().split(separator: "\\n")
            return lines.reversed().joined(separator: "\\n")
        }
    }
    let flippedTriangle = FlippedShape(shape: smallTriangle)
    

    뒤집힌 모양의 Shape을 만들기 위해 Shape을 채택한 FlippedShape이라는 구조체를 만들었다.

    잠깐!

    예제를 보면 ‘왜’ 라는 말이 절로 나온다.

    억지 상황에서 만드는 느낌이라 공감이 가지 않기 때문에 예제를 보는것은 그만하도록 하겠다.

    핵심은 return type으로 불투명 타입을 쓸 수 있다는 점이다.

    불투명 타입이 아닌 프로토콜 타입으로도 반환 할 수 있는데, 왜 굳이 불투명 타입이냐에 대해 아래 글에서 계속 알아보자.

    Opaque Type

    protocol MobileOS {
        associatedtype Version
        var version: Version { get }
        init(version: Version)
    }
    
    struct iOS: MobileOS {
        var version: Int
        init(version: Int) {
            self.version = version
        }
    }
    
    struct Android: MobileOS {
        var version: String
        init(version: Version) {
            self.version = version
        }
    }
    

    protocol은 associatedtype을 가지고 있다.

    프로토콜을 구체화 한 구조체 iOS와 Android가 있다.

    두 구조체는 version을 구체화 하여 프로퍼티로 가지고 있으며, 각각 타입이 다르다.

    자, 여기까진 기존 프로토콜이다.

    이때 protocol type을 반환하는 함수를 만들어보자.

    단순 프로토콜 타입 반환

    func buildPrefferOS() -> MobileOS {
        return iOS(version: 16)
    }
    // compiler Error: 
    // Use of protocol 'MobileOS' as a type must be written 'any MobileOS'
    

    라고 한다면 any 혹은 some 같은 opaque 타입으로 쓰여야 한다고 에러가 발생한다.

    위의 예제와 다른점은 바로 ‘associatedtype’을 사용한다는 점이다.

    이러한 제약조건은 컴파일러가 빌드하지 못하게 한다.

    컴파일러는 protocol을 반환 형식으로 사용할 때 반환된 값의 type identity를 보장하지 않는다.

    즉, protocol이 associatedtype을 가지고 있다면 반환 타입으로 사용 할 수 없다.

    protocol을 채택한 구조체로 반환

    우리가 생각할 때 상위의 프로토콜 혹은 클래스를 반환하면 그 아래 있는 concrete type을 반환 할 수 있다고 생각한다.

    하지만 위의 예제처럼 associatedtype을 가지고 있는 프로토콜이라면 불가능하다.

    하지만 protocol을 채택한 바로 그 타입을 반환한다면 어떨까?

    func buildPreferredOS() -> iOS {
        return iOS(version: 16)
    }
    

    당연히 반환된다. 문제 될게 없다.

    return type을 iOS 타입으로 지정해주고 return 값으로 iOS 구조체를 넘긴다.

    하지만, 우리는 지금 POP 혹은 함수형 프로그래밍 혹은 선언형 프로그래밍을 위해 달려가고 있다.

    Opaque type이 외부에서 비공개로 되어 있다고 했는데 이와 같이 iOS라는 특정 타입이 외부에 공개 되었다.

    buildPreferredOS()를 확장하고 싶다면 이렇게 된다.

    func buildiOS() -> iOS {
        return iOS(version: 16)
    }
    func buildAndroid() -> Android {
        return Android(version: "Jelly Bean")
    }
    

    비슷한 기능…

    이를 해결하기 위해 swfit가 지원해주는 기능이 있다.

    제네릭을 통한 반환

    func buildPreferredOS<T: MobileOS>(version: T.Version) -> T {
        return T(version: version)
    }
    let android: Android =  buildPreferredOS(version: "Jelly Bean")
    let ios: iOS = buildPreferredOS(version: 16)
    

    우리가 늘 그래왔듯이 제네릭을 통해 재사용성을 높혔다.

    하지만 이 함수를 호출하기 위해선 구체적인 제네릭 타입을 넘겨야 한다.

    극한의 모듈화, 선언형, 순수 함수란 무엇인가?

    호출부에서 타입에 특정 타입에 상관없이 좀 더 넓은 범위에서 사용하고 싶다면!!

    이때 opaque type이 등장한다.

    opaque type을 사용한 associatedtype을 가지고 있는 protocol 반환 값으로 넘기기

    func buildPreferredOS() -> some MobileOS {
        return iOS(version: 16)
    }
    

    opaque type을 사용해서, 최상위 protocol인 MobileOS를 반환 값으로 넘겼다.

    컴파일러는 protocol을 채택하고 있는 특정 타입 유형을 유지하고 이 함수를 호출하는 부분에서는 MobileOS를 구현하는 한 반환 타입의 내부 타입을 알 필요가 없다.

    Opaque 타입과 Protocol 타입의 차이점

    기존에도 되던 것이다.

    associatedtype이 들어오기 전엔..

    Opaque 타입과 Protocol 타입의 차이가 도대체 뭔지 보자.

    일단 associate value를 가지고 있는 프로퍼티의 경우 타입으로 넘길 수 없다.

    Returning an opaque type looks very similar to using a protocol type as the return type of a function, but these two kinds of return type differ in whether they preserve type identity. An opaque type refers to one specific type, although the caller of the function isn’t able to see which type; a protocol type can refer to any type that conforms to the protocol. Generally speaking, protocol types give you more flexibility about the underlying types of the values they store, and opaque types let you make stronger guarantees about those underlying types.

    불투명 타입을 반환하는 것은 함수 타입을 반환할 때 프로토콜 타입을 반환하는 것과 비슷해 보인다.

    하지만, 이 두 종류의 반환 타입은 타입 identity를 보존하는 것에서 차이가 있다.

    불투명한 타입은 하나의 특정 타입을 참조하지만 함수 호출자는 어떤 타입인지 볼 수 없다.

    Identity에 대해 조금 더 알아보자

    identity를 보장 한다는게 정확히 어떤 말이냐?

    func buildPreferredOS() -> some MobileOS {
       let isEven = Int.random(in: 0...100) % 2 == 0
        return isEven ? iOS(version: Int(13.1)) : Android(version: "13.0") // compile error
    }
    

    위와 같은 코드에서 MobileOS를 채택하고 있는 iOS와 Android 타입을 반환하는 함수에서 컴파일 에러가 발생한다.

    이는 Opaque Type이 타입에 대한 강력한 보장을 하기 때문이다.

    func opaqueFlip<T: Shape>(_ shape: T) -> some Shape {
        if shape is Square {
            return shape // compile error
        }
      
        return FlippedShape(shape: shape)
    }
    
    func protocolFlip<T: Shape>(_ shape: T) -> Shape {
        if shape is Square {
            return shape
        }
        
        return FlippedShape(shape: shape)
    }

    위의 코드에서 opaque type을 반환하는 함수는 단일 타입을 보장하기 때문에 Shape을 채택한 여러 타입을 반환 할 수 없다.

    "Function declares an opaque return type, but the return statements in its body do not have matching underlying types"

    함수가 opaque 타입을 반환 하지만, 반환부에서 일치하는 underlying 타입이 없다.

    protocol 타입은 조금 더 유연한 

    SwiftUI에서의 Opaque Type

    불투명 타입을 처음 접한곳이 SwiftUI에서의 some이다.

    struct Row: View {
        var body: some View {
            HStack {
               Text("Hello SwiftUI")
               Image(systemName: "star.fill")
            }
        }
    }
    

    View 또한 some 키워들 사용한 불투명 타입이다.

    때문에 View protocol을 채택하고 있는 모든 View는 구체화 된 반환 타입을 들어내지 않아도 된다.

    위의 코드에서 body가 반환하는 타입은

    HStack<TupleView<(Text, Image)>>
    

    이다.

    대부분의 View 혹은 modifier들 또한 View protocol을 채택하기 때문에..

    그렇기 때문에 opaque type이 유용한 것이다.

    결론

    불투명 타입은 associatedType을 가지고 있는 protocol을 채택한 모든 타입들에 대해 return type으로써 유연함을 가지게 해준다.

    하지만 단일 타입 반환을 보장하기 때문에 유연한 타입 반환을 원한다면 protocol type을 사용하는 것이 좋다.

    https://developer.apple.com/wwdc19/402

    'Swift' 카테고리의 다른 글

    Swift) 성능 최적화 - Dispatch와 메모리 할당  (0) 2022.08.30
    Swift) URLSession Network Layer  (1) 2022.08.10
    Data(contentsOf:)? URLSession?  (0) 2022.08.01
    Swift) json 다루기  (0) 2021.06.16
    Swift) API Design Guidelines  (0) 2021.06.10

    댓글

Designed by Tistory.