[Swift] some, any 제대로 구분해서 사용하기 (Opaque and Boxed Protocol Types in Swift)

2025. 2. 26. 15:19Swift, iOS Foundation

오랜만에 Swift 문법 글로 돌아왔습니다!

Swift는 타입을 숨기기 위한 두 가지 흥미로운 방법을 제공하고 있습니다.
불투명한 타입 (Opaque Types)이라고 불리는 some과, 박스형 프로토콜 타입 (Boxed Protocol Types)이라 불리는 any가 그것이죠.

오늘 글에서는 최신 트렌드에 해당하는 이 두 문법 개념 (some과 any)에 대해서 이해해보는 시간을 가져보도록 할게요!
각 문법 개념이 무엇인지 자세하게 설명해보고,
두 개년은 어떤 차이점이 있는지, Swift는 왜 이 문법을 도입하게 되었는지, 그리고 실전에서는 어떻게 활용할 수 있는지까지 함께 살펴보도록 하겠습니다.

 

불투명한 타입 (Opaque Types)

some은 불투명한 타입 (Opaque Type)을 나타내기 위해 Swift 5.1부터 새롭게 추가된 문법입니다.

some은 연산 프로퍼티의 타입이나, 함수의 반환 타입 (return type)으로 사용될 수 있고,
Swift 5.7부터는 함수의 파라미터 타입 (parameter type)으로도 추가로 사용할 수 있게 되었는데요.

어떤 상황에서 some이라는 키워드를 사용해 불투명한 타입 (Opaque type)을 표현해야 하는지 코드를 통해서 알아볼게요!


Animal이라는 프로토콜이 있고,
이 프로토콜을 채택해 자세한 내용을 구현하고 있는 구조체 (struct) Cow와 Chicken이 있다고 생각해봅시다.

protocol Animal { ... }

struct Cow: Animal { ... }
struct Chicken: Animal { ... }


우리는 메서드의 반환타입으로 구체 타입 Cow나 Chicken이 아니라,
프로토콜인 Animal을 반환하도록 설정하고 싶어, Animal 프로토콜을 반환 타입에 코드로 작성해주게 되면 에러를 마주하게 됩니다.

Swift의 메서드 반환 타입으로는 프로토콜 자체를 설정하는 것이 불가능하기 때문이죠.

/// ❌ Error
/// 메서드에는 프로토콜 자체를 직접 반환하는 것이 불가능하다.
func makeAnimal() -> Animal { 
    ...
}


반환 타입이 아니라, 함수의 파라미터 타입으로 프로토콜을 설정하고 싶은 경우도 위와 마찬가지였습니다.

그래서 우리는 이런 상황에서 제네릭 (Generic)을 활용하는 방식으로 프로토콜을 활용할 수 있었는데요.

이런 상황에서는 제네릭을 활용하다 보니 메서드가 불필요하게 길어진다는 문제가 보이고, 
makeAnimal 메서드를 호출하는 쪽에서 구체적인 타입 (여기서는 Chicken이 해당)을 명시해줘야 한다는 불편함이 생기게 됩니다.
-> 즉, 메서드에서 프로토콜 타입을 반환하는 의미가 없어지는 것과 다름이 없는 셈이죠!

/// 메서드의 반환 타입으로 설정하는 경우
func makeAnimal<A: Animal>() -> A {
    return Chicken() as! T
}
let animal: Chicken = makeAnimal()  // Chicken이라는 타입을 명시해야함

/// 메서드의 파라미터 타입으로 설정하는 경우
func feed<A>(_ animal: A) where A: Animal {
    animal.eat()
}


이런 경우 활용할 수 있는 Swift의 문법이 바로 some인 것입니다.

💡 Swift에서는 특정 프로토콜을 준수하는 고정된 (concrete) 타입을 나타내기 위해 some을 사용합니다.

구체 타입이 아니라 프로토콜을 타입으로 설정함으로써,
사용하는 쪽에서는 구체적인 타입은 알 필요가 없으니 숨기는 캡슐화 원칙을 지킬 수 있고 + 동시에 (구체 타입이 컴파일 시점에 결정되기 때문에) 타입 안정성을 높여 사용할 수 있게 되는 거죠.

/// 메서드의 반환 타입으로 설정하는 경우
func makeAnimal() -> some Animal {
    return Chicken()
}

/// 메서드의 파라미터 타입으로 설정하는 경우
func feed(_ animal: some Animal) {
    animal.eat()
}

 

이때 some 키워드를 사용해서 설정한 프로토콜 타입을 불투명한 타입 (Opaque Type)이라고 부르는 것이고,
Opaque Type로 숨겨진 구체화된 타입 (여기서는 Chicken이 해당)을 기초 타입 (Underlying Type)이라고 부릅니다.

그럼 다시 말해 some 키워드에 대해 아래와 같이 정의할 수도 있겠군요.

💡 Swift의 Opaque Type는 특정 프로토콜을 준수하는 고정된 단일 Underlying Type를 숨겨, 캡슐화와 컴파일러의 최적화 + 안정성을 제공합니다.

SwiftUI의 기본 뷰를 정의하는 body 프로퍼티 부분이
some View로 정의되어 있는 것도 이것과 같은 맥락이라고 생각하면 될 것 같습니다!

body에 정의된 some View 코드는 이유가 있었다고 한다.


