TIL: 타입 캐스팅(Type Casting)

Royce·2025년 3월 23일

Swift 문법

목록 보기
40/63

타입 캐스팅 (Type Casting)

  • 타입 캐스팅(Type Casting) 은 인스턴스의 타입을 확인하거나, 해당 인스턴스를 다른 타입으로 변환하는 방법이다
  • Swift에서 타입 캐스팅은 클래스 상속 관계에서 주로 사용되며, 구조체나 열거형에서는 사용되지 않는다
  • 타입 캐스팅을 통해 인스턴스가 특정 클래스 타입인지 확인하고, 필요에 따라 타입을 변환할 수 있다

타입 캐스팅의 종류

  1. 타입 검사 (is 연산자)
    • 인스턴스가 특정 클래스 타입의 인스턴스인지 확인한다
    • 상속 관계에서 부모 클래스, 자식 클래스 여부를 확인하는 데 사용한다
    • 반환값은 Bool 타입 (true 또는 false)
  2. 타입 변환 (as 연산자)
    • 인스턴스를 특정 클래스 타입으로 변환한다
    • as?: 안전한 타입 변환 (Optional 반환)
    • as!: 강제 타입 변환 (강제 언래핑, 변환이 실패하면 런타임 에러 발생)
  3. 업캐스팅 (Upcasting)
    • 하위 클래스의 인스턴스를 상위 클래스 타입으로 변환하는 것
    • 항상 성공하며, as 키워드를 사용한다
    • 컴파일러가 항상 성공하는 것을 보장하기 때문에 에러가 발생하지 않는다
  4. 다운캐스팅 (Downcasting)
    • 상위 클래스의 인스턴스를 하위 클래스 타입으로 변환하는 것
    • 실패할 가능성이 있기 때문에 as? 또는 as! 를 사용한다
    • as?: 안전한 타입 변환 (성공하면 옵셔널 타입으로 반환, 실패하면 nil 반환)
    • as!: 강제 타입 변환 (성공하면 일반 타입으로 반환, 실패하면 런타임 에러 발생)
  5. 브릿징 (Bridging)
  • Swift와 Objective-C 타입 간의 변환을 의미한다
  • 예를 들어, String ↔️ NSString, Array ↔️ NSArray 와 같은 변환이 자동으로 이루어진다
  • 타입 캐스팅 (as) 를 사용하여 서로 변환할 수 있다 (강제 캐스팅 필요 없음)

클래스 상속 구조 정의

// 상위 클래스 정의 (Animal)
class Animal {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

// 하위 클래스 정의 (Mammal)
class Mammal: Animal {
    var isWarmBlooded: Bool = true
}

// 하위 클래스 정의 (Bird)
class Bird: Animal {
    var canFly: Bool
    
    init(name: String, canFly: Bool) {
        self.canFly = canFly
        super.init(name: name)
    }
}
  • Animal, Mammal, Bird 클래스는 상속 관계로 정의되었다
  • Animal 클래스는 모든 동물의 공통 속성인 name을 저장한다
  • Mammal 클래스는 Animal을 상속받아 포유류의 특징을 추가한다 (isWarmBlooded)
  • Bird 클래스는 Animal을 상속받아 새의 특징을 추가한다 (canFly)
  • 각 클래스의 인스턴스를 초기화하여 생성한다

타입 검사 (is 연산자)

