Any, AnyObject, 타입 캐스팅, ARC, Extension 정리

Any, AnyObject, 타입 캐스팅, ARC, Extension 정리


1. Any와 AnyObject

  • Any

    • Swift에서 모든 타입을 담을 수 있는 범용 타입이다.
    • 값 타입(Int, String, Struct 등)과 참조 타입(Class 등) 모두 저장할 수 있다.
  • AnyObject

    • Swift에서 모든 클래스 타입(참조 타입)만 담을 수 있는 타입이다.
    • Struct, Enum 같은 값 타입은 저장할 수 없다.
  • 장점

    • 매우 유연하다. 타입을 제한하지 않고 어떤 값이든 저장할 수 있다.
  • 단점

    • 컴파일러가 타입을 알 수 없어 안정성이 떨어진다.
    • 타입을 확인하고 변환(casting)해야 하므로 코드 가독성이 나빠질 수 있다.
    • 런타임 오류가 발생할 가능성이 높아진다.

가급적 명확한 타입을 사용하는 것을 권장

예제

var data: Any = 1
data = "23"
 
// 옵셔널 체이닝을 통한 타입 변환
let count = (data as? String)?.count
print(count ?? 0) // 2
 

타입 캐스팅 패턴

  • switch 문을 사용해서 Any 타입을 구체적인 타입으로 구분할 수 있다.
switch data {
case let str as String:
    print("문자열 길이: \(str.count)")
case is Double:
    print("Double 타입입니다")
default:
    print("기타 타입입니다")
}
 
  • as? : 타입 변환이 실패하면 nil을 반환하는 안전한 다운캐스팅
  • as! : 타입 변환이 실패하면 런타임 오류가 발생하는 강제 다운캐스팅 (웬만하면 지양)

Type Erasure (타입 소거)

  • Type Erasure는 제네릭 타입처럼 구체적인 타입 정보를 숨기고 싶을 때 사용한다.
  • 주로 프로토콜을 사용할 때 특정 타입 제약을 없애거나, 제네릭의 타입 파라미터를 외부로부터 감추고 싶을 때 쓴다.
  • Any, AnyObject도 일종의 타입 소거 기법이다.

예시

let values: [Any] = [1, "Hello", 3.14, true]
 
for value in values {
    switch value {
    case let number as Int:
        print("Int: \(number)")
    case let text as String:
        print("String: \(text)")
    case let decimal as Double:
        print("Double: \(decimal)")
    default:
        print("Other Type")
    }
}
 

AnyObject

class Animal {}
class Dog: Animal {}
 
let dog = Dog()
let object: AnyObject = dog
 

→ object는 AnyObject 타입으로 선언되었기 때문에 클래스 타입 인스턴스라면 어떤 것도 저장 가능하다.


2. Overloading

  • 같은 이름의 함수, 메서드, 생성자, 서브스크립트를 여러 형태로 정의할 수 있는 기능

오버로딩이 가능한 조건

  1. 이름은 같고, 파라미터 수가 다르면 오버로딩된다.

  2. 이름과 파라미터 수가 같아도, 파라미터 타입이 다르면 오버로딩된다.

  3. 이름, 파라미터 수, 파라미터 타입이 모두 같아도, Argument Label이 다르면 오버로딩된다.

  4. 이름과 파라미터가 완전히 같으면, 리턴 타입만 다르게 해도 오버로딩할 수 있다.

    (단, 리턴 타입만 다르고 호출 방법이 모호하면 컴파일러가 혼란스러워할 수 있다.)


Deinitializer와 메모리 비교 정리

Deinitializer란?

  • 클래스 인스턴스가 메모리에서 사라지기 직전에 자동으로 호출되는 메서드
  • 생성자(init)는 여러 개 만들 수 있지만, deinit은 클래스당 하나만 만들 수 있다.
  • 클래스 전용 기능이다. (구조체, 열거형에서는 사용 불가)

예제

 
class Size {
    var width = 0.0
    var height = 0.0
 
    deinit {
        print("Size 해제됨")
    }
}
 
class Position {
    var x = 0.0
    var y = 0.0
 
    deinit {
        print("Position 해제됨")
    }
}
 
class Rect {
    var origin = Position()
    var size = Size()
 
    deinit {
        print("Rect 해제됨")
    }
}
 
// 인스턴스 생성
var r: Rect? = Rect()
 
// 인스턴스 제거
r = nil
 
print("완료")
 

실행 결과

 
Size 해제됨
Position 해제됨
Rect 해제됨
완료
 
  • r = nil을 통해 Rect 인스턴스를 메모리에서 해제했다.
  • Rect가 해제되면서 origin, size도 함께 해제된다.

Value Type과 Reference Type 비교

