티스토리 뷰

반응형

이번 글에서는 Swift 공식 문서의 23번째 단원인 Opaque Type을 읽고 정리한 글을 쓰려고 한다.

Apple Swift Document 23단원 - Opaque Types

Opaque Types

Opaque는 불투명체라는 뜻을 가진 단어이다. Opaque 반환 타입이 있는 함수 또는 메서드는 반환 값의 타입 정보를 숨긴다. 함수의 반환 타입으로 구체적인 타입을 제공하지 않고 지원되는 프로토콜 측면에서 설명된다. 반환 값의 타입이 비공개로 유지될 수 있기 때문에 모듈과 모듈을 호출하는 코드 사이의 경계에서 타입 정보를 숨기는 것이 유용하다. 타입이 프로토콜 타입을 반환하는 것과 달리 Opaque 타입은 타입 ID를 유지한다. 하지만 컴파일러는 타입 정보에 접근할 수 있지만 모듈의 클라이언트는 그럴 수 없다.


The Problem That Opaque Types Solve

예를 들어 ASCII Art Shape를 그리는 모듈을 작성한다고 생각해보자. ASCII Art Shape의 기본 특성는 shape 프로토콜의 요구사항으로 사용할 수 있는 해당 shape의 문자열 표현을 반환하는 draw()함수이다.

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())
// *
// **
// ***

위의 코드는 Int 타입 프로퍼티의 값에 따라 그림을 그려주는 코드이다. 이 코드를 저번에 배운 제네릭을 사용하여 뒤집어진 모양을 만들어보자.

 

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())
// ***
// **
// *

위와 같이 제네릭을 사용하여 모양을 수직으로 뒤집는 것과 같은 작업을 구현할 수 있다. 하지만 이 방법은 모양을 생성하는 데 사용된 제네릭 타입을 노출하게 된다. 이번 장에서는 이러한 노출을 막기 위한 Opaque 타입을 배우는 장이기 때문에 이를 숨겨보자.

 

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())
// *
// **
// ***
// ***
// **
// *

위의 코드는 두 개의 모양을 만들고 이를 합친 것을 반환하는 코드이다. 위의 코드로 새로 생성된 타입은 JoinedShape <FlippedShape <Triangle>, Triangle>타입이다. 위와 같이 코드를 바꾸면 타입이 노출되지 않고 모듈 자체는 동일한 동작을 하게 된다. 모듈 외부의 다른 코드는 변환 목록에 대한 세부 정보를 고려할 필요가 없다. 즉 JoinedShape,FlippedShape와 같은 래퍼 타입은 모듈을 사용하는데 중요하지 않으며 표시되지 않아야 한다. 모듈의 공용 인터페이스는 모양 결합 및 뒤집기와 같은 작업으로 구성되며 이러한 작업은 다른 Shape값을 반환한다.


Returning an Opaque Type

Opaque 타입은 제네릭 타입의 반대처럼 생각할 수 있다. 제네릭 타입을 사용하면 함수를 호출하는 코드에서 해당 함수의 매개변수에 대한 타입을 선택하고 사용한다.

 

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

위의 코드는 호출자에 따라 반환 값의 타입이 달라진다. max(_: _:)를 호출하는 코드는 x,y값을 선택하고 이 값의 타입에 따라 T라는 타입 매개변수의 구체적인 타입이 결정된다. 이때 TComparable 프로토콜을 준수하는 모든 타입을 사용할 수 있다.

하지만 Opaque 타입을 사용하면 달라진다. 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())
// *
// **
// **
// **
// **
// *

위의 코드처럼 함수는 해당 Shape의 타입을 노출하지 않고 사다리꼴을 반환한다. makeTrapezoid() 함수에서 반환 타입을 some 키워드를 붙인 some Shape로 선언한 것을 볼 수 있다. 즉 함수는 구체적인 타입을 지정하지 않았고 Shape 프로토콜을 준수하는 특정 타입의 값만 반환하면 된다. 이러한 방식으로 정의된 makeTrapezoid()는 반환되는 값의 타입을 정의하지 않고도 값을 반환할 수 있다.

즉 제네릭 타입을 사용할 때와 반대로 작동한다는 것이다. Opaque 타입을 사용한 makeTrapezoid()함수는 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())
// *
// **
// ***
// ***
// **
// *

위의 코드처럼 Opaque 반환 타입을 제네릭과 결합할 수도 있다. 위의 코드의 함수는 Shape 프로토콜을 준수하는 타입의 값을 반환한다.
위의 코드에서 opaqueJoinedTriangles의 값은 아까 본 joinedTriangles와 동일하다. 하지만 joinedTriangles와 달리 opaqueJoinedTriangles에서 사용하는 flip(_ :),join(_ :)은 반환하는 타입을 Opaque 타입으로 래핑 하여 타입이 표시되지 않도록 한다. 두 함수의 타입 매개변수가 FlippedShape,JoinedShape에 필요한 타입 정보를 전달하기 때문에 두 함수는 모두 제네릭이다.

 

Opaque 타입을 반환하는 함수가 여러 위치에서 반환되는 경우 가능하다면 모든 반환 값은 동일한 타입을 가져야 한다. 제네릭 함수의 경우 해당 반환 타입은 함수의 타입 매개변수를 사용하지만 하나로 통일되어 있다.

 

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
}

