개발아 담하자

[iOS] Memory Leak (1) : 강한 순환 참조 본문

📱 iOS

[iOS] Memory Leak (1) : 강한 순환 참조

choidam 2021. 11. 27. 16:10

deinit

메모리 릭에 대해 알아보기에 앞서, deinit 함수에 대해 알아보겠습니다.

deinit 은 클래스의 인스턴스가 메모리에 해제될 때 즉시 호출되는 함수입니다. (클래스 타입에서만 작성 가능)

이 클래스 인스턴스가 메모리에서 해제되는 시점은 ARC 의 규칙에 따라 결정되는데, ARC는 클래스 인스턴스가 더 이상 필요없을 때 자동으로 메모리 해제합니다.

즉, deinit 이 호출되지 않는 경우 = ARC에서 메모리가 해제되지 않음 = 메모리 누수 (Memory Leak, Retain Cycle) 이 발생하고 있다는 것을 의미합니다.

 

ARC

그렇다면 ARC 는 정확히 무엇이고, 어떤 규칙으로 인스턴스를 메모리에서 해제시킬까요?

ARC 란 Automatic Referencing Counting의 약자입니다. 이름 그대로 각 인스턴스가 얼마나 참조되고 있는지를 추적하고 저장함으로써 메모리를 관리합니다.

 

만약 참조 counting 이 1 이상이면 인스턴스는 메모리에 계속 남아있고, 0이 되었을 때 메모리에서 해제됩니다. 참조 counting은 강한 참조가 될 때 +1 만큼 더해집니다.

Swift 는 이 ARC 를 활용해서 자동으로 앱의 메모리 사용을 추적하고 관리합니다.

 

그렇다면 언제 메모리 누수가 일어나는지(언제 deinit이 호출되지 않는지) 살펴봅시당

예제는 swift 공식 문서를 참조했습니다.

 

Automatic Reference Counting - The Swift Programming Language (Swift 5.5)

 

Automatic Reference Counting — The Swift Programming Language (Swift 5.5)

Automatic Reference Counting Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management your

docs.swift.org

 

1. 서로 강한 참조를 할 때

두 인스턴스의 강한 참조로 순환이 이루어질 때 메모리 릭이 발생합니다.

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

Person, Apartment 클래스가 있다고 가정합시다.

 

var john: Person?
var unit4A: Apartment?

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

위와 같이 할당하는 경우 john은 Person에, unit4A는 Apartment 에 강한참조를 하고 있습니다. (Person reference count = 1, Apartment reference Count = 1)

john = nil, unit4A = nil 을 할당하면 reference Count 가 각각 0이 되므로 deinit 함수가 무사히 호출됩니다. 즉, 메모리 릭이 일어나지 않습니다.

 

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

이 경우는 강한 순환 참조가 일어나는 경우입니다.

Person 인스턴스는 Apartment를 강한 참조 하고 있고, Apartment 인스턴스는 다시 Person을 강한 참조하고 있습니다. (Person reference count = 2, Apartment reference count = 2)

그래서 john = nil, unit4A = nil 을 해도 둘 다 reference count = 1 이기 때문에 이 두 개의 인스턴스는 메모리에서 해제되지 않습니다. → 이는 메모리 누수로 이어집니다.

 

이런 경우 약한 참조(weak)미소유 참조(unowned) 를 사용해야 합니다.

 

약한 참조

약한 참조를 사용하면 Reference Count를 올리지 않습니다.

약한 참조는 변수 앞에 weak 를 선언함으로써 사용 가능합니다.

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person? // 🔥 약한 참조 weak 사용!! 🔥
    deinit { print("Apartment \(unit) is being deinitialized") }
}

Person 은 Apartment를 여전히 강한 참조를 하고 있지만, Apartment는 Person에 대해 약한 참조를 하고 있습니다. (Person Reference Count: 1, Apartment Reference Count: 2)

 

john = nil
// Prints "John Appleseed is being deinitialized"

이 경우 john = nil 을 할당하면 Person 인스턴스의 Reference Count 값이 0이 되므로 무사히 메모리가 해제 됨을 확인할 수 있습니다. (Person Reference Count: 0, Apartment Reference Count: 1)

 

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

다시 unit4A = nil 까지 할당하면 Apartment 인스턴스의 Reference Count 값 까지 모두 0이 됩니다. 즉 두 인스턴스 모두 메모리에서 무사히 해제 됩니다.

 

미소유 참조 (Unowned Reference)

미소유 참조 역시 참조를 강하게 유지하지않음으로써 강한 참조 순환이 발생하는 것을 방지합니다. 다만 미소유참조는 다른 인스턴스의 수명이 같거나 수명이 더 길 때 사용됩니다. 또 약한 참조와 다르게 미소유 참조는 항상 값을 가질 것으로 예상되어야 합니다. (nil이 될 수 없기 때문 → Optional 로 선언되어서는 안 됨)

 

감이 잘 안 잡히므로 예제를 살펴봅시당

 

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 // 🔥 미소유 참조 unowned 사용!!
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

Customer 의 경우 Credition Card 를 소지할 수도 안 할 수도 있지만 (옵셔널로 설정),

카드의 경우 무조건 고객이 있어야만 합니다. 그리고 카드는 결코 고객의 수명보다 오래 지속될 수 없습니다.

 

var john: Customer?

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

Customer 인스턴스는 CreditCard 를 강한참조 하고 있고, CreditCard 인스턴스는 Customer 를 미소유 참조를 하고 있습니다. 그래서 Customer 가 메모리에서 해제될 때 무조건 CreditCard 역시 메모에서 해제됩니다.

 

여기까지 Memory Leak 이 일어나는 경우인 강한 순환 참조와 해결 방법(weak, unowned) 를 알아봤는데용

다음엔 메모리 릭이 일어나는 또 다른 경우인 클로저 캡처, delegate 에 대해 작성하도록 하겠습니다.

 

너무 길어서 끝!