티스토리 뷰

 

 

Opaque Types — The Swift Programming Language (Swift 5.5)

Opaque Types A function or method with an opaque return type hides its return value’s type information. Instead of providing a concrete type as the function’s return type, the return value is described in terms of the protocols it supports. Hiding type

docs.swift.org

 

 

 

Opaque Types

불투명 리턴 타입 (opaque return types) 을 가지는 함수 또는 메서드는 리턴 값의 타입 정보를 숨깁니다. 함수의 리턴타입으로 구체적인(concrete) 타입을 제공하는 대신, 리턴 값은 지원하는 프로토콜에 따라 설명이 됩니다. 모듈과 모듈을 호출하는 코드 사이 경계에서 타입 정보를 숨기는 것은 유용합니다. 리턴 값의 기본 타입이 비공개로 유지될 수 있기 때문입니다. 프로토콜 타입을 가지는 값을 리턴하는 것과 달리 불투명 타입은 타입 독자성(type identity) 를 유지합니다. 컴파일러는 타입 정보에 접근할 수 있지만, 모듈의 클라이언트는 액세스 할 수 없습니다.

 

 

 

 

The Problem That Opaque Types Solve (불투명 타입이 해결하는 문제)

예를 들어  ASCII art shape(도형) 를 그리는 모듈을 작성한다고 가정합니다. ASCII art shape 의 기본 특성은 해당 shape를 표현하는 문자열을 리턴하는 draw() 함수입니다. Shape 프로토콜의 요구사항으로 사용할 수 있습니다.

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 smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())
// *
// **
// ***

 

아래 코드에 표시된 대로, 제네릭을 사용하여 도형을 수직으로 뒤집는 작업을 수행할 수 있습니다. 그러나 이러한 접근 방식은 큰 제한이 있습니다. 뒤집힌 결과는 생성할 때 사용했던 제네릭 타입을 나타냅니다.

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

 

JoinedShape<T: Shape, U: Shape> 구조체를 정의하는 이러한 방식은 JoinedShape<FlippedShape<Triangle>, Triangle> 와 같은 타입이 뒤집힌 삼각형을 다른 삼각형과 결합하는 결과를 낳습니다.

struct JoinedShape<T: Shape, U: Shape>: Shape {
    var top: T
    var bottom: U
    func draw() -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
print(joinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *

 

도형 생성에 대한 자세한 정보를 노출하면 ASCII art 모듈의 공개 인터페이스에 포함되지 않은 타입이 유출될 수 있습니다. 전체 리턴 타입을 명시해야 하기 때문입니다. 모듈 내부의 코드는 같은 도형을 여러 가지 방식으로 만들 수 있습니다. 도형을 사용하는 모듈 밖의 다른 코드는 변환 목록에 대한 구현 세부 정보를 고려하지 않아도 됩니다.

JoinedShape 와FlippedShape 같은 래퍼(Wrapper) 타입은 모듈의 사용자에 중요하지 않고 보이지도 않아야 합니다. 모듈의 public 인터페이스는 도형을 결합하고 뒤집는 작업으로 구성되며 이러한 작업들은 다른 Shape 값을 리턴합니다.

 

 

 

 

 

Returning an Opaque Type (불투명 타입 리턴)

불투명 타입은 제네릭 타입의 반대로 생각할 수 있습니다. 제네릭 타입은 함수를 호출하는 코드가 "해당 함수의 파라미터"와 "함수 구현에서 추상화된 방식으로 값을 리턴하는 타입"을 선택하게 해줍니다.

아래 예제 코드는 호출자(caller) 에 의존해 타입을 리턴합니다.

func max<T>(_ x: T, _ y: T) -> T where T: Comparable { ... }

max(_:_:) 를 호출하는 코드는 x, y 를 위해 값을 선택합니다. 해당 값들의 타입은 T의 구체적인 타입을 결정합니다. 호출한 코드는 Comparable 프로토콜을 준수하는 모든 타입을 사용할 수 있습니다. 함수 내부의 코드는 일반적인 방식으로 작성되어, 호출자가 제공하는 모든 타입을 처리할 수 있습니다. max(_:_:) 의 구현은 모든 Comparable 타입 공유하는 기능만을 사용합니다.

 

이러한 역할은 불투명 리턴 타입(opaque return type)을 사용하는 함수에서는 반대입니다. 불투명 타입은 함수 구현이 "함수를 호출하는 코드로부터 추상화된 방식으로 리턴되는 값의 타입"을 선택하게 해줍니다. 예를 들어 아래 예제의 함수는 해당 도형의 기본 타입을 노출하지 않고 사다리꼴(trapezoid)를 리턴합니다.

struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array<String>(repeating: line, count: size)
        return result.joined(separator: "\n")
    }
}

