반응형

Swift.org 사이트의 강좌 번역본입니다. 

원문 : 애플 개발 문서 Swift 5.1 Language Guide - Opaque Types

불분명한 타입(Opaque Types)

불분명한(opaque) 반환 타입으로된 함수나 메소드는 반환 값의 타입 정보를 숨깁니다. 함수의 반환 타입에 구체적인 타입을 제공하는 대신에, 해당 반환 값은 지원하는 프로토콜로 설명합니다. 반환 값의 근본적인 타입은 비공개로 유지되기 때문에, 타입 정보를 숨기는 것은 모듈과 모듈을 호출하는 코드 사이의 경계에서 유용합니다. 타입이 프로토콜 타입인 값을 반환하는 것과는 다르게, 불분명한 타입은 타입의 독자성(identity)을 지켜줍니다 - 컴파일러는 타입 정보를 사용할 수 있지만, 모듈의 클라이언트는 사용할 수 없습니다.

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

예를들어, ASCII art shape을 그리는 모듈을 작성한다고 가정해보세요. ASCII art 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())
// *
// **
// ***

 

아래 코드와 같이, 수직으로 도형을 뒤집는것과 같은 작업을 구현하기 위해 제네릭(generics)을 사용할 수 있습니다. 하지만, 이런 접근 방법은 다음과 같은 중요한 제한이 있습니다: 뒤집힌 결과는 그것을 만드는데 사용된 정확한 제네릭 타입을 드러냅니다.

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> 구조체를 정의하는 이러한 접근법은 2개의 도형을 함께 수직으로 결합하고, 뒤집힌 삼각형을 다른 삼각형과 결합해서 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 타입은 모듈의 사용자에게 중요하지 않고, 보이지 않아야 합니다. 해당 모듈의 공개 인터페이스는 도형 결합하기와 뒤집기와 같은 작업으로 구성되고, 해당 작업은 또 다른 Shape값을 반환합니다.

불분명한 타입 반환하기(Returning an Opaque Type)

불분명한(opaque) 타입은 제네릭(generic) 타입과 반대인 것으로 생각할 수 있습니다. 제네릭 타입은 함수를 호출하는 코드가 해당 함수의 매개변수를 고르고 해당 함수 구현에서 벗어나 추상화되는 방법으로 값을 반환합니다. 예를들어, 다음 코드에 있는 함수는 호출자에 따라 타입을 반환 합니다.

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

 

해당 코드는 x와 y에 대한 값을 골라 max(_:_:)를 호출하고, 해당 값의 타입에 의해 구체적인 타입 T가 결정됩니다. 호출하는 코드는 Comparable 프로토콜을 준수하는 모든 타입에 사용할 수 있습니다. 함수 내부의 코드는 일반적인 방법으로 작성되었으므로 호출자가 제공하는 어떤 타입이든 처리할 수 있습니다. max(_:_)의 실행은 모든 Comparable 타입이 공유하는 기능만을 사용할 수 있습니다. 

이러한 규칙들은 불분명한(opaque) 반환 타입으로 된 함수와는 반대입니다. 불분명한 타입은 함수 실행이 함수를 호출하는 코드에서 벗어나서 추상화된 방식으로 반환되는 값의 타입을 선택하게 됩니다. 예를들어, 다음 에제의 함수는 해당 도형을 기반으로 하는 타입을 드러내지 않고 사다리꼴을 반환합니다. 

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()작성하는 것은 도형이 공용 인터페이스의 일부분으로 만들어지는 특정 타입을 만들지 않고, 공개 인터페이스(반환하는 값은 shape입니다)의 근본적인 측면을 표현하게 됩니다. 이러한 구현은 2개의 삼각형과 1개의 사각형을 사용하지만, 해당 함수는 반환 타입을 변경하지 않고 다양한 다른 방법으로 사다리꼴을 그리기 위해 재작성 할 수 있습니다.