위의 코드처럼 제네릭 함수는 shape 매개변수에 들어가는 타입들이 모두 동일하다. 따라서 항상 같은 타입의 값을 반환할 수 있다.

 

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 반환 타입에 제네릭을 사용하지 못하는 것은 아니다. 위의 코드는 반환 값이 타입 매개변수에 영향을 받도록 만든 함수이다. 이 경우 반환 값의 타입은 타입 매개 변수인 T에 따라 다르다. 전달되는 모양이 무엇이든 repeat(shape:count:)는 해당 모양의 배열을 만들고 반환한다. 그렇지만 반환 값은 항상 동일한 타입인 [T]를 가지므로 Opaque 타입을 반환하는 함수는 단일 타입을 반환해야 한다는 요구사항을 따르게 된다.


Differences Between Opaque Types and Protocol Types

Opaque 타입을 반환하는 것은 프로토콜 타입을 반환하는 것과 비슷하지만 타입 ID를 유지하는지 여부가 다르다. Opaque 타입은 하나의 특정 타입을 참조하지만 함수 호출자는 어떤 타입도 볼 수 없다. 프로토콜 타입은 프로토콜을 준수하는 모든 타입을 참조할 수 있다. 따라서 프로토콜 타입이 더 많은 유연성을 제공하게 되며 Opaque 타입을 사용하면 기본 타입에 대해 더 강력한 보증을 할 수 있다.

 

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

위의 코드는 프로토콜 타입을 반환하는 protoFlip(_ :)이다. 이 함수는 아까 만든 flip(_ :)함수와 동일한 본문을 가진다. flip(_ :)함수는 항상 동일한 타입을 반환했지만 protoFlip(_ :) 함수는 Shape 프로토콜을 준수하기만 하면 된다. 즉 protoFlip(_ :) 함수의 유연성이 더 크다는 말이다.

 

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

    return FlippedShape(shape: shape)
}

 

위의 코드는 아까 만든 protoFlip(_ :)를 수정한 것으로 매개변수로 전달되는 값에 따라 Square의 인스턴스 또는 FlippedShape의 인스턴스를 반환한다. 이렇게 반환된 두 개의 값은 완전히 다른 타입을 가질 수 있다.

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

다른 타입을 반환할 수 있기 때문에 위와 같이 ==연산자를 사용하면 에러를 발생할 수 있다. 에러의 발생 이유를 자세히 살펴보면 우선 Shape 프로토콜의 요구 사항의 일부로 ==연산자가 포함되지 않았다. 또한 ==연산자는 비교하는 인수들의 타입을 알아야 한다는 점이다.

 

프로토콜 타입을 반환 타입으로 사용하면 프로토콜을 준수하는 모든 타입을 유연하게 반환할 수 있다. 아지만 이러한 유연함의 이면에는 반환된 값에 대해 일부 작업을 수행할 수 없다는 점이 있다. 수행할 수 없는 일부 작업의 예로는 위의 코드처럼 ==연산자를 사용할 수 없는 것을 볼 수 있다.

 

이 방식의 또 다른 문제는 모양 변형이 중첩되지 않는다는 것이다. 삼각형을 뒤집은 모양은 Shape 타입이고 protoFlip(_ :) 함수는 Shape 프로토콜을 준수하는 타입을 매개변수로 사용한다. 하지만 프로토콜 타입의 값은 해당 프로토콜을 따르지 않는다. 즉 protoFlip(_ :) 함수는 Shape 프로토콜을 준수하지 않는다. 따라서 여러 변형을 적용하는 protoFlip(protoFlip(smallTriangle))과 같은 코드는 뒤집은 삼각형이 protoFlip(_ :)에 유효한 매개변수가 아니기 때문에 사용할 수 없다.

 

이와 반대로 Opaque 타입은 기본 타입의 ID를 유지한다. Swift는 연관된 타입을 추론할 수 있기 때문에 프로토콜 타입을 반환 값으로 사용할 수 없는 위치에서 Opaque 타입을 반환값으로 사용할 수 있다.

 

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

위의 코드는 Container 프로토콜의 제네릭 버전이다. 이를 활용해 몇 가지 예를 살펴보자.

// 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]
}

위의 프로토콜에는 연관된 타입이 있기 때문에 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"

Opaque 타입인 some Container를 반환 타입으로 사용하면 이를 수행할 수 있다. 함수는 Container를 반환하지만 Container 타입 지정을 거부한다. 실제 사용한 코드를 보면 makeOpaqueContainer(item:)의 구현에서 Opaque Container의 기본 타입은 [T]이다. 위의 코드에서는 TInt이므로 반환 값은 Int Array이고 ItemInt로 추론된다. Container의 서브 스크립트는 Item을 반환하는데 이 때도 Int로 추론되는 것을 볼 수 있다.

 

 

 

음... 이 개념은 처음 봐서 이해하기 어려운 것 같다. 글도 조금 이해하지 못하게 쓴 것같아 수정이 필요할 것 같다. 몇 번을 반복해서 읽어 이해를 한 뒤 수정을 해보도록 해야겠다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/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
글 보관함