ARC(Automatic Reference Counting) – Swift 메모리 관리의 핵심

이때 등장하는 것이 바로 ARC(Automatic Reference Counting)예요
ARC는 이름만 봐도 알 수 있듯이 자동으로 참조를 카운팅해 주는 메모리 관리 시스템이에요
Swift에서 클래스 인스턴스를 만들면 ARC가 알아서 메모리를 관리해 줍니다
근데 어떻게 관리한다는 거냐??
그럼 바로 살펴볼게요
ARC가 뭐길래?
ARC는 클래스 인스턴스의 참조 횟수를 추적해서 더 이상 필요 없는 인스턴스를 자동으로 메모리에서 해제해 줘요
Objective-C와 Swift에서 사용하는 메모리 관리 방식인데, 컴파일 타임에 retain과 release 코드를 자동으로 삽입해 줍니다
GC(Garbage Collection)와는 다르게 런타임이 아니라 컴파일 시점에 메모리 관리 코드가 들어가기 때문에 성능 오버헤드가 적어요
참조 카운트는 어떻게 세는 거냐?
클래스 인스턴스를 만들면 힙(heap) 영역에 메모리가 할당되고, 이 인스턴스를 참조하는 변수나 상수가 생길 때마다 참조 카운트(RC)가 1씩 증가해요
반대로 참조가 끊기면 RC가 1씩 감소하고, RC가 0이 되면 deinit이 호출되면서 메모리에서 해제됩니다
간단한 예시를 볼게요
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name)이 초기화됐어요")
}
deinit {
print("\(name)이 메모리에서 해제됐어요")
}
}
var reference1: Person? = Person(name: "철수") // RC: 1
var reference2 = reference1 // RC: 2
var reference3 = reference1 // RC: 3
reference1 = nil // RC: 2
reference2 = nil // RC: 1
reference3 = nil // RC: 0, deinit 호출
이미지 출처: 개발자 소들이 - 티스토리
이렇게 ARC가 자동으로 참조를 세고 메모리를 관리해 줘요
개발자가 직접 retain이나 release를 호출할 필요가 없어서 편하죠
값 타입은 ARC 대상이 아니에요
중요한 점은 ARC가 참조 타입(class)에만 적용된다는 거예요
struct, enum, Int, String 같은 값 타입은 스택(stack)에 저장되고 복사 방식으로 동작하기 때문에 ARC의 관리 대상이 아닙니다
struct Book {
var title: String
}
var book1 = Book(title: "스위프트")
var book2 = book1 // 값이 복사됨, ARC 관리 X
강한 순환 참조 문제
ARC를 사용하다 보면 강한 순환 참조(Strong Reference Cycle)라는 문제를 만날 수 있어요
두 인스턴스가 서로를 강하게 참조하면 RC가 0이 되지 않아서 메모리 누수가 발생합니다
class Person {
var apartment: Apartment?
deinit { print("Person 해제") }
}
class Apartment {
var tenant: Person?
deinit { print("Apartment 해제") }
}
var john: Person? = Person()
var unit4A: Apartment? = Apartment()
john?.apartment = unit4A
unit4A?.tenant = john
john = nil
unit4A = nil
// deinit이 호출되지 않음 - 메모리 누수 발생!
이런 순환 참조를 해결하려면 weak나 unowned를 써야 해요
weak 참조
weak는 약한 참조로, 참조하는 인스턴스의 RC를 증가시키지 않아요
참조하던 인스턴스가 해제되면 자동으로 nil이 됩니다
그래서 weak 참조는 항상 옵셔널 타입이어야 해요
class Apartment {
weak var tenant: Person? // weak로 순환 참조 방지
deinit { print("Apartment 해제") }
}
클로저에서도 weak를 자주 사용하는데요, 캡처 리스트로 self를 약하게 참조해 줘요
someAsyncOperation { [weak self] in
self?.doSomething() // self가 옵셔널이므로 ?로 접근
}
unowned 참조
unowned는 미소유 참조로, weak처럼 RC를 증가시키지 않지만 옵셔널이 아니에요
참조하는 인스턴스가 항상 존재한다고 가정하기 때문에 해제된 인스턴스에 접근하면 크래시가 발생합니다
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer // 카드는 항상 고객이 있어야 함
init(customer: Customer) {
self.customer = customer
}
}
unowned는 인스턴스의 생명주기가 명확할 때 사용하면 좋아요
클로저에서의 순환 참조
클로저는 참조 타입이라서 self를 캡처할 때 강한 참조가 생길 수 있어요
class ViewController {
var completionHandler: (() -> Void)?
func setupHandler() {
completionHandler = {
self.doSomething() // 강한 순환 참조 발생
}
}
}
이럴 때는 캡처 리스트로 weak나 unowned를 써줘야 해요
func setupHandler() {
completionHandler = { [weak self] in
guard let self = self else { return }
self.doSomething()
}
}
weak self를 사용하면 클로저가 실행될 때 self가 이미 해제됐을 수도 있으니까 옵셔널 바인딩으로 체크해 줘야 합니다
이미지 출처: 코딩하는 체대생 - 티스토리
ARC의 장점
ARC를 사용하면 수동으로 메모리 관리(MRC - Manual Reference Counting)할 필요가 없어요
컴파일러가 알아서 retain과 release를 삽입해 주니까 실수로 메모리 누수를 만들 가능성이 줄어들죠
그리고 컴파일 시점에 메모리 관리 코드가 삽입되기 때문에 런타임 성능도 좋아요
GC처럼 주기적으로 메모리를 스캔하지 않아도 되거든요
주의할 점
ARC는 자동이지만 완벽하지 않아요
강한 순환 참조 같은 문제는 개발자가 직접 해결해 줘야 합니다
특히 클로저나 델리게이트 패턴을 사용할 때 순환 참조가 생기기 쉬우니까 항상 주의해야 해요
메모리 프로파일링 툴로 주기적으로 체크하는 것도 좋은 습관이에요
정리하면, ARC는 Swift에서 메모리를 자동으로 관리해 주는 편리한 시스템이지만
순환 참조 같은 문제는 개발자가 직접 신경 써야 한다는 거예요
weak와 unowned의 차이를 이해하고 적재적소에 사용하면 메모리 누수 없는 안정적인 앱을 만들 수 있습니다!
이전 글 보기