Automatic Reference Counting(ARC)은 Swift와 Objective-C의 메모리 관리 시스템으로, 각 객체에 대한 참조 수를 자동으로 계산하고 카운터가 0에 도달하면 객체를 해제합니다. Apple Swift 문서, 2026에 따르면 ARC는 컴파일러에 내장되어 컴파일 타임에 작동하며 적절한 위치에 retain/release 호출을 삽입합니다. Garbage Collection과 달리 ARC는 별도의 컬렉터 스레드가 필요하지 않으며 애플리케이션 실행 중에 일시 중지를 만들지 않습니다.
핵심 사항
ARC(Automatic Reference Counting)는 Apple이 Xcode 4.2(2011)에서 Objective-C용으로 도입하고 Swift가 계승한 컴파일러 기반 메모리 관리 메커니즘입니다. 수동 메모리 관리(Manual Retain-Release, MRR)와 달리 ARC는 retain, release, autorelease 호출을 완전히 자동화하여 개발자의 개입 없이 컴파일 타임에 삽입합니다.
ARC는 가비지 컬렉터가 아닙니다. 동적 코드 삽입이 있는 정적 분석입니다. 컴파일러는 객체 수명을 분석하고 객체가 생성, 복사 또는 범위를 벗어나는 지점에 retain/release를 배치합니다. 결과는 결정론적 메모리 해제입니다. 객체는 지연이나 일시 중지 없이 더 이상 참조가 없을 때 정확히 삭제됩니다.
WWDC 2011 Session 323에 따르면 MRR에서 ARC로 전환하면 Apple 애플리케이션에서 메모리 관련 충돌 버그가 70% 감소했습니다. 개발자는 수동으로 retain/release 균형을 맞추지 않아도 되어 누수 및 double-free 오류의 전체 클래스가 제거되었습니다.
메모리의 각 객체에는 참조 카운터(retain count)가 있습니다. 객체가 생성되면 카운터는 1로 설정됩니다. 새로운 strong 참조가 객체를 가리키면 카운터가 증가하고(retain), strong 참조가 사라지면 카운터가 감소합니다(release). 0에 도달하면 객체가 즉시 해제됩니다.
Swift 컴파일러는 모든 할당에서 retain/release를 삽입하지 않습니다. 최적화를 위해 정적 분석을 사용합니다. 예를 들어 객체가 전달된 후 사용되지 않음이 보장되면 컴파일러는 불필요한 release/retain을 건너뛸 수 있습니다. 이 최적화를 ARC Optimization이라고 합니다.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) initialized (retain count: 1)")
}
deinit {
print("\(name) deallocated")
}
}
func testARC() {
let p = Person(name: "Alice") // retain count = 1
let q = p // retain count = 2
// q가 범위를 벗어남
// retain count = 1
// p가 범위를 벗어남
// retain count = 0 → deinit
}
이 예제는 ARC가 카운터를 관리하는 방법을 보여줍니다. q = p를 할당하면 카운터가 증가하고, q가 범위를 벗어나면 감소합니다. 마지막 strong 참조가 사라지면 소멸자가 즉시 호출됩니다. 가비지 컬렉터가 기다리지 않고 메모리가 즉시 해제됩니다.
ARC와 Garbage Collection은 동일한 문제인 자동 메모리 관리를 해결하지만 근본적으로 다른 접근 방식을 사용합니다. 선택은 언어 아키텍처를 결정합니다: Swift(ARC) vs Java/Go(GC). 주요 차이점을 살펴보겠습니다.
| 특성 | ARC(Swift/ObjC) | GC(Java/Go) |
|---|---|---|
| 해제 시점 | 결정론적: 카운터가 0이 되면 즉시 | 비결정론적: 다음 수집 주기에서 |
| 실행 일시 중지 | 없음(retain/release가 컴파일 타임에 삽입됨) | Stop-The-World 일시 중지(2~200ms) |
| 오버헤드 | 각 참조에서 카운터 증가/감소 | 객체 그래프 순회, 마킹, 스위핑 |
| 문제점 | Retain Cycle(수동 해결) | 힙 단편화, 잊혀진 참조로 인한 누수 |
| 추가 스레드 | 필요 없음 | 가비지 컬렉터 스레드 필요 |
핵심 절충: ARC는 예측 가능한 객체 수명과 제로 일시 중지를 제공하지만 개발자가 retain cycle을 이해하고 weak/unowned를 올바르게 선택해야 합니다. GC는 개발자를 이러한 문제에서 해방시키지만 비결정론적 일시 중지와 추가 스레드의 비용이 듭니다.
ARC는 세 가지 유형의 참조 한정자를 정의하며, 각각 카운터와 객체 수명 주기에 다르게 영향을 미칩니다. 올바른 한정자를 선택하는 것은 Swift에서 안전한 메모리 관리의 기초입니다.
Strong은 기본 한정자입니다. 각 strong 참조는 객체의 retain count를 1 증가시킵니다. 하나 이상의 strong 참조가 존재하는 한 객체는 살아 있습니다. Swift의 모든 클래스 속성과 지역 변수는 기본적으로 strong입니다. strong 참조는 소유 관계를 만듭니다: 객체 A가 객체 B를 소유합니다.
Weak은 retain count를 증가시키지 않는 참조입니다. weak 참조가 객체를 가리키고 있어도 객체가 해제될 수 있습니다. 해제 후 weak 참조는 자동으로 nil로 설정됩니다. weak 참조는 항상 var로 옵셔널 타입(?)과 함께 선언됩니다. 특히 delegate 패턴에서 retain cycle을 끊는 데 사용됩니다.
Unowned는 weak과 마찬가지로 retain count를 증가시키지 않는 비소유 참조입니다. 그러나 unowned 참조는 해제 후 nil로 설정되지 않습니다. 해제된 객체에 접근하면 크래시가 발생합니다. Unowned는 객체가 참조하는 객체보다 최소한 같은 기간 동안 살아 있음이 보장될 때 사용됩니다. 일반적인 사용 사례는 클로저(closures)와 보장된 수명을 가진 부모-자식 관계입니다.
class Customer {
let name: String
var card: CreditCard? // strong
init(name: String) { self.name = name }
deinit { print("\(name) deallocated") }
}
class CreditCard {
let number: String
unowned let customer: Customer // unowned — 소유하지 않음
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card \(number) deallocated") }
}
var customer: Customer? = Customer(name: "Bob")
customer?.card = CreditCard(number: "1234", customer: customer!)
customer = nil
// Customer와 CreditCard 모두 해제 — retain cycle 없음
여기서 CreditCard는 Customer에 대한 unowned 참조를 사용합니다. Customer는 카드를 소유하고(strong), 카드는 고객을 소유하지 않습니다(unowned). Customer가 해제되면 두 객체 모두 해제됩니다. retain cycle이 발생하지 않습니다. card.customer가 strong이었다면 사이클이 해제를 차단했을 것입니다.
자동화에도 불구하고 ARC는 만능이 아닙니다. 개발자는 내부 메모리 관리 메커니즘에 대한 이해가 필요한 몇 가지 일반적인 문제에 직면합니다.
Swift의 클로저(closures)는 strong 참조로 외부 변수를 캡처합니다. 클로저가 클래스 속성에 할당되고 self를 캡처하면 retain cycle이 발생합니다: 클래스가 클로저를 보유하고 클로저가 self를 보유합니다. 해결책은 weak 또는 unowned를 사용한 캡처 목록입니다.
class NetworkManager {
var completionHandler: ((Data?) -> Void)?
var data: Data?
func fetchData() {
completionHandler = { [weak self] result in
guard let self else { return }
self.data = result
self.processResult()
}
}
func processResult() { }
}
캡처 목록 [weak self]는 클로저 내에서 self에 대한 weak 참조를 만듭니다. 이는 잠재적인 retain cycle을 끊습니다. Guard let self는 코드 실행 전에 객체가 살아 있음을 보장합니다. weak self는 Swift에서 비동기 클로저의 표준 관행입니다.
retain/release는 가벼운 작업이지만 핫 루프에서 빈번한 카운터 증가/감소는 오버헤드를 추가합니다. Swift 5.9+에서 컴파일러는 분석기가 안전함을 증명하면 중복 retain/release를 제거하는 최적화를 사용합니다. 그러나 Objective-C에서 retain/release는 초당 수백만 호출이 있는 고부하 시나리오에서 여전히 병목이 될 수 있습니다.
Autorelease Pool은 Objective-C와 일부 Swift 시나리오에서 사용되는 지연 해제 메커니즘입니다. 객체는 풀에 배치되고 풀이 비워질 때 release를 받습니다. 많은 임시 객체(예: JSON 파싱)가 있는 루프에서 사용자 지정 autoreleasepool을 만들면 피크 메모리 소비가 줄어듭니다.
자주 묻는 질문
수동 관리(MRR)에서는 개발자가 명시적으로 retain, release, autorelease를 호출했습니다. ARC는 이러한 호출을 컴파일 타임에 자동으로 삽입하여 double-free 위험, release 누락으로 인한 누수, retain/release 균형 오류를 제거합니다.
ARC는 Objective-C 객체와 Swift 클래스만 관리합니다. C/C++ 구조체와 포인터에는 ARC가 적용되지 않습니다. 이러한 객체는 수동으로 또는 C++ 스마트 포인터(shared_ptr, unique_ptr)를 통해 관리됩니다. Core Foundation 객체(CFString, CGColor)도 ARC 대상이 아닙니다.
weak — 객체가 참조하는 객체보다 먼저 해제될 수 있는 경우(델리게이트, 비동기 클로저). unowned — 객체가 참조하는 객체보다 최소한 같은 기간 동안 살아 있음이 보장되는 경우(자식이 부모 없이 존재할 수 없는 부모-자식 관계). 확실하지 않으면 weak을 선택하세요.
Swift의 실존 타입(프로토콜을 타입으로 사용)은 값을 특수 컨테이너(실존 컨테이너)에 래핑합니다. 이는 프로토콜 경계에서 retain/release 수를 증가시킵니다. Swift 5.7+에서 불투명 결과 타입(opaque result type)과 some 매개변수는 컨테이너를 제거하여 오버헤드를 줄입니다.
Swift에서 retain count를 읽는 직접적인 API는 없습니다. 구현 세부 사항으로 간주됩니다. 진단을 위해 Xcode의 Instruments(Allocations, Leaks) 또는 메모리 디버거를 사용하세요. 이러한 도구는 라이브 클래스 인스턴스 수와 보유 체인을 보여줍니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.