  • is 연산자는 인스턴스가 특정 타입에 속하는지 검사하는 연산자이다
  • 상속 관계의 클래스에서 타입을 확인하는 데 주로 사용된다
  • 형식: 인스턴스 is 타입 ➔ 반환 값: true 또는 false

타입 검사 (is 연산자) 사용

// 인스턴스 생성
let animal = Animal(name: "동물")
let mammal = Mammal(name: "사자")
let bird = Bird(name: "독수리", canFly: true)

// 인스턴스 타입 확인하기
print(animal is Animal)    // true  - animal은 Animal 타입
print(animal is Mammal)     // false - animal은 Mammal 타입이 아님
print(animal is Bird)       // false - animal은 Bird 타입이 아님

print(mammal is Animal)     // true  - mammal은 Animal 타입 (상속 관계)
print(mammal is Mammal)     // true  - mammal은 Mammal 타입
print(mammal is Bird)       // false - mammal은 Bird 타입이 아님

print(bird is Animal)       // true  - bird는 Animal 타입 (상속 관계)
print(bird is Mammal)       // false - bird는 Mammal 타입이 아님
print(bird is Bird)         // true  - bird는 Bird 타입
  • is 연산자를 사용하여 인스턴스가 특정 타입인지 확인하고 있다
  • Animal 타입은 모든 인스턴스에서 참(true)으로 반환된다 (상속 관계)
  • Mammal 타입은 Mammal 인스턴스에서만 참(true)으로 반환된다
  • Bird 타입은 Bird 인스턴스에서만 참(true)으로 반환된다
  • 이 예제는 타입 검사 기능을 간단히 확인하는 코드이다

타입 검사 활용 예제

// 다양한 인스턴스를 포함한 배열 생성
let animals: [Animal] = [animal, mammal, bird, Mammal(name: "코끼리"), Bird(name: "펭귄", canFly: false)]

// 포유류(Mammal) 인스턴스의 개수를 세기 위한 변수
var mammalCount = 0

// 배열의 각 인스턴스를 검사하여 Mammal 타입인지 확인
for someAnimal in animals {
    if someAnimal is Mammal {  // someAnimal이 Mammal 타입 또는 그 하위 타입인지 확인
        mammalCount += 1
    }
}

print("Mammal 타입의 인스턴스 개수: \(mammalCount)")  // 출력: 2
  • animals 배열은 다양한 인스턴스를 포함하고 있다 (Animal, Mammal, Bird)
  • is 연산자를 사용하여 배열의 인스턴스가 Mammal 타입인지 검사한다
  • 검사 결과가 true인 경우에만 mammalCount 값을 증가시킨다
  • 결과적으로, 배열 내의 Mammal 타입 인스턴스는 2개이다

요약

  • is 연산자는 인스턴스가 특정 타입에 속하는지 확인하는 연산자이다
  • Animal, Mammal, Bird 클래스는 상속 관계로 정의되었다
  • is 연산자는 상속 관계에서 부모 타입 또는 자식 타입 여부를 확인하는 데 사용된다
  • 타입 검사 결과는 true 또는 false로 반환된다

as 연산자 / 다운 캐스팅의 정확한 의미에 대한 이해

클래스 상속 구조 정의

// 상위 클래스 정의 (Employee)
class Employee {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 하위 클래스 정의 (Manager)
class Manager: Employee {
    var department: String
    
    init(name: String, age: Int, department: String) {
        self.department = department
        super.init(name: name, age: age)
    }
}

// 하위 클래스 정의 (Developer)
class Developer: Employee {
    var programmingLanguage: String
    
    init(name: String, age: Int, programmingLanguage: String) {
        self.programmingLanguage = programmingLanguage
        super.init(name: name, age: age)
    }
}
  • Employee, Manager, Developer 클래스는 상속 관계로 정의되었다
  • Employee 클래스는 nameage라는 저장 속성을 가진다
  • Manager 클래스는 Employee를 상속받아 department 속성을 추가한다
  • Developer 클래스는 Employee를 상속받아 programmingLanguage 속성을 추가한다
  • 각 클래스의 인스턴스를 초기화하여 생성한다

업캐스팅(Upcasting) (as 연산자)

  • 하위 클래스의 인스턴스를 상위 클래스 타입으로 변환하는 것을 의미한다
  • 업캐스팅은 항상 성공하며, 타입 변환 후 상위 클래스에서 정의된 속성과 메서드만 사용할 수 있다
  • as 키워드를 사용하여 변환한다
  • 업캐스팅은 컴파일러가 항상 성공하는 것을 보장한다

업캐스팅 (as 연산자) 사용 예제

// 인스턴스 생성
let developer = Developer(name: "Ethan", age: 29, programmingLanguage: "Swift")

// 업캐스팅 (Developer → Employee)
let employee: Employee = developer as Employee

print("이름: \(employee.name), 나이: \(employee.age)")
  • developer 인스턴스를 Employee 타입으로 변환한다
  • 업캐스팅은 항상 성공하며, 상위 클래스의 속성 (name, age) 에 접근할 수 있다
  • programmingLanguage 속성에는 접근할 수 없다 (하위 클래스의 고유 속성이기 때문)

브릿징 (Bridging)

