GGURUPiOS

Swift 공식 문서 정리 - (24) Opaque Types 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (24) Opaque Types

꾸럽 2023. 5. 14. 15:45

Opaque Types ( 불투명 타입? )

불투명한 반환 타입이 있는 함수 메소드는 반환 값의 타입 정보를 숨김

함수의 반환 타입으로 구체적인 타입을 제공하는 대신 반환 값은 지원하는 프로토콜 측면에서 설명됨

반환 값의 기본형식이 비공개로 유지될 수 있기 때문에 형식 정보를 숨기는 것은 모듈과 모듈을 호출하는 코드 사이에 경계에서 유용함

타입이 프로토콜 유형인 값을 반환하는 것과 달리 불투명 타입은 타입 ID를 유지함

컴파일러는 타입정보에 액세스 할 수 있지만 모듈의 클라이언트는 액세스 불가

불투명 타입이 해결하는 문제

ASCII 아트 도형을 그리는 모듈을 작성한다고 가정, ASCII 아트 쉐잎의 기본 특성은 해당 쉐잎의 문자열 표현을 반환하는 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())
// ***
// **
// *

아래 코드와 같이 두 도형을 수직으로 결합하는 결합된 도형 T, U 구조를 정의하는 이 접근 방식은 결합된 도형 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 아트 모듈의 공용 인터페이스에 포함되지 않은 타입이 전체 반환 타입을 설명해야 하기 때문에 새어나올 수 있음

모듈 내부의 코드는 다양한 방식으로 동일한 모양을 만들 수 있으며, 이 모양을 사용하는 모듈 외부의 다른 코드는 변환 목록에 대한 구현 세부사항을 고려할 필요가 없음

JoinedShape 및, FlippedShape와 같은 래퍼 유형은 모듈 사용자에게 중요하지 않으며 보이지 않아야 함

모듈의 공용 인터페이스는 쉐입 결합 및 플립과 같은 작업으로 구성되며, 이러한 작업은 다른 쉐입값을 반환 함

→ 아직은 이해가 잘 안간다.. 지금까지 이해한 내용

즉 모양 생성에 필요한 코드는 다양한 방식으로 동일한 모양을 만들 수 있으므로 ( 내부에서 대응하므로 ) → 외부에 다른 코드는 변환 목록에 대한 세부사항을 고려할 필요가 없다

→ 즉 래퍼유형의 무엇인지는 모듈 사용자에게는 중요하지 않고 보일 필요가 없음

→ 모듈관리, 모듈간의 의존성을 최소화 할 수 있음

→ 또한, 구현 세부 정보에 대한 변경사항은 모듈 외부에 영향을 미치지 않음 → 코드 유지 보수성 향상


불투명 타입 반환

제네릭 타입과 반대되는 불투명 타입을 생각할 수 있음

제네릭 타입을 사용하면 함수를 호출하는 코드가 함수의 파라미터 타입을 선택하고

함수 구현에서 추상화된 방식으로 값을 반환 할 수 있음

예를 들어, 다음 코드의 함수는 호출자에 종속된 타입을 반환함

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

max(:,:)를 호출하는 코드는 x,y 의 값을 선택하고, 이러한 값의 타입은 T의 구체적인 타입을 결정함

호출 코드는 비교 가능한 프로토콜을 준수하는 모든 타입을 사용할 수 있음

함수 내부의 코드는 일반적인 방식으로 작성되어 발신자가 제공하는 모든 타입을 처리함

max의 구현은 모든 비교 가능한 타입이 공유하는 기능만 사용함

이러한 역할은 반환 타입이 불투명한 함수에 대해 반대로 적용됨

불투명 타입을 사용하면 함수를 호출하는 코드에서 추상화되는 방식으로 함수 구현에서 반환되는 값의 타입을 선택할 수 있음.

다음 예제의 함수는 해당 도형의 기본유형을 노출하지 않고 사다리꼴을 반환 함

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() 함수는 반환 타입을 일부 Shape로 선언함.

그 결과 함수는 특정 구체적인 타입을 지정하지 않고 Shape 프로토콜을 준수하는 some 타입을 반환 함

이런식으로 쓰면 공용 인터페이스의 기본적인 측면인 반환 값을 공용 인터페이스의 일부로 모양을 만드는 특정 타입을 만들지 않고도 표현할 수 있음.

이 예에서는 불투명 반환 타입이 일반 타입의 반대와 같은 방식을 강조함

makeTrapezoid() 내부의 코드는 호출 코드가 제네릭 함수에 대해 수행하는 것처럼

Shape 프로토콜을 준수하는 한 필요한 모든 타입을 반환 할 수 있음

함수가 호출하는 코드는 제네릭 함수의 구현과 같이 일반적인 방식으로 작성되어야 하므로

makeTraffesoid()에서 반환되는 모든 Shape 값과 함께 작동이 가능함

불투명 반환 타입을 제네릭과 결합할 수도 있음

아래 코드의 함수는 모드 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 의 값은 앞부분에 있는 제네릭 예제의 joinedTriangles 와 같음

그러나 앞부분 예제의 값과 달리 flip 및 join 은 일반 Shape 작업이 반환하는 기본 타입을 불투명 반환 타입으로 래핑하므로 해당 타입이 표시되지 않음