some은 다시 한번 말하지만, "특정 프로토콜을 준수하는 고정된 타입"을 나타내기 위한 코드입니다.

그러다 보니, 아래와 같이 "타입의 사용이 고정되지 않는 상황"들에 대해서는 some 키워드를 사용하는 것이 적합하지 않습니다.

  • 한 번 정해진 구체 타입과 다른 타입을 대입하려고 하는 경우 (Cow -> Chicken)
  • Collection 타입 (some은 다형성을 제공하지 않습니다.)
  • 하나의 메서드에 서로 다른 구체 타입을 반환하고자 하는 경우

단, 함수의 파라미터 타입에서 some을 사용하는 경우에는
매개변수 호출 범위에 대해서만 타입을 고정하기 때문에 / 각 호출별로 다른 구체 타입을 대입하는 것이 가능하다고 해요!

/// ❌ Error
var animal: some Animal = Cow()
animal = Chicken() 

/// ❌ Error
let animals: [some Animal] = [Cow(), Chicken()]

/// ❌ Error
func makeAnimal() -> some Animal {
    return isCondition ? Cow() : Chicken()
}

/// ✅ OK
feed(Cow())
feed(Chicken())

some을 Array로 사용하는 경우, Collection의 모든 Element는 동일한 타입을 가져야 합니다.

 

 

박스형 프로토콜 타입 (Boxed Protocl Types)

any는 박스형 프로토콜 타입 (Boxed Protocl Type)이라 불리기도 하지만, 
"해당 프로토콜을 준수하는 T 타입이 존재합니다"라는 문장의 의미와 같이 존재 타입 (Existential Type)이라고도 불립니다.

any는 some과 다르게 한 Collection 안에 여러 개의 타입을 담는 것이 가능합니다.


some과 마찬가지로 any는 특정 프로토콜을 준수하는 추상적인 타입을 정의하기 위해 사용됩니다.

단 some과 다른 점이 있다면, 다양한 구체적인 타입을 동시에 다룰 수 있다는 점일 것 같아요!

💡 Swift에서는 특정 프로토콜을 준수하는 임의의 (arbitrary) 타입을 나타내기 위해 any를 사용합니다.


아래 코드만 봐도 보이는 것처럼,
some에서는 제약이었던 한 번 지정된 구체 타입의 값을 변경해 담는 것도,
하나의 Collcetion Type (Array)에 다양한 구체 타입 (Cow와 Chicken)을 담는 것도, 한 메서드의 반환 값으로 다양한 구체 타입을 주는 것도 모두 가능합니다.

/// ✅ OK
var animal: any Animal = Cow()
animal = Chicken() 

/// ✅ OK
let animals: [any Animal] = [Cow(), Chicken()]

/// ✅ OK
func makeAnimal() -> any Animal {
    return isCondition ? Cow() : Chicken()
}


any를 사용하면 해당 프로토콜을 준수하는 다양한 구체 타입을 표현할 수 있습니다.

우리는 이 상황을 구체 타입의 정보가 지워진다고 하여, 타입 소거 (type erasure)이라고 부르죠.

컴파일 시점에 타입이 소거되어 다양한 타입을 사용할 수 있게 되며 -> 다형성이 증가했다고 생각할 수 있지만,
반대로 해당 타입이 런타임에 동적으로 정해지기 때문에 성능적으로는 저하를 일으킬 수 있다는 점도 함께 기억해야 할 것 같아요!

 

 

some, any 정리해봅시다!

자 지금까지 배운 내용 정리해 볼까요?

  • some : Opaque Type 임을 나타내는 키워드 -> 특정 프로토콜을 따르는 고정된 타입임을 나타내기 위함, 성능 최적화의 목적
  • any : Existential Type 임을 나타내는 키워드 -> 특정 프로토콜을 따르는 다양한 타입을 담기 위함, 타입이 소거되며 성능이 떨어질 수 있음.

Capabilities of some and any : some과 any 키워드의 핵심 내용을 비교합니다.


결론!
Apple의 말로는, 기본적으로 some 키워드를 사용하되, 임의의 값 저장이 필요한 상황에서 추가로 any의 사용을 고려하라고 설명합니다.

구체적인 타입 정보를 제거하는 Type Erasure와 
프로토콜의 associated type와 Self 요구사항을 사용할 수 없다는 any의 Semantic Limitation의 비용을 "필요한 경우"에만 지불하라는 것이죠.

마치 상수 let과 변수 var의 사용에 있어,
기본적으로는 let을 사용하되, 값의 변경이 필요한 상황에 대해서만 var의 사용을 고려하는 것처럼요!

이제 some 키워드와 any 키워드를 확실하게 이해하고 사용하실 수 있겠죠?? 오늘 글은 여기까지입니다!

결론! 가능하면 some을 쓰쇼.

 

Reference

 

Documentation

 

docs.swift.org

 

Embrace Swift generics - WWDC22 - Videos - Apple Developer

Generics are a fundamental tool for writing abstract code in Swift. Learn how you can identify opportunities for abstraction as your code...

developer.apple.com

 

Design protocol interfaces in Swift - WWDC22 - Videos - Apple Developer

Learn how you can use Swift 5.7 to design advanced abstractions using protocols. We'll show you how to use existential types, explore how...

developer.apple.com