func makeTrapezoid() -> some Shape {
    let top = Triangle(size: 2)
    let middle = Square(size: 2)
    let bottom = FlippedShape(shape: top)
    let trapezoid = JoinedShape(
        top: top,
        bottom: JoinedShape(top: middle, bottom: bottom)
    )
    return trapezoid
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
// *
// **
// **
// **
// **
// *

makeTrapezoid() 함수는 리턴 타입을 some Shape로 선언합니다. 함수는 Shape 프로토콜을 준수하는 지정된 타입의 값을 리턴합니다. 어떤 특정 구체적인 타입을 명시하지 않고 말입니다. 이러한 방식으로 makeTrapezoid() 를 작성하는 것은 public 인터페이스의 기본적인 측면을 표현할 수 있습니다. (반환하는 값이 shape 이다) 모양이 public 인터페이스의 일부로 만들어지는 특정 타입을 만들지 않고 말입니다.

이러한 구현은 두개의 삼각형과 한 개의 정사각형을 사용하지만, 반환 타입을 바꾸지 않고 다양한 방식으로 사다리꼴을 그리도록 함수를 다시 작성할 수 있습니다.

 

예제에서는 불투명 리턴 타입(opaque return type) 이 제네릭 타입의 반대와 같은 방식이라는 것을 강조합니다. makeTrapezoid() 안의 코드는 Shape 프로토콜을 준수하는 한 필요한 모든 타입을 반환할 수 있습니다. 제네릭 함수를 호출하는 코드처럼 말입니다.

함수를 호출하는 코드는 제네릭 함수의 구현처럼, 일반적인(general) 방식으로 작성되어야 합니다. 그에 따라 makeTrapezoid() 에서 리턴되는 모든 shape 값을 사용할 수 있습니다.

 

불투명 타입 리턴(opaque return type)을 제네릭(generics) 와 결합할 수도 있습니다.

아래 예제코드의 함수 모두 Shape 프로토콜을 준수하는 일부 타입의 값을 리턴합니다.

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}
func join<T: Shape, U: Shape>(_ top: T, _ bottom: U) -> some Shape {
    JoinedShape(top: top, bottom: bottom)
}

let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
print(opaqueJoinedTriangles.draw())
// *
// **
// ***
// ***
// **
// *

이전의 예제들과 달리 flip(_:) 과 join(_:) 은 제네릭 도형 구현이 불투명 리턴 타입으로 리턴되는 기본 타입을 감쌉니다. 해당 타입들이 표시되지 않게 합니다. 두 함수 모두 제네릭입니다. 타입이 제네릭에 의존하고 있기 때문입니다. 함수의 타입 파라미터는 FlippedShape 와 JoinedShape에 필요한 타입 정보를 전달합니다.

 

불투명 리턴 타입이 있는 함수가 여러 위치에서 리턴된다면, 리턴 가능한 모든 값들은 반드시 같은 타입을 가져야 합니다. 제네렉 함수의 경우, 리턴 타입이 함수의 제네릭 타입 파라미터(generic type parameters) 를 사용할 수 있습니다. 하지만 반드시 단일 타입이어야 합니다.

여기 사각형의 특수한 경우를 포함하는 도형-뒤집기 함수의 유효하지 않은(invalid) 버전이 있습니다.

func invalidFlip<T: Shape>(_ shape: T) -> some Shape {
    if shape is Square {
        return shape // Error: return types don't match
    }
    return FlippedShape(shape: shape) // Error: return types don't match
}

Square로 이 함수를 호출하면, Square를 리턴합니다. 그렇지 않으면 FlippedShape를 리턴합니다. 이는 "리턴 값이 반드시 하나의 타입이어야 한다"는 요구사항을 위반합니다. 그리고 invalidFlip(_:) 가 유효하지 않게합니다. 