  • 브릿징(Bridging) 은 Swift와 Objective-C 간의 호환성을 위해 사용된다
  • Swift의 기본 타입(String, Array, Dictionary)은 Objective-C의 타입과 상호 변환될 수 있다
  • 타입 캐스팅(as)을 사용하여 변환하며, 컴파일러가 자동으로 변환을 허용한다
  • 강제 타입 캐스팅 (as!) 이나 안전한 타입 변환 (as?) 이 필요하지 않다
  • 브릿징은 타입 변환을 강제로 시도하지 않고 자연스럽게 연결되는 것이 특징이다

브릿징 (Bridging) 사용 예제

import Foundation  // Objective-C 기반의 NSString을 사용하기 위해 필요

// String을 NSString으로 변환 (자동 변환)
let swiftString: String = "Hello, Swift"
let nsString: NSString = swiftString as NSString
print("NSString 타입: \(nsString)")

// NSString을 String으로 변환 (자동 변환)
let newSwiftString: String = nsString as String
print("String 타입: \(newSwiftString)")

// Array를 NSArray로 변환
let swiftArray: [String] = ["Swift", "Objective-C", "Python"]
let nsArray: NSArray = swiftArray as NSArray
print("NSArray 타입: \(nsArray)")

// NSArray를 Array로 변환
let newSwiftArray: [String] = nsArray as! [String]
print("Array 타입: \(newSwiftArray)")
  • Swift의 String 타입이 NSString으로 자동 변환되고, 다시 String으로 변환되는 과정이다
  • Swift의 Array 타입이 NSArray로 자동 변환되고, 다시 Array로 변환되는 과정이다
  • Swift와 Objective-C의 타입은 자연스럽게 변환되므로 타입 캐스팅을 강제로 시도할 필요가 없다

다운캐스팅(Downcasting) (as? / as! 연산자)

  • 상위 클래스의 인스턴스를 하위 클래스 타입으로 변환하는 것을 의미한다
  • 다운캐스팅은 실패할 가능성이 있기 때문에 두 가지 방식으로 구분된다

안전한 타입 변환 (as?)

  • 타입 변환이 성공하면 옵셔널 타입으로 반환된다
  • 타입 변환이 실패하면 nil을 반환한다
  • 왜 실패할 수 있는가?
    • 실제로 해당 인스턴스가 하위 클래스의 인스턴스가 아닌 경우
    • 예를 들어, Employee 타입이지만 실제로는 Manager가 아닌 Developer라면 nil을 반환한다
  • 주로 옵셔널 바인딩 (if let / guard let)과 함께 사용한다

강제 타입 변환 (as!)

  • 타입 변환이 성공하면 일반 타입으로 반환된다
  • 타입 변환이 실패하면 런타임 오류가 발생하여 프로그램이 종료된다
  • 사용 시 매우 주의해야 한다 (프로그램이 비정상 종료될 수 있기 때문)

다운캐스팅 (as?, as! 연산자) 사용 예제

// 인스턴스 생성
let employee: Employee = Developer(name: "Charlie", age: 30, programmingLanguage: "Python")

// 안전한 타입 변환 (as?)
if let developer = employee as? Developer {
    print("개발자 이름: \(developer.name), 언어: \(developer.programmingLanguage)")
} else {
    print("다운캐스팅 실패 - Developer 타입이 아닙니다.")
}

// 강제 타입 변환 (as!) - 런타임 오류 발생 가능
let manager = employee as! Manager  // 런타임 오류 발생 (실제로 Manager 타입이 아니기 때문)
print("매니저 이름: \(manager.name), 부서: \(manager.department)")
  • employeeEmployee 타입으로 선언되었지만 실제로는 Developer 인스턴스이다
  • 안전한 타입 변환 (as?)은 Developer로 변환이 성공하여 옵셔널 타입으로 반환한다
  • 강제 타입 변환 (as!)은 실패할 경우 프로그램이 강제로 종료된다
  • 다운캐스팅 실패 시, 안전한 변환은 nil을 반환한다 (타입이 맞지 않기 때문)

요약