구분Value Type (구조체 Struct)Reference Type (클래스 Class)
저장 위치스택(Stack)스택 + 힙(Stack + Heap)
let 사용 시값 전체가 잠긴다주소만 잠긴다, 실제 데이터는 변경 가능
복사 방식값을 복사주소를 복사
예시Int, Double, StructClass, NSObject
  • Value Type은 값 자체가 복사된다.
  • Reference Type은 주소만 복사되고, 실제 데이터(힙 메모리)는 공유한다.

항등 연산자 (Identity Operator)

  • 항등 연산자는 인스턴스의 메모리 주소를 비교한다.
  • 값이 아니라 참조가 같은지를 판단한다.

종류

연산자의미
===메모리 주소가 같으면 true
!==메모리 주소가 다르면 true

예제

 
class Dog {
    var name = "강아지"
}
 
let dog1 = Dog()
let dog2 = Dog()
let dog3 = dog1
 
print(dog1 === dog2) // false (다른 인스턴스)
print(dog1 === dog3) // true (같은 인스턴스)
print(dog1 !== dog2) // true
 
  • dog1dog3같은 인스턴스를 가리킨다.
  • dog1dog2다른 인스턴스다.

요약

주제정리
Deinitializer클래스 인스턴스가 메모리에서 해제되기 직전에 호출
Value Type값 자체를 복사, 스택 공간 사용
Reference Type주소를 복사, 스택 + 힙 공간 사용
항등 연산자(===, !==)메모리 주소(참조) 비교

--- deinit은 메모리 관리할 때 꼭 필요한 개념이지만, 직접 호출할 수 없고, 시스템이 알아서 불러준다.

  • Swift에서는 ARC(Automatic Reference Counting) 덕분에 대부분 메모리 관리는 자동이다.
  • 하지만, 강한 순환 참조(Strong Reference Cycle)를 피하려고 deinit이나 weak, unowned를 잘 이해하는 것이 중요하다.

3. Failable Initializer

Failable Initializer란?

  • 생성(초기화) 과정에서 주어진 조건을 만족하지 못하면 nil을 반환하는 생성자
  • Swift에서는 init? 또는 init! 을 사용한다.

언제 사용하는가?

  • 외부 리소스 연동 시 (파일, 네트워크, DB)
  • 입력 값 검증이 필요한 경우
  • "유효하지 않은 값"일 때 인스턴스 생성을 실패시켜야 할 때

기본 문법

  • init? : 실패하면 nil을 반환 (안전)
  • init! : 실패하면 앱이 Crash 발생 (특수한 경우 사용)

예제

나이를 입력 받아 사람(Person)을 만드는 예제

 
struct Person {
    let name: String
    let age: Int
 
    // 실패 가능 생성자
    init?(name: String, age: Int) {
        guard age >= 0 else {
            return nil
        }
        self.name = name
        self.age = age
    }
 
    // 강제 실패 가능 생성자
    init!(name: String) {
        guard !name.isEmpty else {
            return nil
        }
        self.name = name
        self.age = 0
    }
}
 

예시

 
var p1 = Person(name: "Alice", age: 20)   // 성공 (Person?)
var p2 = Person(name: "Bob", age: -5)      // 실패 (nil)
 
var p3: Person = Person(name: "Charlie")   // 성공 (Person)
var p4: Person = Person(name: "")          // 실패 시 런타임 에러 (Crash)
 
  • p1은 정상적으로 만들어진다.
  • p2는 실패해서 nil이 된다.
  • p3는 강제 언래핑이라 성공하면 바로 Person 타입이다.
  • p4는 빈 문자열이어서 런타임 에러가 난다.

 
let number = Int("456")    // Int? 타입
let fail = Int("Swift")    // 실패 → nil
 
  • 문자열을 숫자로 변환할 때도 실패 가능성을 고려한다.

요약

구분설명
init?실패하면 nil 반환 (가장 일반적, 안전함)
init!실패하면 런타임 Crash (테스트 상황 등 특별할 때만 사용)
  • 보통은 init?을 써서 실패를 안전하게 처리하는 것이 권장된다.
  • init!은 되도록 지양하고 필요한 경우에만 명확히 사용한다.

4. Extension

  • 기존 타입새로운 기능을 추가하는 문법
  • 저장 프로퍼티(Stored Property)는 추가할 수 없다.
  • 주로 계산 프로퍼티, 메서드, 이니셜라이저, 서브스크립트 등을 추가한다.

기본 문법

 
extension 기존타입 {
    // 새로 추가할 기능들
}

항목가능 여부
저장 프로퍼티 추가❌ 불가능
계산 프로퍼티 추가✅ 가능
메서드 추가✅ 가능
이니셜라이저 추가✅ 가능
서브스크립트 추가✅ 가능

예제

1. Int 타입 확장

 
extension Int {
    var isEven: Bool {
        return self % 2 == 0
    }
 
    var isOdd: Bool {
        return self % 2 != 0
    }
}
 
let number = 4
print(number.isEven) // true
print(number.isOdd)  // false
 
  • isEven, isOdd 프로퍼티를 통해 정수의 홀짝을 바로 알 수 있다.