invalidFlip(_:) 를 고치는 한가 지 방법은 사각형에 대한 특수한 케이스를 FlippedShape 구현으로 옮기는 것 입니다. 이를 통해 함수가 항상 FlippedShape 값을 리턴하게 됩니다.

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        if shape is Square {
            return shape.draw()
        }
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}

단일 타입을 항상 반환한다고 해서 불투명 리턴 타입(opaque return type) 에서 제네릭을 사용하는 것을 방지하지 않습니다. 아래 예제는 리턴되는 값의 기본 타입에 타입 파라미터를 통합하는 함수입니다.

func `repeat`<T: Shape>(shape: T, count: Int) -> some Collection {
    return Array<T>(repeating: shape, count: count)
}

이 경우에 리턴 값의 기본 타입은 T 에 따라 달라집니다. 어떤 Shape를 통과시키든, repeat(shape:count:)는 해당 shape의 배열을 생성하고 리턴합니다.

그럼에도 불구하고 리턴 값은 항상 [T] 의 기본 타입과 같습니다. 이에 따라 "불투명 리턴 타입(opaque return type)이 있는 함수는 반드시 단일 타입을 리턴해야 한다"는 요구사항을 따릅니다. 

 

 

 

 

 

Differences Between Opaque Types and Protocol Types (불투명 타입과 프로토콜 타입의 차이)

"불투명 타입(Opaque Type)을 리턴하는 것"은 "함수의 리턴 타입으로 프로토콜 타입을 사용하는 것"과 매우 유사해 보입니다. 하지만 이 두 종류의 리턴 타입은 타입 identity 를 보전하는지 여부에 있어 다릅니다. 불투명 타입은 하나의 특정 타입을 참조(refer) 합니다. 비록 함수의 호출자가 타입을 볼 수 없을지라도 말입니다.

프로토콜 타입은 프로토콜을 준수하는 모든 타입을 참조할 수 있습니다. 일반적으로 말하면 프로토콜 타입은 저장된 값의 기본 타입에 대해 더 많은 유연성(flexibility) 을 제공합니다. 불투명 타입은 사용자가 해당 기본 타입에 대한 강한 보증을 만들도록 합니다.

 

아래 예제는 리턴 타입으로 불투명 타입 대신 프로토콜 타입을 사용한 flip(_:) 입니다.

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}

protoFlip(_:) 버전은 flip(_:) 과 똑같은 본문을 가집니다. 그리고 항상 같은 타입의 값을 리턴합니다. flip(_:) 과 다르게 protoFlip(_:) 이 리턴하는 값은 항상 동일한 타입을 가질 필요가 없습니다. 단지 Shape 프로토콜을 준수하기만 하면 됩니다. 다른 방식으로, protoFlip(_:) 은 flip(_:) 이 만드는 것보다 호출자(caller) 와 훨씬 더 느슨한  API 계약(contract)를 만듭니다. 여러 타입의 값을 리턴하기 위해 유연성을 가집니다.

 

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }

    return FlippedShape(shape: shape)
}

수정된 버전의 protoFlip(_:) 는 무슨 도형이 들어오는지에 따라 Square 인스턴스나 Flipped 인스턴스를 리턴합니다. 함수에 의해 리턴되는 두 개의 뒤집어진 도형은 아마 완전히 다른 타입을 가질 것입니다.

동일한 모양의 여러 인스턴스를 뒤집을 때, 이 함수의 다른 유효한 버전은 다른 타입의 값을 리턴할 수 있습니다. protoFlip(_:)으로부터의 더 적은 "특정 리턴 타입 정보" 는 "타입 정보에 의존하는 많은 작업을 리턴된 값에 사용할 수 없다는 것"을 의미합니다. 예를 들어 이 함수에 의해 리턴되는 값에 == 연산자를 사용할 수 없습니다.

let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing  // Error

마지막 줄의 에러가 발생하는 데에는 여러가지 이유가 있습니다. 일단은 Shape가 프로토콜 요구사항으로써 == 연산자를 포함하고 있지 않습니다. 만약 == 를 하나 추가하려고 하면, 다음 문제는 == 연산자의 왼쪽과 오른쪽의 타입을 알아야 한다는 것입니다. 이러한 연산자는 보통 "프로토콜을 채택하는 구체적인 타입과 일치하는 Self 타입"을 인수로 가집니다. 하지만 프로토콜에 Self 요구사항을 추가하는 것은 프로토콜을 타입으로 사용할 때 발생하는 형식 삭제(type erasuere)를 허용하지 않습니다.

 