  • as 연산자는 항상 성공하는 업캐스팅을 수행한다
  • as? 연산자는 안전한 다운캐스팅을 수행하여, 실패 시 nil을 반환한다
  • as! 연산자는 강제 다운캐스팅을 수행하며, 실패 시 런타임 오류를 발생시킨다
  • 브릿징은 Swift와 Objective-C 타입 간 변환을 자연스럽게 연결한다

상속과 다형성(Polymorphism)

다형성(Polymorphism)

  • 다형성(Polymorphism) 은 하나의 객체(인스턴스)가 여러 가지 타입의 형태로 표현될 수 있는 것을 의미한다
  • 주로 클래스의 상속을 통해 구현되며, 같은 메서드 호출이 다른 방식으로 동작할 수 있게 한다
  • 다형성은 상위 클래스 타입으로 선언된 변수나 배열을 사용해, 하위 클래스의 인스턴스를 다룰 수 있는 특징을 가진다
  • 메서드를 재정의하여 각 클래스의 인스턴스마다 다른 동작을 수행할 수 있다

클래스 상속 구조 정의

// 상위 클래스 정의 (Animal)
class Animal {
    var name: String  // 모든 동물이 가지는 이름 속성
    
    init(name: String) {
        self.name = name
    }
    
    func makeSound() {  // 상위 클래스의 기본 메서드
        print("동물이 소리를 낸다")
    }
}
  • Animal 클래스는 모든 동물의 공통 속성인 name을 가진다
  • init(name:) 초기화 메서드를 통해 name 값을 설정한다
  • makeSound() 메서드는 동물이 소리를 내는 기본 동작을 정의하며, 상속받은 하위 클래스에서 재정의될 수 있다
  • 이 메서드를 재정의하지 않으면 기본적으로 “동물이 소리를 낸다”라는 메시지가 출력된다

하위 클래스 정의

// 하위 클래스 정의 (Dog)
class Dog: Animal {
    var breed: String  // 개의 품종 정보 추가
    
    init(name: String, breed: String) {
        self.breed = breed
        super.init(name: name)  // 상위 클래스의 초기화 호출
    }
    
    override func makeSound() {  // 메서드 재정의 (Override)
        print("\(name)(\(breed))가 멍멍! 하고 짖는다")
    }
    
    func fetch() {  // Dog 클래스만의 고유 메서드
        print("\(name)가 공을 물어온다")
    }
}

// 하위 클래스 정의 (Cat)
class Cat: Animal {
    var color: String  // 고양이의 색상 정보 추가
    
    init(name: String, color: String) {
        self.color = color
        super.init(name: name)  // 상위 클래스의 초기화 호출
    }
    
    override func makeSound() {  // 메서드 재정의 (Override)
        print("\(name)(\(color))가 야옹! 하고 운다")
    }
    
    func climb() {  // Cat 클래스만의 고유 메서드
        print("\(name)가 나무를 오른다")
    }
}
  • Dog 클래스는 Animal을 상속받아 breed 속성을 추가한다
  • Cat 클래스는 Animal을 상속받아 color 속성을 추가한다
  • 각 클래스는 makeSound() 메서드를 재정의하여 다형성을 구현한다
  • Dog 클래스는 fetch() 라는 고유 메서드를 추가로 정의한다
  • Cat 클래스는 climb() 라는 고유 메서드를 추가로 정의한다
  • 상위 클래스의 메서드와 속성은 상속받아 사용할 수 있으며, 하위 클래스에서 재정의된 메서드는 다형성을 발현한다

다형성의 구현(Polymorphism)

// 다형성을 활용한 인스턴스 생성 (상위 클래스 타입으로 선언)
let animal: Animal = Animal(name: "동물")
let dog: Animal = Dog(name: "바둑이", breed: "진돗개")
let cat: Animal = Cat(name: "나비", color: "하얀색")

// 배열로 묶어서 사용 (다형성 활용)
let animals: [Animal] = [animal, dog, cat]

// 배열을 이용하여 메서드 호출하기 (다형성의 발현)
print("동물들의 소리:")
for creature in animals {
    creature.makeSound()
}


// 출력 결과
동물들의 소리:
동물이 소리를 낸다
바둑이(진돗개)가 멍멍! 하고 짖는다
나비(하얀색)가 야옹! 하고 운다
  • dogcat 인스턴스는 각각 Animal 타입으로 선언되어 업캐스팅된 상태이다
  • 배열 animalsAnimal 타입으로 선언되었지만, 실제로는 다양한 하위 클래스의 인스턴스를 포함하고 있다
  • 배열의 요소가 Animal 타입으로 선언되었더라도, 실제 인스턴스의 타입에 따라 재정의된 makeSound() 메서드가 호출된다
  • 이는 다형성의 중요한 특징으로, 상위 타입으로 선언되어도 실제 인스턴스의 타입이 가지고 있는 메서드가 호출되는 것을 의미한다

다형성의 메서드 호출 원리와 메서드 테이블

