Swift는 정적 타입 언어로 컴파일 시점에서 변수의 타입이 결정된다. 하지만 상속, 프로토콜 채택, 그리고 Any/AnyObject와 같이 런타임에 실제 타입 정보가 중요한 상황에서는 타입 캐스팅을 사용한다.
타입 캐스팅은 두 가지 주요 목적이 존재한다.
타입 확인(Type Checking)
객체가 특정 타입의 인스턴스 인지 확인한다.
타입 변환(Type Conversion)
런타임에 객체의 타입을 다른 타입으로 안전하게 변환한다.
Swift의 주요 타입 캐스팅 연산자
Swift에서는 크게 네 가지 연산자를 통해 타입 캐스팅을 수행한다. 각 연산자의 역할과 사용 예제는 아래와 같다.
1. is 연산자
목적: 객체가 특정 타입에 속하는지 확인하기 위해서 사용한다.
if someObject is SomeClass {
print("someObject는 SomeClass의 인스턴스이다.")
}
이 연산자의 검사 결과는 Bool 타입으로 반환되며, 타입 변환은 수행하지 않는다.
2. as 연산자
목적: 컴파일될 때 이미 타입 변환이 보장된 경우에 사용한다. 주로 업캐스팅이나 Swift와 Objective-C 간의 브리징에 사용된다.
※ 브리징은 두 언어가 서로의 코드를 인식하고 사용할 수 있도록 연결해주는 매커니즘을 의미한다.
import Foundation
let nsString: NSString = "Swift"
let swiftString: String = nsString as String
print(type(of: nsString))
print(type(of: swiftString))
// 결과
// NSTaggedPointerString
// String
이 연산자는 런타임 오류 위험 없이 안전하게 변환된다.
3. as? 연산자(조건부 다운캐스팅)
목적: 다운캐스팅(상위 클래스에서 하위 클래스로의 변환)을 시도할 때 사용된다. 변환에 실패하면 nil을 반환하므로 옵셔널 바인딩과 함께 사용한다.
class Animal { }
class Dog: Animal {
func bark() {
print("멍멍")
}
}
let animal: Animal = Dog()
if let dog = animal as? Dog {
dog.bark()
} else {
print("animal은 Dog 타입이 아닙니다.")
}
이 연산자는 안전하게 캐스팅 결과를 옵셔널로 받아 처리할 수 있으므로, 예기치 못한 타입 오류를 방지할 수 있다.
4. as! 연산자(강제 다운캐스팅)
목적: 다운캐스팅을 강제로 수행한다. 만약 캐스팅이 실패한다면 런타임 에러가 발생하게 된다.
let dog = animal as! Dog
dog.bark()
이 연산자는 캐스팅이 확실할 때만 사용해야 하며, 그렇지 않으면 앱의 안정성이 크게 저하될 수 있다.
업캐스팅과 다운 캐스팅
업 캐스팅(Upcasting)
업 캐스팅은 하위 클래스의 인스턴스를 상위 클래스 타입으로 변환하는 것을 의미한다. 업 캐스팅은 암시적으로 이루어지며, 별도의 캐스팅 연산자가 필요없다.
class Animal { }
class Dog: Animal { }
let dog: Dog = Dog()
let animal: Animal = dog // 자동으로 업 캐스팅이 진행된다.
하위 클래스는 상위 클래스의 모든 특성을 포함하므로, 업캐스팅은 언제나 안전하다.
다운 캐스팅(Downcasting)
다운 캐스팅은 상위 클래스 타입의 인스턴스를 하위 클래스 타입으로 변환하는 작업을 의미한다. 런타임에서 객체의 실제 타입을 확인해야 하므로, 항상 안전하지는 않다. 그러므로 다운 캐스팅은 주로 옵셔널 바인딩을 통해 안전하게 처리하는 것이 좋으며, 잘못된 강제 다운캐스팅은 앱 크래시로 이어질 수 있다.
let cat: Animal = Animal()
if let pet = cat as? Dog {
pet.bark()
} else {
print("cat은 Dog 타입이 아닙니다.")
}
옵셔널 바인딩과 가드 구문 활용
타입 캐스팅은 보통 옵셔널 값으로 반환되므로, 이를 안전하게 처리하기 위해 옵셔널 바인딩을 사용한다.
1. if let 사용 예제
func processAnimal(animal: Animal) {
if let dog = animal as? Dog {
dog.bark()
} else {
print("해당 animal은 Dog 타입이 아닙니다.)
}
}
캐스팅이 성공하면 dog 라는 이름으로 다운캐스팅된 객체를 사용할 수 있다.
2. guard let 사용 예제
func makeAnimalBark(animal: Animal) {
guard let dog = animal as? Dog else {
print("해당 animal은 Dog 타입이 아니므로 처리가 불가능합니다.")
}
dog.bark()
}
guard let 을 사용하면 조건이 충족되지 않을 경우 조기에 함수 실행을 종료하여 이후 코드는 반드시 다운 캐스팅이 성공한 객체에 대해서만 실행된다.
프로토콜을 활용한 타입 캐스팅
Swift 에서는 프로토콜 자체도 타입으로 취급된다. 따라서 여러 클래스가 동일한 프로토콜을 채택하는 경우, 공통된 인터페이스를 강제할 수 있다.
protocol Flyable {
func fly()
}
class Bird: Flyable {
func fly() {
print("새가 날아갑니다.")
}
}
class Airplane: Flyable {
func fly() {
print("비행기가 이륙합니다.")
}
}
let objects: [Any] = [Bird(), Airplane(), "Not Flyable"]
for object in objects {
if let flyer = object as? Flyable {
flyer.fly()
} else {
print("이 객체는 Flyable을 준수하지 않습니다.")
}
}
서로 다른 클래스의 인스턴스가 동일한 프로토콜을 채택하고 있다면, 이를 하나의 배열에 담아 공통된 메서드를 호출할 수 있다. 프로토콜 캐스팅은 다양한 타입의 객체를 다룰 때 유용하다.
Any 와 AnyObject를 활용한 타입 캐스팅
Any와 AnyObject
Any는 모든 타입의 값을 포함할 수 있는 타입이다. 기본 데이터 타입, 클래스, 구조체 등 모든 종류의 데이터가 포함된다. 그에 비해 AnyObject는 클래스 타입의 인스턴스만 포함할 수 있다.
Any/AnyObject사용 예제
let mixedArray: [Any] = [Dog(), "Hello", 42, Bird()]
for item in mixedArray {
swift item {
case let dog as Dog:
dog.bark()
case let text as String:
print("문자열: \(text)")
case let number as Int:
print("숫자: \(number)")
case let flyer as Flyable:
flyer.fly()
default:
print("알 수 없는 타입")
}
}
이 예제 코드는 Any 타입의 배열을 순회하면서 각 요소의 실제 타입을 캐스팅하여 적절한 처리를 수행한다. 스위치 구문을 활용하면 코드의 가독성이 높아진다.
Swift와 Objective-C 간의 브리징
Swift는 Objective-C와의 상호 운용성을 위해 다양한 타입 간 브리징을 지원한다. 이때도 타입 캐스팅이 중요한 역할을 한다.
브리징 예제
let nsArray: NSArray = ["Swift", "Objective-C"]
if let swiftArray = nsArray as? [String] {
print("Swift 배열로 변환: \(swiftArray)")
}
Objective-C의 NSArray를 Swift의 [String] 배열로 변환할 때 as? 연산자를 사용하여 안전하게 캐스팅한다. 브리징은 자동으로 이루어지지만, 명시적 캐스팅을 통해 타입 안정성을 확보할 수 있다.
제네릭과 타입 제약
Swift의 제네릭은 타입 안정성과 재사용성을 극대화 하는 방식이다. 제네릭 함수나 타입을 설계할 때, 타입 캐스팅 대신 타입 제약을 통해 컴파일 타임에 타입 오류를 줄일 수 있다.
func process<T: Animal>(animal: T) {
// Animal 또는 그 하위 타입만 허용
print(type(of: animal))
}
제네릭과 타입 제약을 활용하면, 특정 타입만 허용하는 로직을 컴파일 타임에 보장할 수 있어 런타임 오류 가능성을 줄일 수 있다.
'TIL(Today I Learned)' 카테고리의 다른 글
| Swift 에러 처리 (2) | 2025.03.14 |
|---|---|
| Swift의 ARC (0) | 2025.03.01 |
| 2025.02.14 Today I Learned (0) | 2025.02.14 |
| 2025.02.05 Today I Learned (0) | 2025.02.05 |
| 2025.01.24 Today I Learned (0) | 2025.01.24 |