[Swift] some과 any는 무엇일까?

이정훈·2025년 10월 30일

Swift 파헤치기

목록 보기
13/13
post-thumbnail

다형성

다형성(Polymorphism)을 간단하게 정의하자면,

하나의 타입(인터페이스)으로 여러 구체 타입을 다룰 수 있는 능력.

위와 같이 정의할 수 있다.

Swift에서 여러 방법으로 다형성을 구현할 수 있는데, 첫 번째는 추상 클래스를 이용하는 방법이다.

// 추상 클래스
class Animal {
    func sound() { fatalError("Subclass must implement sound()") }
} 

// 구체 클래스: Dog
class Dog: Animal {
    override func sound() {
        print("🐶 멍멍")
    }
}

// 구체 클래스: Cat
class Cat: Animal {
    override func sound() {
        print("🐱 야옹")
    }
}

DogCat과 같은 구현체에서 각자의 방식대로 sound()라는 메서드를 정의하고 있다.

func makeSound(animal: Dog) {
	animal.sound()
}

func makeSound(animal: Cat) {
	animal.sound()
}

제네릭

추상 클래스를 사용했기 때문에 우리는 위와 같은 코드 대신, 아래와 같이 제네릭을 사용하여 하나의 함수로 여러 기능의 구현이 가능해졌다.

func makeSound<T>(animal: T) where T: Animal {
	animal.sound()
}

하지만 위와 같은 예시에서 추상 클래스는 구체 타입에서 sound()라는 메서드를 재정의 했는지 실행 전까지 알 수 없는 즉, 컴파일 타임에 에러를 알 수 없다는 단점이 있다.

또한 제네릭 타입을 사용 했을 때, 제약 사항이 하나, 둘 늘어나면 코드 가독성이 떨어진다는 단점도 존재한다.

protocol

protocol Animal {
    func sound()
}

struct Dog: Animal {
    func sound() {
        print("🐶 멍멍")
    }
}

struct Cat: Animal {
    func sound() {
        print("🐱 야옹")
    }
}

Swift에서는 이러한 추상 클래스 대신 인터페이스를 활용한 다형성 구현을 지향하고 있다. Swift에서는 protocol을 활용하여 다형성을 구현할 수 있으며, 제네릭 타입의 코드를 리팩토링하면 위와 같이 protocol을 사용한 코드를 작성할 수 있다.

some 키워드

func makeSound(animal: some Animal) {
    animal.sound()
}

하지만 위와 같이 protocol을 사용한다면, Swift 5.1 부터 등장한 some 키워드를 사용하여 제네릭 타입 보다 심플하게 코드를 작성할 수 있다.

some 키워드는 함수의 매개변수 타입을 지정할 때 뿐만 아니라, protocol을 사용하여 변수의 타입을 지정할 때도 사용이 가능한데

let animals: [some Animal] = [Dog(), Dog(), Dog()]

위와 같이 Animal protocol을 채택한 어떤 타입의 인스턴스라도 올 수가 있다.

var animals: [some Animal] = [Dog(), Dog(), Dog(), Cat()]    // Cannot convert value of type 'Cat' to expected element type 'Dog'

func getAnimal(isDog: Bool) -> some Animal {
    return isDog ? Dog() : Cat()    // Result values in '? :' expression have mismatching types 'Dog' and 'Cat'
}

하지만 변수의 타입을 선언할 때 some 키워드는 타입의 범위를 한정 시킨다는 특성이 있기 때문에 위와 같이 Dog 타입의 인스턴스가 있는 배열에 Cat 타입의 인스턴스를 추가하는 경우, 또는 함수의 반환 타입에 some 키워드를 사용하는 경우 컴파일 에러를 발생시킨다.

var animals: [some Animal] = []    // Underlying type for opaque result type '[some Animal]' could not be inferred from return expression

앞서 언급한 some 키워드의 특성 때문에 위와 같이 빈 값으로 선언하면 에러가 발생한다.

var animals: [some Animal] = [Dog(), Dog()]
animals = []    // ✅ OK

위의 코드와 같이 some Animal의 구체 타입이 이미 [Dog]로 컴파일 시점에 고정되어 있기 때문에, 빈 배열([]) 역시 [Dog] 타입으로 간주되어 타입 일치가 보장된다. 따라서 some 키워드가 사용되었더라도, 해당 타입이 한 번 구체화된 이후에는 동일한 타입의 빈 컬렉션으로 재할당이 가능**하다.

var animal: (some Animal)? = nil    // Underlying type for opaque result type '(some Animal)?' could not be inferred from return expression

some 키워드와 옵셔널을 함께 사용할 때 nil을 직접 할당하면, 컴파일러는 (some Animal)?구체적인 타입(underlying type) 을 추론할 수 없기 때문에 컴파일 오류가 발생한다. 즉, some으로 지정된 불투명 타입의 실제 구현 타입이 명확하지 않아 nil과의 호환성을 판단할 수 없는 것이다.

any 키워드

앞서 언급한 some 키워드의 단점을 해결하기 위해 Swift 5.6에서 any 키워드가 등장했다.

any 키워드는 마치 대상을 박스에 담는 것과 같이 비유할 수 있는데 즉, any 키워드를 사용하면 프로토콜을 준수하는 구체적인 타입의 정보를 컴파일 시점에 고정하지 않고, 런타임에 추상적인 프로토콜 타입으로 다룰 수 있게 된다.

var animals: [any Animal] = [Dog(), Dog(), Dog(), Cat(), Cat()]

func getAnimal(isDog: Bool) -> any Animal {
    return isDog ? Dog() : Cat()
}

이때 any는 내부적으로 타입 이레이저(type erasure) 의 역할을 수행한다. 이는 구체적인 타입 정보를 지워 버리고, 프로토콜이 정의한 인터페이스만 남겨서 다루는 개념이다. 따라서 any를 통해 여러 서로 다른 타입의 인스턴스를 하나의 컬렉션이나 변수에 담아 “프로토콜로서의 공통된 동작” 을 일관성 있게 처리할 수 있다.

예를 들어, any AnimalDog, Cat, Bird 등 어떤 타입이든 Animal 프로토콜을 준수하기만 하면 담을 수 있지만,
이들이 가진 구체적인 타입 정보는 감춰져 있으며, 호출 시에는 Animal 프로토콜이 제공하는 인터페이스만 접근할 수 있다.

이제 위에서 작성한 some 키워드를 any 키워드로 변경하면 위의 코드와 같이 더 유연한 코드를 작성할 수 있다.

var animal: (any Animal)? = nil

뿐만 아니라 some 키워드와 옵셔널을 함께 사용했을 때, nil을 할당할 수 없었던 문제를 any 키워드를 사용하여 쉽게 해결할 수 있게 되었다.

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글