🔥 박스형 타입

724자
8분

박스형 프로토콜 타입은 "실존 타입(existential type)"이라고도 불리는데, "프로토콜을 준수하는 타입 T가 존재한다"는 의미에서 유래했습니다. 박스형 프로토콜 타입을 만들려면 프로토콜 이름 앞에 any를 붙이면 됩니다. 다음은 그 예시입니다.

struct VerticalShapes: Shape {
    var shapes: [any Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}
 
let largeTriangle = Triangle(size: 5)
let largeSquare = Square(size: 5)
let vertical = VerticalShapes(shapes: [largeTriangle, largeSquare])
print(vertical.draw())
swift

위의 예제에서 VerticalShapesshapes의 타입을 [any Shape]로 선언합니다. 이는 박스형 Shape 요소의 배열이죠. 배열의 각 요소는 서로 다른 타입일 수 있으며, 각 타입은 Shape 프로토콜을 준수해야 합니다. Swift는 이런 런타임 유연성을 지원하기 위해 필요할 때 간접 참조 레벨을 추가하는데, 이를 "박스(box)"라고 하며 성능 비용이 발생합니다.

VerticalShapes 내에서는 Shape 프로토콜에서 요구하는 메서드, 속성 및 서브스크립트를 사용할 수 있습니다. 예를 들어, draw() 메서드는 배열의 각 요소에 대해 Shape에서 요구하는 draw() 메서드를 호출합니다. 반면, Shape에서 요구하지 않는 속성이나 메서드에는 접근할 수 없습니다.

shapes에 사용할 수 있는 세 가지 타입을 비교해 보겠습니다:

  1. 제네릭을 사용하여 struct VerticalShapes<S: Shape>var shapes: [S]를 작성하면 요소가 특정 도형 타입인 배열이 만들어지며, 해당 특정 타입의 정체는 배열과 상호 작용하는 모든 코드에 표시됩니다.
  2. 불투명 타입을 사용하여 var shapes: [some Shape]를 작성하면 요소가 특정 도형 타입인 배열이 만들어지지만, 해당 특정 타입의 정체는 숨겨집니다.
  3. 박스형 프로토콜 타입을 사용하여 var shapes: [any Shape]를 작성하면 서로 다른 타입의 요소를 저장할 수 있는 배열이 만들어지며, 해당 타입의 정체는 숨겨집니다.

제네릭을 사용하는 경우:

struct VerticalShapes<S: Shape> {
    var shapes: [S]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}
 
let triangles = VerticalShapes(shapes: [Triangle(size: 3), Triangle(size: 4)])
print(triangles.draw())
swift

이 경우 VerticalShapes는 특정 도형 타입 S로 매개변수화되며, shapes 배열은 해당 타입의 요소만 포함할 수 있습니다. 예를 들어, VerticalShapes<Triangle>Triangle 인스턴스만 저장할 수 있고, draw() 메서드 내에서 S 타입의 모든 속성과 메서드에 접근할 수 있습니다.

불투명 타입을 사용하는 경우:

struct VerticalShapes {
    var shapes: [some Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}
 
let shapes = VerticalShapes(shapes: [Triangle(size: 3), Square(size: 4)])
print(shapes.draw())
swift

이 경우 shapes 배열은 특정 도형 타입의 요소만 포함할 수 있지만, 해당 타입은 외부에서 볼 수 없습니다. draw() 메서드 내에서는 Shape 프로토콜의 요구사항인 draw() 메서드만 접근 가능합니다.

박스형 프로토콜 타입을 사용하는 경우:

struct VerticalShapes {
    var shapes: [any Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}
 
let shapes = VerticalShapes(shapes: [Triangle(size: 3), Square(size: 4), Circle(radius: 5)])
print(shapes.draw())
swift

이 경우 shapes 배열은 Shape 프로토콜을 준수하는 모든 타입의 요소를 포함할 수 있습니다. 예를 들어, Triangle, Square, Circle 등 서로 다른 도형 타입을 함께 저장할 수 있죠. draw() 메서드 내에서는 Shape 프로토콜의 요구사항인 draw() 메서드만 접근할 수 있습니다.

박스형 값의 기본 타입을 알고 있다면 as? 다운캐스트를 사용할 수 있습니다. 예를 들면 다음과 같죠.

if let downcastTriangle = vertical.shapes[0] as? Triangle {
    print(downcastTriangle.size)
}
// "5" 출력
swift

더 자세한 정보는 다운캐스팅을 참조하세요.

불투명 타입(some Shape)을 사용하면 컬렉션에 저장된 요소의 실제 타입 정보가 숨겨지기 때문에 as?를 사용한 다운캐스팅이 불가능하며, Shape 프로토콜에서 정의된 멤버만 접근할 수 있습니다.

반면에 박스형 프로토콜 타입(any Shape)을 사용하면 컬렉션에 저장된 요소의 실제 타입 정보는 런타임에 유지되므로 as?를 사용하여 원래 타입으로 다운캐스팅할 수 있습니다. 다운캐스팅에 성공하면 draw() 메서드뿐만 아니라 해당 타입의 다른 속성이나 메서드에도 접근할 수 있습니다.

struct VerticalShapes {
    var shapes: [any Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}
 
let shapes = VerticalShapes(shapes: [Triangle(size: 3), Square(size: 4), Circle(radius: 5)])
 
if let triangle = shapes.shapes[0] as? Triangle {
    print("Triangle size: \(triangle.size)")
} else {
    print("Not a triangle")
}
swift

하지만 some Shape 컬렉션에서는 이런 다운캐스팅이 불가능해요:

struct VerticalShapes {
    var shapes: [some Shape]
    func draw() -> String {
        return shapes.map { $0.draw() }.joined(separator: "\n\n")
    }
}
 
let shapes = VerticalShapes(shapes: [Triangle(size: 3), Square(size: 4)])
 
if let triangle = shapes.shapes[0] as? Triangle {
    print("Triangle size: \(triangle.size)")
} else {
    print("Not a triangle")
}
// 컴파일 에러: 'some' type cannot be used with 'as?' operator
swift

any 타입과 some 타입은 모두 타입 정보를 숨기지만, any는 런타임에 타입 정보를 유지하여 다운캐스팅을 허용하는 반면, some은 컴파일 타임에 타입 정보를 완전히 숨겨 다운캐스팅을 불가능하게 만듭니다.

선택은 사용 사례에 따라 달라질 수 있습니다. 동일한 타입의 도형만 다루면서 해당 타입의 모든 기능을 사용해야 한다면 제네릭이 적합할 수 있습니다. 특정 도형 타입을 숨기면서 Shape 프로토콜의 기능만 사용하려면 불투명 타입을 선택할 수 있죠. 그리고 다양한 도형 타입을 함께 저장하고 관리해야 한다면 박스형 프로토콜 타입이 최선의 선택이 될 것입니다. 상황에 따라 적절한 방식을 선택하는 것이 중요합니다.