이 예제에서 주목해야 하는 것은 불분명한(opaque) 반환 타입이 제네릭(generic) 타입과 반대되는 방법이라는 것입니다. makeTrapezoid() 내부의 코드는 타입이 Shape 프로토콜을 준수하는한, 필요한 모든 타입을 반환할수 있으며, 호출하는 코드가 제네릭(generic) 함수를 수행하는 것과 같습니다. 함수를 호출하는 코드는 제네릭(generic)한 함수의 구현처럼, 제네릭(generic) 방법으로 작성해야하므로, makeTrapezoid()에 의해 반환되는 모든 Shape 값으로 동작할 수 있습니다. 

불분명한(opaque) 반환 타입을 제네릭(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())
// *
// **
// ***
// ***
// **
// *

 

예제에 있는 opaqueJoinedTriangles의 값은 이전 장의 The Problem That Opaque Types Solve섹션에 있는 제네릭(generics) 예제의 joinedTriangles와 같습니다. 하지만 예제에 있는 값과는 다르게, flip(_:)와 join(_:_:)은 제네릭 도형 연산이 반환하는 기본 타입을 불분명한 반환 타입으로 래핑(wrap)하며, 해당 타입이 보여지지 않도록 합니다. 사용하는 타입이 제네릭이기 때문에, 두 함수 모두 제네릭이고, 함수의 타입 매개변수 FlippedShape와 JoinedShap로 필요한 타입 정보를 전달합니다. 

불분명한 반환 타입의 함수가 여러곳에서 반환되는 경우, 가능한 모든 반환 값은 동일한 타입이어야 합니다. 제네릭 함수에 대해서, 해당 반환 타입은 함수의 제네릭 타입 매개변수를 사용할수 있지만, 여전히 하나의 타입이어야 합니다. 예를들어, 다음은 사각형에 대해 특수한 경우를 포함한 도형 뒤집는 함수의 유효하지 않는(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")
    }
}

 

항상 하나의 타입만 반환하는 요구사항은 불분명한 반환 타입에서 제네릭을 사용하는 것을 막지 않습니다. 다음 함수의 예제는 반환하는 값의 타입을 기반으로 타입 매개변수를 통합합니다. 

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

 

이 경우에, 반환 값의 기본 타입은 T입니다; 어떤 도형이 전달되든간에, repeat(shape:count:)를 만들고 도형의 배열을 전달합니다. 그렇지만, 반환 값은 항상 동일한 기본 타입인 [T]이므로, 불분명한 반환 타입은 하나의 타입의 값을 반환해야 한다는 요구사항을 따릅니다.

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

불분명한 타입을 반환하는 것은 프로토콜 타입을 함수의 반환 타입으로 사용하는 것과 매우 비슷해 보이지만, 이러한 2가지 종류의 반환 타입은 타입 식별(identity)을 유지하는지에 대한 차이가 있습니다. 불분명한 타입은 하나의 특정 타입을 나타내고, 함수의 호출자는 어떤 타입인지 알수가 없습니다. 프로토콜 타입은 프로토콜을 준수하는 모든 타입을 참조할 수 있습니다. 일반적으로 말해서, 프로토콜 타입은 저장하는 값의 기본 타입에 대해 더 많은 유연성을 제공하고, 불분명한 타입은 기본 타입에 대해 더 강력하게 보장할 수 있습니다. 

예를들어, 다음은 불분명한 반환 타입을 사용하는 대신에 프로토콜 타입의 값을 반환하는 flip(_:)의 버젼입니다.

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

 

protoFlip(_:)의 버젼은 flips(_:)와 동일한 본문(body)를 가지고, 항상 같은 타입의 값을 반환합니다. flips(_:)와는 다르게, protoFlip(_:) 반환하는 값은 항상 동일한 타입을 요구하지 않습니다 - 이는 Shape 프로토콜을 준수하면 됩니다. 다시 말해서, protoFlip(_:)은 flip(_:) 보다 훨씬 느슨한 API 입니다. 이는 여러개의 타입의 값을 반환하는 유연함을 가집니다.

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

    return FlippedShape(shape: shape)
}

 

