Swift는 메모리 관리를 자동으로 처리하기 위해서 ARC(Automatic Reference Counting)를 사용한다. ARC는 객체가 사용되는 동안 참조 카운트(Reference Counting)를 관리하여, 더 이상 필요하지 않으면 객체를 자동으로 메모리에서 해제한다.
하지만 객체 간 강한참조(Strong Reference)를 잘못 사용하면, 순환 참조(Strong Reference Cycle)가 발생하여 메모리에서 해제되지 않는 문제가 발생할 수 있다.
ARC(Automatic Reference Counting)의 동작 원리
ARC는 객체가 할당되면 참조 카운트를 증가시키고, 더 이상 필요하지 않을 때 참조 카운트가 0이 되면 메모리에서 해제된다.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 객체 생성")
}
deinit {
print("\(name)" 객체 해제")
}
}
var person1: Person? = Person(name: "jon") // 참조 카운트 1
person1 = nil // 참조 카운트 0 -> 객체가 메모리에서 해제됨
// 결과
// jon 객체 생성
// jon 객체 해제
Strong, Weak, Unowned 참조
객체 간의 참조 방식에 따라 메모리 관리 방식이 달라질 수 있다.
Swift 에서는 Strong(강한 참조), Weak(약한 참조), Unowned(미소유 참조) 이렇게 세 가지 참조 방식을 지원한다.
1. Strong(강한 참조)
모든 변수와 프로퍼티는 기본적으로 strong 참조를 한다. 하지만 객체가 서로 강한 참조를 가지면 순환 참조(Strong Reference Cycle)가 발생하여 메모리가 해제되지 않을 수 있다.
class Person {
let name: String
var pet: Pet?
init(name: String) {
self.name = name
}
deinit {
print("\(name) 해제됨")
}
}
class Pet {
let name: String
var owner: Person?
init(name: String) {
self.name = name
}
deinit {
print("\(name) 해제됨")
}
}
var person: Person? = Person(name: "jon")
var pet: Pet? = Pet(name: "coco")
person?.pet = pet
pet?.owner = person // 순환 참조 발생
person = nil
pet = nil // 메모리에서 해제되지 않음 (메모리 누수 발생)
이 코드는 강한 참조로 인한 순환 참조가 되는 상황을 보여주는 예시 코드이다. 만약 이 코드에서 순환 참조가 되지 않게 하려면 weak 나 unowned 키워드를 사용해야 한다.
2. Weak(약한 참조)
weak 키워드를 사용하면 참조 카운트가 증가하지 않는다. 객체가 해제되면 weak 참조는 자동으로 nil이 된다.
class Pet {
let name: String
weak var owner: Person? // 약한 참조
init(name: String) {
self.name = name
}
deinit {
print("\(name) 해제됨")
}
}
Strong 예시 코드에서 weak를 사용한 순환 참조 해결 예시 코드이다. weak를 사용하여 Person 이 nil이 되면 Pet의 owner가 자동으로 nil이 되어 메모리에서 정상적으로 해제가 된다.
3. Unowned(미소유 참조)
unowned는 weak 와 비슷하지만, nil을 허용하지 않는다는 차이점이 존재한다. 즉, 객체가 해제되었는데 unowned 참조를 사용하면 런타임 에러가 발생할 수 있다.
class CreditCard {
let number: String
unowned let owner: Person
init(number: String, owner: Person) {
self.number = number
self.owner = owner
}
deinit {
print("카드 \(number) 해제됨")
}
}
이 예시 코드를 봤을 때 unowned 를 사용하면 Person 객체가 메모리에서 해제될 때 CreditCard 에서의 Person 객체도 메모리에서 정상적으로 해제된다. 하지만, unowned 는 nil을 허용하지 않기 때문에 아직 .owner 는 메모리에 없는 person 을 참조하려 한다. 그렇기 때문에 person 이 메모리에서 해제가 되었을 때 접근을 하면 런타임 에러가 발생할 수 있다.
클로저에서의 ARC 관리
클로저는 기본적으로 캡쳐를 수행하여 클로저 내부에서 사용되는 객체의 참조 카운트를 증가시킨다. 이로 인해 클로저와 객체간의 순환 참조가 발생할 수 있다.
class Person {
let name: String
var sayHello: (() -> Void)?
init(name: String) {
self.name = name
}
deinit {
print("\(name) 해제됨")
}
}
var person: Person? = Person(name: "jon")
person?.sayHello = {
print("안녕하세요, 저는 \(person!.name)입니다!") // person을 강하게 참조하여 순환 참조 발생
}
person = nil // 메모리 해제되지 않음
위 예시코드를 보면 person?.sayHello 클로저 내부에서 person!.name을 사용하면서 self(person)을 강한 참조로 캡쳐하여 사용한다. 그렇기 때문에 person = nil을 해도 클로저 내부에서 person을 참조하므로 메모리 누수가 발생할 수 있다.
클로저에서의 순환 참조 방지(캡쳐 리스트 사용)
위의 예시 코드에서 순환 참조를 방지하기 위해서 클로저 내부에서 weak 또는 unowned를 사용하여 객체를 캡처할 때 참조 카운트를 증가시키지 않도록 해야한다.
weak 키워드 사용 예시
person?.sayHello = { [weak person] in
print("안녕하세요, 저는 \(person?.name ?? "알 수 없음")입니다!")
}
weak 키워드를 사용하면 person이 해제될 때 자동으로 nil이 되어 순환 참조가 발생하지 않는다.
unowned 키워드 사용 예시
person?.sayHello = { [unowned person] in
print("안녕하세요, 저는 \(person.name)입니다!")
}
unowned 키워드는 객체가 항상 존재한다고 가정할 때 사용한다. 하지만 person 객체가 먼저 해제되었는데 클로저가 호출하면 런타임 에러가 발생할 수 있다.
| 키워드 | ARC 카운트 증가 | nil 허용 여부 | 사용 시기 |
| strong | 증가 | nil 불가능 | 일반적인 객체 참조 |
| weak | 증가 안함 | nil 가능 | API 호출, 비동기 작업, 객체가 사라질 수 있는 경우 |
| unowned | 증가 안함 | nil 불가능 | 순환참조가 일어날 수 있고, 객체가 반드시 존재한다고 가정할 때 사용 |
'TIL(Today I Learned)' 카테고리의 다른 글
| Swift 에러 처리 (2) | 2025.03.14 |
|---|---|
| 2025.02.24 Today I Learned (0) | 2025.02.24 |
| 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 |