Swift 메모리 관리 이해하기: 자동 참조 카운트(ARC)의 작동 원리

작성일 :

Swift 메모리 관리 이해하기: 자동 참조 카운트(ARC)의 작동 원리

Swift는 메모리 관리를 쉽게 하기 위해 자동 참조 카운트(Automatic Reference Counting, ARC)라는 메커니즘을 도입했습니다. 이 글에서는 ARC의 기초 개념부터 작동 원리, 그리고 관련된 현대적인 메모리 관리 기술까지 다루어 보겠습니다. Swift의 메모리 관리 방법론을 이해하면 메모리와 관련된 문제를 예방하고 더 효율적인 코드를 작성할 수 있습니다.

ARC의 기본 개념

자동 참조 카운트(ARC)은 Swift에서 인스턴스의 메모리 생명 주기를 관리하는 주요 메커니즘입니다. 이 메커니즘은 객체가 더 이상 사용되지 않을 때 자동으로 메모리를 해제하여 메모리 누수를 방지합니다. ARC는 객체의 참조 카운트를 추적하여 객체가 필요한 동안 메모리에 상주하도록 하고, 필요 없을 때 메모리에서 제거합니다.

ARC 작동 원리

ARC는 객체의 참조 카운트를 추적함으로써 작동합니다. 객체의 참조 카운트는 다음과 같은 경우 변경됩니다:

  • 증가: 변수가 객체를 참조할 때마다 객체의 참조 카운트가 증가합니다.
  • 감소: 변수가 더 이상 객체를 참조하지 않을 때마다 객체의 참조 카운트가 감소합니다.

ARC를 통해 메모리가 해제되는 주요 상황은 다음과 같습니다:

  • 참조 카운트가 0이 되는 경우: 객체가 더 이상 참조되지 않을 때 메모리가 해제됩니다.
swift
class Person {
    let name: String

    init(name: String) {
        self.name = name
        print("
        (name) is being initialized")
    }
    
deinit {
        print("
        (name) is being deinitialized")
    }
}

var reference1: Person?
var reference2: Person?
var reference3: Person?


reference1 = Person(name: "John
Doe")
// 참조 카운트: 1

reference2 = reference1
// 참조 카운트: 2

reference3 = reference1
// 참조 카운트: 3

reference1 = nil
// 참조 카운트: 2

reference2 = nil
// 참조 카운트: 1

reference3 = nil
// 참조 카운트: 0
// 메모리 해제

이 예제는 Person 객체의 생명 주기를 잘 보여줍니다. 객체의 참조가 모두 해제될 때, 메모리도 해제됩니다.

강한 참조 사이클 방지하기

때로는 두 객체가 서로를 강하게 참조하여 강한 참조 사이클(strong reference cycle)이 발생할 수 있습니다. 이는 메모리 누수를 일으킬 수 있기 때문에 이를 방지하는 것이 중요합니다. 강한 참조 사이클을 방지하기 위해 Swift에서는 약한 참조(weak reference)미소유 참조(unowned reference)를 사용합니다.

약한 참조 (weak reference)

약한 참조는 weak 키워드를 사용합니다. 약한 참조는 참조하고 있는 객체의 참조 카운트를 증가시키지 않으며, 참조하고 있는 객체가 해제되면 nil로 설정됩니다. 따라서 약한 참조는 항상 옵셔널이어야 합니다.

swift
class Person {
    let name: String

    init(name: String) {
        self.name = name
    }

    var apartment: Apartment?
}

class Apartment {
    let unit: String

    init(unit: String) {
        self.unit = unit
    }

    weak var tenant: Person?
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Doe")
unit4A = 
Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
// Person 객체 메모리 해제

print(unit4A!.tenant) // nil

미소유 참조 (unowned reference)

미소유 참조는 unowned 키워드를 사용합니다. 미소유 참조는 참조하고 있는 객체의 참조 카운트를 증가시키지 않으며, 참조하고 있는 객체가 해제되어도 nil로 설정되지 않습니다. 따라서 미소유 참조는 옵셔널이 아니며, 참조하고 있는 객체가 해제되면 접근할 때 크래시가 발생할 수 있습니다.

swift
class Customer {
    let name: String
    var card: CreditCard?

    init(name: String) {
        self.name = name
    }

deinit {
        print("
        (name) is being deinitialized")
    }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer

    
init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }

init {
        print("
        Card #
        (number) is being deinitialized")
    }

var alice: Customer?
alice = Customer(name: "Alice
Smith")

alice!.card = CreditCard(number: 1234_5678_9012_3456,
alice)

alice = nil
// Customer와 CreditCard 객체 메모리 해제

폐웨어와 강한 참조 사이클 방지하기

강한 참조 사이클은 Swift의 선거처리계와 비동기 클로저에서도 발생할 수 있습니다. 비동기 클로저 내부에서 self강하게 참조할 경우, 이를 회피하기 위해 클로저 참조를 핀다 웨서 파질리 되고 강환참 구조를 방지하는 방 수 있습니다.

swift
class HTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<
            self.name>
            < 
            self.text!>

        } else {
            return "<
            self.name>"
        }
    }

init(name: String, text: String? = null) {
        self.name = name
        self.text = text
    }

deinit {
        print("
        (name) is being deinitialized")
    }
}

var paragraph: HTMLElement? = HTMLElement(name "p", text: "hello")
print(paragraph!.asHTML())
paragraph = nil
// HTMLElement 객체 메모리 해제

결론

Swift의 자동 참조 카운트(ARC)는 메모리 관리를 간결하고 안전하게 해 주지만, 원치 않은 강한 참조 사이클은 예기치 않은 메모리 문제를 일으킬 수 있습니다. 강한 참조 사이클을 피하기 위해 약한 참조(weak reference)미소유 참조(unowned reference)를 잘 활용하고, 특히 클로저를 사용할 때 주의해야 합니다. 이러한 개념들을 확실히 이해하고 적용하면 더 안전하고 효율적인 Swift 코드를 작성할 수 있습니다.