함수에 리턴타입으로써 프로토콜 타입을 사용하는 것은 프로토콜을 준수하는 모든 타입을 리턴하는 유연성(flexibility)를 줍니다. 그러나 이 유연성의 비용은 리턴된 값에 대해 일부 연산을 수행할 수 없다는 것 입니다. 예제는 어떤식으로 == 연산자를 사용할 수 없는지 보여줍니다. 프로토콜 타입을 사용하여 보존되지 않는 특정 타입 정보에 따라 다릅니다.

 

이러한 접근방식의 또 다른 문제는 도현 변형이 중첩(nest)되지 않는다는 것입니다. 뒤집힌 삼각형의 결과는 Shape 타입의 값을 가집니다. protoFlip(_:) 함수는 Shape 프로토콜을 준수하는 타입의 인수를 가집니다.

그러나 프로토콜 타입의 값은 프로토콜을 채택하지 않습니다.  protoFlip(_:) 에 의해 리턴되는 값은 Shape를 채택하지 않습니다. 이는 여러 변형(transformation)을 적용하는 protoFlip(protoFlip(smallTriange)) 같은 코드는 유효하지 않다는 것을 의미합니다. 변형된 도형인 protoFlip(_:) 에 유효한 인수가 아니기 때문입니다.

 

이와 반대로, 불투명 타입은 비존 타입의 identity를 보존합니다. Swift는 asscoiated types 을 추론할 수 있습니다. 프토콜 타입을 리턴 값으로 사용할 수 없는 위치에서 불투명 리턴 값을 사용할 수 있습니다.

 

아래 Container 프로토콜이 있습니다.

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container { }

Container를 함수의 리턴 타입으로 사용할 수 없습니다. 해당 프로토콜이 associatedtype을 가지고 있기 때문입니다. 제네릭 리턴 타입에서 제약조건(constraint)로 사용할 수도 없습니다. 제네릭 타입이 되어야 하는 것을 추론하는 충분한 정보가 함수 본문 밖에 없기 때문입니다.

// Error: Protocol with associated types can't be used as a return type.
func makeProtocolContainer<T>(item: T) -> Container {
    return [item]
}

// Error: Not enough information to infer C.
func makeProtocolContainer<T, C: Container>(item: T) -> C {
    return [item]
}

 

리턴 타입으로써 불투명 타입 some Container를 사용하는 것은 원하는 API 계약을 표현합니다. 함수는 container를 리턴합니다. 하지만 컨테이너의 타입 명시를 거부합니다.

func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Prints "Int"

twelve 의 타입은 Int로 추론됩니다. 이는 타입 추론이 불투명 타입에서 작동한다는 사실을 보여줍니다. makeOpaqueContainer(item:) 의 구현에서 불투명 컨테이너의 기본 타입은 [T] 입니다. 이 경우 T는 Int 입니다. 따라서 리턴 값은 정수 배열이고 Item의 associated type은 Int로 추론됩니다. Container 의 첨자 구문은 Item을 리턴합니다. 이는 twelve 의 타입도 Int 로 추론된다는 것을 말해줍니다.

 

 

 

 

불투명 타입(Opaque Type) 은 리턴 타입 정보를 외부에 숨길 수 있다.

특정 타입이 아닌 특정 프로토콜을 준수하는 타입을 리턴한다.

불투명 타입을 사용하는 함수와 메소드는 반환하는 모든 값의 타입이 같아야 한다.

 

 

SwiftUI 의 기본 View. some 을 사용한 것을 볼 수 있다

SwiftUI의 경우 여러 개의 View를 조합해서 사용한다.

만약 View에 불투명 타입을 사용하지 않는다면 View 마다 타입을 일일이 지정해줘야하는 복잡한 일이 발생한다. 또 body 프로퍼티 내부를 바꿀 때마다 타입을 업데이트 해야한다.

body를 불투명 타입으로 선언하고 개발자는 굳이 타입에 신경쓰지 않고, 컴파일러가 스스로 타입을 결정하도록 한다.

이를 통해 더 쉽게 코드를 짤 수 있다.

 

 

 

불투명 타입(Opaque Type) 이해하는데 쉽지 않았지만 View를 생각하니 조금은 이해가 된다

끛 😊

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함