본문 바로가기

TIL

Opaque And Boxe Type (1)

Opaque & Boxed Type의 역할

값의 타입에 대한 자세한 정보를 숨김

→ 반환값의 기본 타입이 비공개로 유지될 수 있기 때문에 모듈과 이를 호출하는 코드 사이에 타입 정보를 숨길 수 있음

값의 타입을 숨길 수 있는 방법

Opaque Type - 불투명한 타입

→ 타입 식별을 유지하기 때문에 컴파일러는 해당 타입의 정보에 접근할 수 있지만, 모듈의 클라이언트(이를 사용하는 곳)에서는 접근할 수 없음

→ 반환하는 함수는 반환값의 타입을 숨기는 대신, 이를 지원하는 Protocol에서 반환값을 설명함

Boxed Protocol Type - 박스형 타입

→ 타입 식별을 유지하지 않기 때문에, 값의 타입을 런타임까진 알 수 없으며 다른 값이 저장됨에 따라 변경될 수 있음

→ 프로토콜을 준수하는 타입의 인스턴스를 저장할 수 있음

Opaque Type이 해결하는 문제

예시 코드 : ASCII 그림을 그리는 모듈

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

 

🚨 문제상황 🚨

 

ASCII 그림을 그리는 모듈을 Generic을 사용하여 Shape을 준수하는 무언가를 뒤집을 수 있지만, Flipped Triangle이 생성될 때 정확한 제네릭 타입(FlippedShape<Triangle>)을 노출함

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

이를 활용하여 Shape(Triangle)과 Shape(Flipped Triangle)을 합치는 결합하는 구조체를 생성 시, 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())
// *
// **
// ***
// ***
// **
// *

이런 경우 Shape에 대한 자세한 내용이 노출되게 되면 전체 반환 타입을 명시해야 됨

→ 이는 모듈의 공개 인터페이스에 포함되지 않은 타입이 유출되는 상황이 발생할 수 있음. 하지만 모듈 내부에서 다양한 방법으로 동일한 결과를 낼 수 있다는 점, 그리고 모듈 밖에선 모듈 내부의 세부 구현 정보를 고려할 필요가 없다는 점에서 래퍼 타입은 모듈의 사용자에게 중요하지 않기때문에 표시되지 않아야 함.

Generic vs. Opaque Type의 차이는?

Generic의 경우

제네릭 타입을 사용하면 함수를 호출하는 코드에서 파라미터의 타입을 선택하고, 함수 구현에서 추상화된 방식으로 값을 반환

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

→ x와 y의 값을 선택하고 이 값의 타입에 따라 T의 구체적인 타입을 결정하며, 이는 Comparable을 준수하는 모든 타입을 사용할 수 있음

Opaque Type의 경우

함수 구현에서 호출 시 반환될 값의 타입을 추상화된 방식으로 선택할 수 있음.

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

→ some Shape을 반환 타입으로 선언하면, Generic과 반대로 구체적인 타입을 지정하지 않고 Shape 프로토콜을 준수하는 특정 타입의 값을 반환함. 이러한 방법은 Shape을 구체적인 타입을 만들지 않고 공용 인터페이스의 한 부분으로 사용할 수 있게 되어 반환 타입을 변경하지 않고 다양한 방법으로 변경할 수 있음

Generic + Opaque Type를 합치면…

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 Type으로 FlippedShape<Triangle>과 JoinedShape<FlippedShape<Triangle>, Triangle> 와 같은 Generic 반환값 타입을 감쌈.

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

→ 반환값의 기본 타입은 T에 따라 결정되며, 항상 동일한 기본 타입 [T]는 Opaque Type로 감싸져있기 때문에 아래의 요구사항을 충족함

 

🚨 주의점 🚨

Opaque Type의 경우 여러곳에서 함수 반환 시 모든 반환값은 동일한 타입이어야 함

Generic의 경우 반환타입은 함수의 Generic 타입의 파라미터를 사용할 수 있지만, 단일 타입이어야 함

< 문제 상황 예시 >

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을 반환하는 예시의 경우

→ 단일 타입의 값을 반환해야한다는 것을 위반

< 해결 방법 예시 >

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")
    }
}

Square의 특수 경우를 FlippedShape로 옮겨서 invalidFlip가 항상 FlippedShape을 반환하도록 수정

 

참고링크

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/opaquetypes/

728x90

'TIL' 카테고리의 다른 글

Opaque And Boxe Type - 이해용 샘플 코드  (0) 2024.01.19
Opaque And Boxe Type (2)  (0) 2024.01.19
UICollectionView  (0) 2023.12.15
MarkDown 접기  (0) 2023.12.11
SOLID  (0) 2023.12.01