🔥 불투명 타입 반환하기

759자
9분

불투명 타입은 제네릭 타입의 반대라고 생각할 수 있습니다. 제네릭 타입은 함수를 호출하는 코드에서 해당 함수의 매개변수와 반환 값에 대한 타입을 함수 구현과는 독립적으로 타입을 선택할 수 있게 해줍니다. 예를 들어, 다음 코드의 함수는 호출하는 코드에 따라 달라지는 타입을 반환합니다.

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

max(_:_:)를 호출하는 코드는 xy의 값을 선택하고, 이 값들의 타입이 T의 구체적인 타입을 결정합니다. 호출 코드는 Comparable 프로토콜을 준수하는 모든 타입을 사용할 수 있어요. 함수 내부의 코드는 호출자가 제공하는 모든 타입을 처리할 수 있도록 일반적인 방식으로 작성됩니다. max(_:_:)의 구현은 모든 Comparable 타입이 공유하는 기능만을 사용하죠.

불투명 반환 타입을 가진 함수에서는 이러한 역할이 반대입니다. 불투명 타입을 사용하면 함수 구현에서 반환하는 값의 타입을 함수를 호출하는 코드와는 독립적으로 반환 타입을 선택할 수 있게 해줍니다. 예를 들어, 다음 예제의 함수는 사다리꼴을 반환하지만 해당 모양의 기본 타입을 노출하지 않아요.

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

이 예제의 makeTrapezoid() 함수는 반환 타입을 some Shape로 선언합니다. 그 결과, 함수는 Shape 프로토콜을 준수하는 어떤 특정 구체 타입을 지정하지 않고, 주어진 타입의 값을 반환하게 되죠. 이렇게 makeTrapezoid()를 작성하면 반환하는 값이 도형이라는 공개 인터페이스의 근본적인 측면을 표현할 수 있습니다. 이때 도형을 구성하는 특정 타입을 공개 인터페이스의 일부로 만들 필요가 없어요. 이 구현에서는 두 개의 삼각형과 정사각형을 사용하지만, 반환 타입을 변경하지 않고도 다양한 다른 방식으로 사다리꼴을 그리도록 함수를 다시 작성할 수 있습니다.

이 예제는 불투명 반환 타입이 제네릭 타입의 반대와 같은 방식임을 보여줍니다. makeTrapezoid() 내부의 코드는 제네릭 함수에 대해 호출 코드가 하는 것처럼, Shape 프로토콜을 준수하는 한 필요한 모든 타입을 반환할 수 있어요. 함수를 호출하는 코드는 제네릭 함수의 구현과 같이 일반적인 방식으로 작성되어야 하므로 makeTrapezoid()에서 반환되는 모든 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())
// *
// **
// ***
// ***
// **
// *
 
swift

이 예제에서 opaqueJoinedTriangles의 값은 이 장의 앞부분에 있는 불투명 타입이 해결하는 문제 섹션의 제네릭 예제에서 joinedTriangles와 동일합니다. 그러나 해당 예제의 값과 달리 flip(_:)join(_:_:)은 제네릭 도형 연산이 반환하는 기본 타입을 불투명 반환 타입으로 래핑하여 해당 타입이 보이지 않도록 합니다. 두 함수 모두 의존하는 타입이 제네릭이기 때문에 제네릭이며, 함수에 대한 타입 매개변수는 FlippedShapeJoinedShape에 필요한 타입 정보를 전달하죠.

불투명 반환 타입을 가진 함수가 여러 곳에서 반환하는 경우, 가능한 모든 반환 값은 동일한 타입을 가져야 합니다. 제네릭 함수의 경우 해당 반환 타입은 함수의 제네릭 타입 매개변수를 사용할 수 있지만, 여전히 단일 타입이어야 해요. 예를 들어, 다음은 정사각형에 대한 특별한 경우를 포함하는 도형 뒤집기 함수의 invalid 버전입니다.

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
}
 
swift

이 함수를 Square로 호출하면 Square를 반환하고, 그렇지 않으면 FlippedShape를 반환합니다. 이는 하나의 타입만 반환해야 한다는 요구사항을 위반하고 invalidFlip(_:)을 유효하지 않은 코드로 만들어요. invalidFlip(_:)을 수정하는 한 가지 방법은 정사각형에 대한 특별한 경우를 FlippedShape의 구현으로 옮기는 것인데, 이렇게 하면 이 함수가 항상 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")
    }
}
 
swift

항상 단일 타입을 반환해야 한다는 요구사항은 불투명 반환 타입에서 제네릭을 사용하는 것을 막지 않아요. 다음은 반환하는 값의 기본 타입에 타입 매개변수를 통합하는 함수의 예입니다.

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

이 경우 반환 값의 기본 타입은 T에 따라 달라집니다. 어떤 도형이 전달되든 repeat(shape:count:)는 해당 도형의 배열을 생성하고 반환하죠. 그럼에도 불구하고 반환 값은 항상 [T]라는 동일한 기본 타입을 가지므로, 불투명 반환 타입을 가진 함수는 단일 타입의 값만 반환해야 한다는 요구사항을 따릅니다.

lecture image

이 다이어그램은 제네릭 함수와 불투명 반환 타입을 가진 함수의 차이점을 보여줍니다. 제네릭 함수에서는 호출하는 코드가 타입을 선택하는 반면, 불투명 반환 타입을 가진 함수에서는 구현이 타입을 선택하죠. 이를 통해 함수의 공개 인터페이스를 단순화하고 내부 구현 세부 사항을 숨길 수 있습니다.