  • 다형성의 구현은 메서드 테이블(Method Table) 이라는 개념을 통해 이루어진다
  • 상위 클래스 타입으로 선언된 객체라도, 실제 메모리에 저장된 인스턴스의 메서드 테이블을 참조하여 메서드를 호출한다
  • 만약 makeSound() 메서드를 재정의했다면, 해당 메서드 테이블의 주소가 업데이트되어 올바르게 호출된다
  • 따라서, 코드에서는 Animal 타입으로 선언되었어도 실제로는 Dog 또는 Cat 클래스의 메서드를 호출할 수 있는 것이다

요약

  • 다형성은 상위 클래스 타입으로 선언된 객체가 하위 클래스의 메서드를 호출할 수 있는 특성이다
  • 상속과 메서드 재정의를 통해 구현되며, 메서드 테이블을 이용해 다형성을 실현한다
  • 배열을 사용하여 다양한 인스턴스를 한 번에 처리할 수 있다
  • 타입 검사와 다운캐스팅을 사용하여 필요한 기능을 활용할 수 있다

AnyAnyObject를 위한 타입 캐스팅

  • 스위프트는 불특정한 타입을 다룰 수 있는 타입을 제공한다
  • 특정 타입이 아닌 다양한 타입을 저장하거나 사용할 때 유용하게 활용할 수 있다

Any 타입

  • Any 타입 은 모든 타입의 인스턴스를 표현할 수 있는 타입이다
  • 기본 타입(Int, String, Bool 등) 뿐만 아니라, 커스텀 클래스, 구조체, 열거형, 함수 타입까지도 포함한다
  • 옵셔널 타입 또한 포함할 수 있다
  • Any 타입으로 저장된 값은 메모리 구조를 알 수 없기 때문에, 항상 타입캐스팅을 통해 사용해야 한다

Any 타입 사용 예제

// Any 타입의 변수 선언 및 초기화
var unknownValue: Any = "Hello, Swift"  // Any 타입의 변수에 문자열 저장 (String 타입)
unknownValue = 42                        // 동일 변수에 정수 저장 (Int 타입)
unknownValue = 99.99                     // 동일 변수에 실수 저장 (Double 타입)

// Any 타입의 변수는 저장할 때 타입이 명확하지 않기 때문에 사용하려면 타입 캐스팅이 필요함
if let stringValue = unknownValue as? String {  // unknownValue를 String 타입으로 변환 시도
    print("문자열: \(stringValue)")
} else if let intValue = unknownValue as? Int {  // unknownValue를 Int 타입으로 변환 시도
    print("정수: \(intValue)")
} else if let doubleValue = unknownValue as? Double {  // unknownValue를 Double 타입으로 변환 시도
    print("소수: \(doubleValue)")
}


// 출력 결과
소수: 99.99
  • unknownValueAny 타입으로 선언되었기 때문에 모든 타입의 값을 저장할 수 있다
  • Any 타입은 저장된 값의 실제 타입을 알 수 없으므로, 사용할 때는 항상 타입캐스팅을 해야 한다
  • as? 연산자를 사용하여 안전하게 타입을 변환할 수 있다 (변환에 실패하면 nil 반환)
  • 변환이 성공하면 if let 구문으로 옵셔널 바인딩하여 해당 타입으로 사용할 수 있다

AnyObject 타입

