Swift

Swift - ARC (Automatic Reference Count) 정리 및 강한 순환 참조 해결

Goniii 2024. 12. 8. 17:28

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의 특징

  1. 자동 메모리 관리
    • 프로그래머가 명시적으로 메모리를 해제하지 않아도 자동으로 관리해줌
    • Swift 컴파일러가 코드를 분석하여 객체의 라이프스타일을 추적하고 메모리를 자동으로 관리함
  2. 효율성
    • 불필요한 메모리 낭비를 줄임
  3. 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)

  1. 약한 참조 (weak)
    • weak 키워드는 참조 카운트를 증가 시키지 않음
    • 참조 대상이 해제 되면 자동으로 nil이 됨
    • 옵셔널로 선언해야 함
  2. 비소유 참조 (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 호출되지 않음
  1. 클로저 내부에서 객체 참조
    • 클로저가 실행될 때, 클로저 내부에서 self(또는 다른 객체)를 참조하는 경우, 이 객체는 클로저에 의해 캡처
    • 클로저는 객체를 강하게 참조
  2. 강한 참조 순환
    • 클로저와 객체가 서로 강하게 참조하는 상태가 되어 두 객체는 메모리에서 해제 될 수 없음 → 메모리 누수 발생

 

시뮬레이션된 참조 카운트 변화

단계 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