
ARC(Automatic Reference Counting, 자동 참조 카운팅)
- Swift에서 힙(Heap) 영역에서 메모리 관리를 자동으로 수행하는 메커니즘
- 앱의 메모리 사용량을 효율적으로 관리하여 객체가 더 이상 필요하지 않을 때 메모리를 해제함
클래스 객체 생성 할 때의 ARC 매커니즘
1. 클래스 객체 생성
- 클래스 객체가 생성되어 Heap에 올라오면 ARC는 자동적으로 클래스 객체를 참조하는 수를 관리함
- 객체가 생성될 때, ARC는 해당 객체에 대한 참조 카운트를 1로 설정

2. 객체 참조 추가
- 객체를 다른 변수나 상수에 할당하거나, 다른 객체에서 참조할 때마다 참조 카운트 증가

3. 객체 참조 삭제
- 참조하는 객체가 해제될 때마다 참조 카운팅이 자동으로 감소시킴

4. 객체 메모리 해제
- 참조 카운팅이 0이 되면 ARC는 해당 객체의 메모리를 자동으로 해제함

ARC 예제
class Person {
var name: String
init(name: String) {
self.name = name
print("\\(name) is initialized.")
}
deinit { // 객체가 메모리에서 해제되기 직전에 호출됨
print("\\(name) is deinitialized.")
}
}
var person1: Person? = Person(name: "Alice") // 참조 카운트: 1 (객체 생성 : 참조 카운트 1로 설정)
var person2 = person1 // 참조 카운트: 2
person1 = nil // 참조 카운트: 1
person2 = nil // 참조 카운트: 0 -> 메모리 해제(deinit 메서드 실행)
ARC의 특징
- 자동 메모리 관리
- 프로그래머가 명시적으로 메모리를 해제하지 않아도 자동으로 관리해줌
- Swift 컴파일러가 코드를 분석하여 객체의 라이프스타일을 추적하고 메모리를 자동으로 관리함
- 효율성
- 불필요한 메모리 낭비를 줄임
- deinit 메서드
- 객체가 메모리에서 해제되기 직전에 호출됨
- 객체의 리소스를 정리하거나 추가 작업을 수행할 때 사용
ARC의 문제 : 강한 순환 참조 (Strong Reference Cycle)
- ARC가 참조 카운트를 관리하더라도 강한 순환 참조가 발생하면 메모리가 해제되지 않는 문제가 발생함 → 메모리 누수 발생
- 강환 순환 참조 : 두 객체가 강하게 참조하면서 참조 카운트가 0이 되지 않아 메모리가 해제 되지 않는 상황
class Person {
var name: String
var apartment: Apartment? // 서로 참조하는 관계
init(name: String) {
self.name = name
}
deinit {
print("\\(name) is deinitialized.")
}
}
class Apartment {
var unit: String
var tenant: Person? // 서로 참조하는 관계
init(unit: String) {
self.unit = unit
}
deinit {
print("Apartment \\(unit) is deinitialized.")
}
}
var alice: Person? = Person(name: "Alice") // Person - RC : 1
var unit101: Apartment? = Apartment(unit: "101") // Apartment - RC : 1
alice?.apartment = unit101 // Apartment - RC : 2
unit101?.tenant = alice // Person - RC : 2
alice = nil // Person - RC : 1
unit101 = nil // Apartment - RC : 1
// 참조 카운트가 0이 되지 않아 메모리 해제되지 않음
→ 메모리 해제가 되지 않아 deinit 메서드가 실행되지 않음
시뮬레이션된 참조 카운트 변화
단계 | alice (Person) RC |
unit101 (Apartment) RC |
비고 |
초기 생성 | 1 | 1 | |
alice.apartment | 1 | 2 | alice.apartment = unit101로 인해 강한 참조 추가 |
unit101.tenant | 2 | 2 | unit101?.tenant = alice로 인해 강한 참조 추가 |
alice = nil | 1 | 2 | alice 메모리 해제되지 않음 |
unit101 = nil | 1 | 1 | unit101 메모리 해제되지 않음 |
해결 방법 : 약한 참조 (Weak Reference), 비소유 참조(Unowned Reference)
- 약한 참조 (weak)
- weak 키워드는 참조 카운트를 증가 시키지 않음
- 참조 대상이 해제 되면 자동으로 nil이 됨
- 옵셔널로 선언해야 함
- 비소유 참조 (unowned)
- unowned 키워드는 참조 카운트를 증가시키지 않음
- 참조 대상이 해제되더라도 nil이 되지 않음
- 비옵셔널로 선언할 수 있지만, 참조 대상이 먼저 해제되면 런타임 오류가 발생함 → 약한 참조 사용하는 게 좋음
class Person {
var name: String
var apartment: Apartment? // 강한 참조
init(name: String) {
self.name = name
}
deinit {
print("\\(name) is deinitialized.")
}
}
class Apartment {
var unit: String
weak var tenant: Person? // 약한 참조
init(unit: String) {
self.unit = unit
}
deinit {
print("Apartment \\(unit) is deinitialized.")
}
}
var alice: Person? = Person(name: "Alice") // Person - RC : 1
var unit101: Apartment? = Apartment(unit: "101") // Apartment - RC : 1
alice?.apartment = unit101 // Apartment - RC : 2
unit101?.tenant = alice // Person - RC : 1
alice = nil // Person 메모리 해제 -> Person - RC : 0, Apartment - RC : 1
unit101 = nil // Apartment 메모리 해제 -> Apartment - RC : 0
- alice를 nil로 설정 시 Person - RC는 0이 되고, alice의 apartment도 메모리에서 해제되면서 Apartment의 RC도 1감소
- → Person 클래스에서 Apartment를 강한 참조하고 있기 때문
시뮬레이션된 참조 카운트 변화
단계 | alice (Person) RC | unit101 (Apartment) RC | 비고 |
초기 생성 | 1 | 1 | |
alice.apartment | 1 | 2 | alice.apartment = unit101로 인해 강한 참조 추가 |
unit101.tenant | 1 | 2 | 약한 참조는 RC에 영향 없음 |
alice = nil | 0 | 1 | alice 메모리 해제 alice.apartment도 해제되면서 unit101의 RC 감소 |
unit101 = nil | 0 | 0 | unit101의 강한 참조 모두 제거 |
클로저의 강한 순환 참조
- 클로저가 객체를 캡처할 때, 해당 객체에 대한 강한 참조를 생성하기 때문에 메모리 해제가 되지 않는 상태 발생
import Foundation
class Person {
var name: String
var greeting : String?
init(name: String) {
self.name = name
}
lazy var closure = {
print("My Name is \(self.name)") // 강한 순환 참조 발생(self를 사용하면 Person RC = 2)
}
func getName() {
closure()
}
deinit {
print("\\(name) is deinitialized.")
}
}
var alice: Person? = Person(name: "Alice") // Person RC = 1
CFGetRetainCount(alice) // 2
alice?.getName()
CFGetRetainCount(alice) // 3
// alice는 여전히 메모리에 남아 있음
alice = nil // 메모리에서 해제되지 않음, deinit 호출되지 않음
- 클로저 내부에서 객체 참조
- 클로저가 실행될 때, 클로저 내부에서 self(또는 다른 객체)를 참조하는 경우, 이 객체는 클로저에 의해 캡처 됨
- 클로저는 객체를 강하게 참조
- 강한 참조 순환
- 클로저와 객체가 서로 강하게 참조하는 상태가 되어 두 객체는 메모리에서 해제 될 수 없음 → 메모리 누수 발생
시뮬레이션된 참조 카운트 변화
단계 | alice (Person) RC | 비고 |
초기 생성 | 1 | |
alice.getName() | 1 | |
closure() | 2 | 클로저에서 self가 캡처되어 강한 참조 발생 self.name : alice RC + 1 |
alice = nil | 1 | alice 메모리 해제되지 않음 |
→ 순환 참조를 방지하려면, 클로저 내부에서 weak나 unowned 참조를 사용하여 강한 참조 순환을 끊어야 함
import Foundation
class Person {
var name: String
var greeting : String?
init(name: String) {
self.name = name
}
lazy var closure = {[weak self] in // 약한 참조 사용 Person RC가 증가하지 않음
print("My Name is \\(self?.name ?? "UnKnown")")
}
func getName() {
closure()
}
deinit {
print("\\(name) is deinitialized.")
}
}
var alice: Person? = Person(name: "Alice")
CFGetRetainCount(alice) // 2
alice?.getName()
CFGetRetainCount(alice) // 2
alice = nil // 메모리에서 해제, deinit 호출
시뮬레이션된 참조 카운트 변화
단계 | alice (Person) RC | 비고 |
초기 생성 | 1 | |
alice.getName() | 1 | |
closure() | 1 | 클로저에서 약한 참조 선언 (weak self) self.name : alice RC 증가하지 않음 |
alice = nil | 0 | alice 메모리 해제 |
참조 카운팅 확인 메서드
https://developer.apple.com/documentation/corefoundation/1521288-cfgetretaincount
CFGetRetainCount(_:) | Apple Developer Documentation
Returns the reference count of a Core Foundation object.
developer.apple.com
'Swift' 카테고리의 다른 글
Swift - GCD (Grand Central Dispatch) (3) | 2024.12.10 |
---|---|
Swift - Dispatch를 이용한 성능 개선 (1) | 2024.12.09 |
Swift - Struct와 Class 선택과 차이점 (0) | 2024.12.06 |
Swift - UICollectionViewFlowLayout 셀 크기 다르게 구현 (UILabel) (0) | 2024.12.02 |
Swift - KeyChain이란? (0) | 2024.11.24 |