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
- 같은 이름의 함수, 메서드, 생성자, 서브스크립트를 여러 형태로 정의할 수 있는 기능
오버로딩이 가능한 조건
-
이름은 같고, 파라미터 수가 다르면 오버로딩된다.
-
이름과 파라미터 수가 같아도, 파라미터 타입이 다르면 오버로딩된다.
-
이름, 파라미터 수, 파라미터 타입이 모두 같아도, Argument Label이 다르면 오버로딩된다.
-
이름과 파라미터가 완전히 같으면, 리턴 타입만 다르게 해도 오버로딩할 수 있다.
(단, 리턴 타입만 다르고 호출 방법이 모호하면 컴파일러가 혼란스러워할 수 있다.)
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 , Struct | Class , 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
dog1
과dog3
은 같은 인스턴스를 가리킨다.dog1
과dog2
는 다른 인스턴스다.
요약
주제 | 정리 |
---|---|
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
Owner
와Pet
이 서로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
Pet
이Owner
를 weak 참조하게 수정- 객체가 정상적으로 메모리에서 해제됨
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
CreditCard
는User
를 unowned로 참조- 메모리 해제 시 문제가 없으며,
user
는 항상 존재한다고 가정
요약
- 기본은
strong
- 순환 참조 위험이 있으면
weak
을 사용 - 항상 존재해야 하는 참조는
unowned
사용 - ARC는 Swift가 자동으로 관리하지만, 개발자가 참조 관계를 잘 설계해야 메모리 누수를 막을 수 있다.
SwiftUI 4대 기본 컨셉
요소 | 설명 | 실체 | 비고 |
---|---|---|---|
App | 앱의 진입점(entry point)을 정의 | App 프로토콜 | @main 속성 붙은 타입이 채택 |
Scene | 앱의 주요 장면(스크린 단위)을 관리 | Scene 프로토콜 | 보통 WindowGroup, NavigationStack 등 |
View | 사용자에게 보여지는 실제 화면 구성 요소 | View 프로토콜 | 대부분 SwiftUI는 View 중심 |
Modifier | View를 꾸미거나 변형하는 메서드들의 체이닝 | - | .modifier() 도 있지만 보통 .font() , .padding() 같은 메서드 |