두 함수 모두 종속된 타입이 제네릭이기 때문에 함수에 대한 타입 파라미터는 FlippedShape 및 JoinedShape 에 필요한 타입 정보를 전달 함

반환 타입이 불투명한 함수가 여러위치에서 반환되는 경우 가능한 모든 반환 값의 타입이 동일해야함

제네릭 함수의 경우 해당 반환 타입은 함수의 일반 타입 파라미터를 사용할 수 있지만 단일 형식이어야 함

예를들어, 아래는 잘못된 버전의 모양 뒤집기 함수임

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(사각형)을 사용하여 함수를 호출하면 사각형이 반환되고, 그렇지 않으면 플립된 모양이 반환됨

이는 한가지 타입의 값만 반환해야 하는 요구사항을 위반하며 잘못된 코드로 만듬.

invalidFilp(_:)을 수정하는 한 가지 방법은 사각형에 대한 특수 구현을 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에 따라 달라짐. 반환되는 shape 에 관계없이 repeat(shape:count:)는 해당 shape의 배열을 만들고 반환함

그럼에도 불구하고 반환 값은 항상 기본 타입이 동일한 [T]이므로 불투명 반환 타입이 있는 함수는 단일 타입의 값만 반환해야 한다는 요구사항을 따름

불투명 타입과 프로토콜 타입의 차이점

불투명 타입을 반환하는 것은 프토코로 타입을 함수의 반환 타입으로 사용하는 것과 매우 유사 하지만 이 두 가지 타입의 반환 타입은 타입 아이덴티티를 보존하는지 여부가 다름

불투명 타입은 특정 타입을 가리키지만 함수 호출자는 볼 수 없다

프로토콜 타입은 프로토콜을 준수하는 모든 타입을 가리킬 수 있음

일반적으로,

프로토콜 타입을 사용하면 저장하는 기본 값 타입에 대한 유연성이 향상되고

불투명 타입을 사용하면 이러한 기본 타입에 대한 보장을 강화 할 수 있음

→ 프로토콜 타입은 유연하고, 불투명 타입은 모듈성이 강화 된다는 의미인듯함.

예를들어 불투명 반환 타입 대신 프로토콜 타입을 반환 타입으로 사용하는 flip(_:) 버전은 다음과 같음

func flip<T: Shape>(_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}
// 불투명 타입을 반환 타입으로 사용

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}
// 프로토콜 타입을 반환 타입으로 사용 

protoFilp은 flip과 동일한 본문을 가지며 항상 동일한 타입의 값을 반환함.

flip과 달리 protoFilp 이 반환하는 값은 항상 동일한 타입을 가질 필요는 없음

Shape 프로토콜을 준수하면 됨

즉, protoFlip은 호출자와 flip보다 훨씬 느슨한 API 계약을 맺음

→ 여러 타입의 값을 반환할 수 있는 유연성 확보

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

    return FlippedShape(shape: shape)
}

위의 수정된 버전은 전달된 모양에 따라 Square 또는 FlippedShaped의 인스턴스를 반환함

이 함수에 의해 반한된 두 개의 뒤집힌 모양은 완전히 다른 타입을 가질 수 있음

이 함수의 다른 유효한 버전은 동일한 모양의 여러 인스턴스를 플립할 때 다른 타입의 값을 반환할 수 있음

protoFlip의 반환 형식 정보가 구체적이지 않다는 것은 반환된 값에 대해 형식 정보에 의존하는 많은 작업을 사용할 수 없음을 의미함

예를들어, 이 함수에서 반환된 결과를 비교하는 == 연산자는 작성할 수 없음

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

→ 즉, 함수의 반환 형식으로 프로토콜 타입을 사용하면 유연해지지만, 반환된 값에 대해 일부 작업을 수행할 수 없음 ( 위의 예에서는 == 연산자를 사용할 수없음 )

또 다른 문제는 shape 변환이 중첩되지 않는다는 것

삼각형 flip의 결과는 Shape타입의 값이며, protoFlip 함수는 프로토콜을 준수하는 일부 타입의 인자를 사용함. 그러나 프로토로 타입의 값은 해당 프로토콜과 일치하지 않음

protoFlip에서 반환된 값음 Shape와 일치하지 않음

이는 뒤집힌 모양이 protoFlip에 대해 유효한 인자가 아니기 때문에 여러변환을 적용하는 protoFlip과 같은 코드가 올바르지 않음을 의미

반대로 불투명 타입은 기본 타입의 아이덴티티를 유지함.

스위프트는 관련 타입을 추론할 수 있으므로 프로토콜 타입을 반환 값으로 사용할 수 없는 위치에서 불투명 반환 값을 사용할 수 있음

예를들어 Generics의 Container 프로토콜 버전 ( 제네릭 챕터의 Container )

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

해당 프로토콜에 associatedtype 이 있기 때문에 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를 반환 타입으로 사용하면 원하는 API 가 표현됨.

함수는 컨테이너를 반환하지만 컨테이너 타입 지정을 거부함

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

12의 타입은 Int로 추론되는데, 이는 타입 추론이 불투명 타입에서 동작한다는 사실을 보여줌

makeOpaqueContainer 구현에서 불투명 컨테이너의 기본 타입은 [T]임. 이 경우 T는 Int이므로

반환값은 정수 배열이고 Item 관련 유형은 Int로 유추 됨. 컨테이너의 subscript가 Item을 반환함

즉 12의 유형도 Int로 유추됨