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

400자
5분

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

이 코드에서는 Shape 프로토콜을 정의하고, Triangle 구조체가 이를 채택하여 구현합니다. draw() 메서드는 삼각형 모양의 ASCII 아트를 생성하여 문자열로 반환하죠.

이제 아래 코드와 같이 제네릭을 사용하여 모양을 수직으로 뒤집는 등의 작업을 구현할 수 있습니다. 그러나 이 접근 방식에는 중요한 제한 사항이 있어요. 뒤집힌 결과는 생성에 사용된 정확한 제네릭 타입을 노출한다는 점입니다.

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

FlippedShape 구조체는 제네릭 타입 T를 사용하여 어떤 Shape 인스턴스라도 받아들일 수 있습니다. 그리고 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())
// *
// **
// ***
// ***
// **
// *
swift

모양 생성에 대한 자세한 정보를 노출하면 전체 반환 타입을 명시해야 하기 때문에 ASCII 아트 모듈의 공개 인터페이스에 포함되지 않아야 할 타입이 누출될 수 있어요. 모듈 내부의 코드는 다양한 방식으로 동일한 모양을 구축할 수 있는데, 모듈 외부에서 모양을 사용하는 다른 코드는 변환 목록에 대한 구현 세부 정보를 고려할 필요가 없습니다.

JoinedShapeFlippedShape와 같은 래퍼 타입은 모듈 사용자와 관련이 없으며, 보이지 않아야 해요. 모듈의 공개 인터페이스는 모양을 결합하고 뒤집는 등의 작업으로 구성되고, 이러한 작업은 다른 Shape 값을 반환합니다.

lecture image

이 다이어그램은 Shape 프로토콜과 이를 채택한 타입들 간의 관계를 보여줍니다. Triangle, FlippedShape, JoinedShape 모두 Shape 프로토콜을 준수하지만, 후자의 두 타입은 제네릭을 사용하여 구현되었죠. 이는 모듈 외부에 불필요한 타입 정보를 노출하는 문제를 야기합니다.

이러한 문제를 해결하기 위해 Swift는 불투명 타입(Opaque Type)을 도입했습니다. 불투명 타입을 사용하면 모듈 경계에서 타입 정보를 숨기고, 공개 인터페이스를 단순화할 수 있어요.