2. String 타입 확장

 
extension String {
    var isNotEmpty: Bool {
        return !self.isEmpty
    }
 
    func repeated(count: Int) -> String {
        return String(repeating: self, count: count)
    }
}
 
let text = "Hi"
print(text.isNotEmpty)       // true
print(text.repeated(count: 3)) // "HiHiHi"
 
  • 문자열이 비어있지 않은지 확인하거나, 문자열을 여러 번 반복할 수 있다.

3. Array 타입 확장

 
extension Array {
    var second: Element? {
        return count > 1 ? self[1] : nil
    }
}
 
let numbers = [10, 20, 30]
print(numbers.second) // Optional(20)
 
let emptyArray: [Int] = []
print(emptyArray.second) // nil
 
  • 배열에서 두 번째 요소를 쉽게 가져올 수 있다.

4. Date 타입 확장

 
extension Date {
    var isToday: Bool {
        Calendar.current.isDateInToday(self)
    }
}
 
let today = Date()
print(today.isToday) // true
 
  • isToday로 오늘 날짜인지 바로 확인할 수 있다.

--- 기존 타입을 수정 없이 기능만 추가한다.

  • 타입을 깔끔하게 확장할 수 있어 유지보수가 좋아진다.
  • 저장 프로퍼티 추가는 불가능하다.

5. Automatic Reference Counting (ARC)

ARC란?

  • Swift가 자동으로 메모리 관리를 해주는 시스템.
  • 인스턴스가 더 이상 필요 없을 때 자동으로 메모리에서 해제된다.
  • 참조 횟수(Reference Count)를 기반으로 동작한다.

참조(Reference) 종류

타입설명특징
strong기본 참조 방식참조 카운트를 증가시켜 객체가 해제되지 않게 유지
weak약한 참조참조는 하지만 카운트를 증가시키지 않음, 객체가 해제되면 nil로 변경 (항상 옵셔널 타입)
unowned소유하지 않는 참조참조는 하지만 카운트를 증가시키지 않음, 객체가 해제되어도 nil이 아님 (비옵셔널 타입)

Strong Reference Cycle

  • 두 객체가 서로를 strong으로 참조할 때 발생.
  • 참조 카운트가 0이 되지 않아 메모리 누수(Memory Leak) 발생.

예제

Strong 참조로 인한 메모리 누수

 
class Owner {
    var pet: Pet?
 
    deinit {
        print("Owner deallocated")
    }
}
 
class Pet {
    var owner: Owner?
 
    deinit {
        print("Pet deallocated")
    }
}
 
var owner: Owner? = Owner()
var pet: Pet? = Pet()
 
owner?.pet = pet
pet?.owner = owner
 
owner = nil
pet = nil
 
  • OwnerPet이 서로 strong 참조 → 메모리 해제되지 않음 → 메모리 누수 발생

해결 방법 (weak 사용)

 
class Owner {
    var pet: Pet?
 
    deinit {
        print("Owner deallocated")
    }
}
 
class Pet {
    weak var owner: Owner?
 
    deinit {
        print("Pet deallocated")
    }
}
 
var owner: Owner? = Owner()
var pet: Pet? = Pet()
 
owner?.pet = pet
pet?.owner = owner
 
owner = nil
pet = nil
 
  • PetOwnerweak 참조하게 수정
  • 객체가 정상적으로 메모리에서 해제됨

unowned 사용 예시

  • 항상 메모리에 존재해야 하는 관계라면 unowned 사용
  • 예를 들어, CreditCard는 항상 User에 귀속되어야 한다.
 
class User {
    var card: CreditCard?
 
    deinit {
        print("User deallocated")
    }
}
 
class CreditCard {
    unowned var user: User
 
    init(user: User) {
        self.user = user
    }
 
    deinit {
        print("CreditCard deallocated")
    }
}
 
var user: User? = User()
user?.card = CreditCard(user: user!)
 
user = nil
 
  • CreditCardUserunowned로 참조
  • 메모리 해제 시 문제가 없으며, user는 항상 존재한다고 가정

요약

  • 기본은 strong
  • 순환 참조 위험이 있으면 weak을 사용
  • 항상 존재해야 하는 참조는 unowned 사용
  • ARC는 Swift가 자동으로 관리하지만, 개발자가 참조 관계를 잘 설계해야 메모리 누수를 막을 수 있다.

SwiftUI 4대 기본 컨셉

요소설명실체비고
App앱의 진입점(entry point)을 정의App 프로토콜@main 속성 붙은 타입이 채택
Scene앱의 주요 장면(스크린 단위)을 관리Scene 프로토콜보통 WindowGroup, NavigationStack 등
View사용자에게 보여지는 실제 화면 구성 요소View 프로토콜대부분 SwiftUI는 View 중심
ModifierView를 꾸미거나 변형하는 메서드들의 체이닝-.modifier()도 있지만 보통 .font(), .padding() 같은 메서드