  • AnyObject 타입 은 어떤 클래스 타입의 인스턴스도 표현할 수 있는 타입이다
  • 클래스 인스턴스만을 다루기 때문에 구조체, 열거형 등의 값 타입은 포함되지 않는다
  • 주로 클래스 타입의 객체들을 담는 배열 등에 사용된다

AnyObject 타입 사용 예제

// 클래스 정의
class Animal {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Vehicle {
    var model: String
    
    init(model: String) {
        self.model = model
    }
}

// 클래스 인스턴스 생성
let animal = Animal(name: "강아지")
let vehicle = Vehicle(model: "자동차")

// AnyObject 타입 배열 선언 (클래스 인스턴스만 저장 가능)
let objectArray: [AnyObject] = [animal, vehicle]

// 배열의 요소를 타입캐스팅하여 사용하기
for object in objectArray {
    if let animal = object as? Animal {  
        print("동물의 이름: \(animal.name)")
    } else if let vehicle = object as? Vehicle {  
        print("탈것의 모델: \(vehicle.model)")
    }
}


// 출력 결과
동물의 이름: 강아지
탈것의 모델: 자동차
  • objectArrayAnyObject 타입의 배열로, 클래스 인스턴스만 저장할 수 있다
  • 배열의 요소를 사용하기 위해서는 타입캐스팅이 필요하다 (as? 사용)
  • AnimalVehicle 클래스 인스턴스만 저장할 수 있기 때문에, 구조체나 열거형은 포함할 수 없다
  • 타입캐스팅을 성공하면 해당 타입으로 사용 가능하며, 실패하면 nil 을 반환한다

타입캐스팅 + 분기 처리

// 클래스 정의
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 다양한 타입의 값을 가진 배열 생성
let mixedArray: [Any] = [
    42,                                    
    "Hello",                               
    3.14,                                  
    Person(name: "Alice", age: 25),        
    { (x: Int) -> Int in return x * 2 }    
]

// 배열의 요소를 타입캐스팅하여 분기 처리
for (index, item) in mixedArray.enumerated() {
    switch item {
    case is Int:                                    
        print("Index \(index): 정수입니다")
    case let value as String:                      
        print("Index \(index): 문자열 - \(value) 입니다")
    case let value as Double:                      
        print("Index \(index): 실수 - \(value) 입니다")
    case let person as Person:                     
        print("Index \(index): 사람 - 이름: \(person.name), 나이: \(person.age)")
    case is (Int) -> Int:                           
        print("Index \(index): 클로저 타입입니다")
    default:
        print("Index \(index): 알 수 없는 타입입니다")
    }
}
  • mixedArray 는 다양한 타입의 값을 포함한 배열이다
  • for 문과 switch 문을 이용하여 배열의 각 요소를 검사한다
  • isas 를 사용하여 타입을 확인하고 안전하게 변환한다
  • 변환된 타입에 따라 각기 다른 처리를 수행한다

옵셔널 값의 Any 변환

let optionalValue: Int? = 42

print(optionalValue)              // 경고 발생 (Optional(...) 형태로 출력됨)
print(optionalValue as Any)        // Any 타입으로 변환하면 경고 없이 출력 가능
  • 옵셔널 값은 직접 사용하면 경고가 발생한다 (Optional(…) 형태로 출력)
  • 옵셔널 값은 임시적인 값으로 간주되므로, 안전하게 사용하려면 옵셔널 바인딩을 해야 한다
  • 하지만 as Any 로 변환하면 경고 없이 출력할 수 있다
  • Any 타입은 옵셔널 값도 포함할 수 있다

요약

  • Any 는 모든 타입을 포함할 수 있는 타입이다 (옵셔널 포함)
  • AnyObject 는 모든 클래스 타입을 포함할 수 있는 타입이다 (구조체와 열거형은 제외)
  • 타입캐스팅을 통해 요소를 구분하고 사용할 수 있다 (as?, is 활용)
  • [Any] 타입을 사용하면 배열에 다양한 타입의 값을 저장할 수 있다
profile
iOS 개발자 지망생

0개의 댓글