🔥 불투명 타입과 박스형 프로토콜 타입의 차이점 2편

690자
9분

이전에 같이 살펴본 예제가 좀 어려운 것 같아 좀 더 쉬운 예제로 다시 설명 드리겠습니다.

불투명 타입과 박스형 프로토콜 타입은 타입 정보를 숨기는 방식과 타입 캐스팅 가능 여부에 있어 중요한 차이점이 있습니다. 이를 이해하기 위해 간단한 예제를 살펴보겠습니다.

먼저, Animal이라는 프로토콜과 이를 준수하는 DogCat 구조체를 정의해 봅시다.

protocol Animal {
    var name: String { get }
    func makeSound()
}
 
struct Dog: Animal {
    var name: String
    func makeSound() {
        print("멍멍!")
    }
}
 
struct Cat: Animal {
    var name: String
    func makeSound() {
        print("야옹!")
    }
}
swift

불투명 프로토콜 타입 (any)

이제 Animal 프로토콜을 반환 타입으로 사용하는 함수를 작성해 보겠습니다.

func getAnimal(_ type: String) -> any Animal {
    switch type {
    case "dog":
        return Dog(name: "뽀삐")
    case "cat":
        return Cat(name: "나비")
    default:
        fatalError("알 수 없는 동물 타입입니다.")
    }
}
swift

getAnimal(_:) 함수는 any Animal을 반환합니다. 이는 박스형 프로토콜 타입으로, 반환되는 값은 Animal 프로토콜을 준수하는 어떤 타입이든 될 수 있습니다.

이 함수를 호출하면 다음과 같이 Animal 프로토콜의 메서드와 속성에 접근할 수 있습니다.

let animal: any Animal = getAnimal("dog")
print(animal.name) // "뽀삐" 출력
animal.makeSound() // "멍멍!" 출력
swift

하지만 박스형 프로토콜 타입은 타입 정보를 런타임에 숨기므로, 다음과 같이 다운 캐스팅을 시도할 수 있습니다.

if let dog = animal as? Dog {
    print("개입니다!")
} else if let cat = animal as? Cat {
    print("고양이입니다!")
}
swift

이 코드는 animalDog 타입인지 Cat 타입인지를 런타임에 확인합니다. 다운 캐스팅이 성공하면 해당 타입으로 특화된 동작을 수행할 수 있습니다. 그러나 이는 런타임 오류의 가능성을 내포하고 있습니다.

불투명한 타입 (some)

이번에는 some Animal을 반환 타입으로 사용하는 함수를 작성해 보겠습니다.

func getDog() -> some Animal {
    return Dog(name: "뽀삐")
}
 
func getCat() -> some Animal {
    return Cat(name: "나비")
}
swift

getDog() 함수는 항상 Dog 타입을 반환하고, getCat() 함수는 항상 Cat 타입을 반환합니다. 두 함수 모두 some Animal을 반환 타입으로 지정하고 있지만, 각 함수 내부에서 반환되는 값의 타입은 일치합니다.

이 함수들을 호출하면 다음과 같이 사용할 수 있습니다.

let dog: some Animal = getDog()
dog.makeSound() // "멍멍!" 출력
 
let cat: some Animal = getCat()
cat.makeSound() // "야옹!" 출력
swift

dogcat 상수는 모두 some Animal 타입으로 선언되었지만, 실제로는 각각 DogCat 타입의 인스턴스를 가지고 있습니다.

그러나 불투명한 타입은 타입 정보를 컴파일 타임에 완전히 숨기므로, 다음과 같이 다운 캐스팅을 시도하면 컴파일 오류가 발생합니다.

// 컴파일 오류: 'some' 타입은 다운 캐스팅할 수 없습니다.
if let cat = cat as? Cat {
    print("고양이입니다!")
}
swift

불투명한 타입은 강력한 타입 안정성을 제공하지만, 타입 정보를 완전히 숨기므로 다운 캐스팅은 불가능합니다.

불투명한 타입의 제약 조건

불투명한 타입을 사용할 때는 함수 내부의 모든 반환 값이 동일한 타입이어야 합니다. 이는 불투명한 타입의 정의와 밀접한 관련이 있습니다.

불투명한 타입은 실제 타입을 숨기면서도 타입 정보를 보존하는 방법입니다. 함수가 some 키워드를 사용하여 불투명한 타입을 반환하면, 컴파일러는 해당 함수가 항상 동일한 타입을 반환한다는 것을 보장합니다. 이를 통해 컴파일러는 반환된 값의 타입에 대한 정보를 유지할 수 있습니다.

만약 다음과 같이 함수 내부에서 서로 다른 타입을 반환하려고 하면, 컴파일 오류가 발생합니다.

func getAnimal() -> some Animal {
    if Bool.random() {
        return Dog(name: "뽀삐")
    } else {
        return Cat(name: "나비")
    }
}
swift

이 함수는 랜덤하게 Dog 또는 Cat 인스턴스를 반환하려고 합니다. 그러나 이는 불투명한 타입의 규칙을 위반합니다. 컴파일러는 이 함수가 항상 동일한 타입을 반환한다는 것을 보장할 수 없기 때문입니다.

Swift 컴파일러는 이러한 코드를 거부하고, 다음과 같은 오류 메시지를 표시합니다:

Function declares an opaque return type 'some Animal', but the return statements in its body do not have matching underlying types
text

이 오류는 함수가 some Animal을 반환하도록 선언되었지만, 함수 본문의 반환 문이 일치하는 기본 타입을 가지고 있지 않다는 것을 나타냅니다.

불투명한 타입의 이러한 제약 조건은 컴파일 타임에 타입 안정성을 보장하고, 반환된 값에 대해 일관된 타입 정보를 유지할 수 있도록 합니다. 이는 불투명한 타입이 제공하는 강력한 타입 안정성과 캡슐화의 이점을 누리기 위해 필요한 제약 조건입니다.

정리해 보면 아래와 같아요.

  • 박스형 프로토콜 타입 (any)은 런타임에 타입 정보를 숨기므로 다운 캐스팅이 가능하지만, 런타임 오류의 가능성이 있습니다.
  • 불투명한 타입 (some)은 컴파일 타임에 타입 정보를 완전히 숨기므로 다운 캐스팅이 불가능하지만, 강력한 타입 안정성을 제공합니다.
  • 불투명한 타입을 사용할 때는 함수 내부의 모든 반환 값이 동일한 타입이어야 합니다. 이는 컴파일러가 반환 타입에 대한 정보를 유지하기 위한 필수 조건입니다.
  • 즉, 런타임(실행시간)에 타입이 변경될 수 있다면 any를 사용해야 하고 아니라면 some 을 사용하는 것이 좋습니다.