수정된 버젼의 코드는 Square의 인스턴스 또는 FlippedShape의 인스턴스를 반환하며, 무슨 도형을 전달했는지에 따라 달라집니다. 이 함수에서 반환된 2개의 뒤집힌 도형은 완전히 다른 타입일수도 있습니다. 같은 도형의 여러개의 인스턴스를 뒤집을때, 이 함수의 다른 유효한 버젼은 다른 타입의 값을 반환할 수 있습니다. protoFlip(_:)로부터 특정 반환 타입 정보가 적은 것은 타입 정보에 의존한 많은 작업이 반환된 값에 사용할 수 없는 것을 의미합니다. 예를들어, 해당 함수에 의해 반환한 결과를 비교하는 == 연산자를 작성할 수 없습니다.

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

몇가지 이유로 예제의 마지막 줄에 오류가 발생합니다. 당면한 문제는 Shape가 프로토콜 요구사항의 일부로 == 연산자를 포함하지 않는 것입니다. 하나를 추가하는 경우, 다음에 직면할 문제는 == 연산자가 자신의 왼쪽과 오른쪽 인자의 타입을 알아야 한다는 것입니다. 이런 종류의 연산자는 보통 Self 타입의 인자를 사용하며, 프로토콜을 채택한 구체적인 타입과 일치하지만, 프로토콜에 Self 요건을 추가하면 프로토콜을 타입으로 사용할때 발생하는 타입 제거가 허용되지 않습니다. 

함수의 반환 타입으로 프로토콜 타입을 사용해서 프로토콜을 준수하는 모든 타입도 유연하게 반환할 수 있습니다. 하지만, 이런 유연함의 비용은 반환된 값에서 일부 작업을 수행할 수 없다는 것입니다. 그 예제는 == 연산자를 사용할 수 없는 방법을 보여줍니다 - 프로토콜 타입을 사용해서 유지되지 않는 특정 타입 정보에 따라 달라집니다. 

이 접근법의 또 다른 문제는 도형 변환이 중첩되지 않는다는 것입니다. 삼각형이 뒤집힌 결과는 Shape 타입의 값이고, protoFlip(_:) 함수는 Shape 프로토콜을 준수하는 타입의 인자를 가집니다. 하지만 프로토콜 타입의 값은 해당 프로토콜을 준수하지 않습니다; protoFlip에 의해 반환된 값은 Shape를 준수하지 않습니다. 이는 뒤집한 도형은 protoFlip(_:)에 유효한 인자가 아니기 때문에, 여러가지 변환을 적용하는 protoFlip(protoFlip(smallTriange)) 와 같은 코드가 유효하지 않다는 것을 의미합니다. 

대조적으로, 불분명한 타입은 기본 타입의 식별자(identify)를 유지합니다. Swift는 관련된(associated) 타입을 추론할 수 있으며, 프로토콜 타입을 반환 값으로 사용할 수 없는 곳에서 불분명한 반환 값을 사용할 수 있습니다. 예를들어, 다음은 Generics Container 프로토콜 버젼입니다.

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

 

프로토콜이 관련된(associated) 타입이 있기 때문에, 함수의 반환 타입으로 Container를 사용할 수 없습니다. 또한 함수 본문(body) 외부에 제네릭 타입이 무엇인지 추론할 정보가 충분하지 않기 때문에, 제약조건(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를 나타냅니다 - 그 함수는 컨테이너를 반환하지만, 컨테이너의 타입을 지정하는 것은 거부합니다.

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) 타입은 Int로 추론됩니다. Container의 첨자(subscript)로 Item을 반환하며, twelve의 타입 또한 Int로 추론되는 것을 의미합니다.

반응형

'Swift > Language Guide' 카테고리의 다른 글

흐름제어(Control Flow)  (0) 2022.06.21
기초(The Basics)  (0) 2022.06.21
동시성(Concurrency)  (0) 2022.06.21
컬렉션 타입(Collection Types)  (0) 2019.03.05
문자열과 문자(Strings and Characters)  (0) 2019.02.28
기본 연산자(Basic Operators)  (0) 2019.02.26
Advanced Operators  (0) 2018.09.18
Access Control  (0) 2018.09.18
Posted by 까칠코더
,