본문 바로가기

WIL

[WIL] ARC(Automatic Reference Counting) 정리

Swift 공식 문서 중 ARC에 관한 내용 정리입니다.

 

ARC란?

Automatic Reference Counting의 약자로, Swift 언어에서 앱의 메모리 사용에 관해 추적(track)하고, 관리(manage)해주는 모델을 의미합니다.
ARC는 이름에서도 알 수 있듯이 오직 Reference 타입인 Class에서만 사용이 가능합니다.
Structure나 Enumeration은 Value 타입이므로 ARC가 적용되지 않습니다.
 

ARC 작동방식

기본적으로 Class의 Instance가 생성되면, ARC는 관련된 저장 속성(Stored Property)들에 대해 메모리를 할당하고, Class의 Instance가 더 이상 사용되지 않으면, ARC는 할당되었던 메모리르 해제시킵니다.

 

하지만 만약 ARC가 사용중인 Class의 Instance의 메모리를 해제시킨다면, 작동중이던 앱은 대부분 Crash가 발생할 것입니다.

 

이를 방지하기 위해 ARC는 각 Class Instance를 참조하는 속성(Property), 상수(Constant), 변수(Variable)의 수를 추적합니다.

 

그리고 이 때 생기는 참조를 "강한 참조(Strong Reference)" 라고 합니다.

 

ARC 예시

만약 아래 코드와 같이 Person 이란 Class가 정의하고, 이 Person 타입을 가진 변수 reference1, reference2, reference3가 선언한 경우가 있다고 가정해 봅시다.

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 Instance를 할당하면, Person Class의 생성자(Constructor)가 실행되면서 "John Appleseed is being initialized" 가 print 됩니다.

reference1 = Person(name: "John Appleseed")
// "John Appleseed is being initialized" print 됩니다.

 

마찬가지로 동일한 Person Instance를 변수 reference2, reference3에 각각 할당해주겠습니다.

이러면 Person Instance에 대한 강한 참조(Strong Reference)가 2개가 더 설정됩니다.

reference2 = reference1
reference3 = reference1

 

자 이제 그러면 메모리를 해제해 보겠습니다.

우선 변수 reference1과 reference2에 각각 nil을 할당하여, 각각의 Person Instance에 대한 강한 참조를 해제시켜보겠습니다.

reference1 = nil
reference2 = nil

여기서 알 수 있는 점은 아직 reference3에 Person Instance에 대한 강한 참조가 할당되어 있는 상태이기 때문에 Deinitializer가 호출되지 않았습니다.

 

따라서 아래와 같이 reference3 변수에 nil을 할당하면, Deinitializer가 호출되는 것을 알 수 있습니다.

reference3 = nil
// "John Appleseed is being deinitialized" 가 print 됩니다.

 

강한 순환 참조(Strong Reference Cycles)

강한 순환 참조(Strong Reference Cycle)란 두 개의 Class Instance가 서로에 대해 강한 참조(Strong Reference)를 가지고 있어 각각의 Instance가 계속 살아있는 상태를 말합니다.

강한 순환 참조 예시

아래 코드와 같이 세입자를 나타내는 Person Class와 이 세입자가 사는 아파트를 나타내는 Apartment Class가 선언했다고 가정해보겠습니다.

 

또한, 각각의 Class에는 서로의 Instance를 저장하는 변수가 각각 선언되어 있다고 가정해보겠습니다.

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment? // 세입자가 사는 아파트를 담는 Propery
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person? // 아파트에 사는 세입자를 담는 Propery
    deinit { print("Apartment \(unit) is being deinitialized") }
}

 

아래 코드 처럼 각각 Person Instance를 담을 john 변수와 Apartment Instance를 담을 unit4A 변수를 아래와 같이 선언하여 각각 Person Instance와 Apartment Instance를 할당해주었습니다.

var john: Person?
var unit4A: Apartment?

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

 

위 코드의 상태를 그림으로 표현하면 아래와 같습니다.

 

이와 더불어 john 변수의 Person Instance 내부에 선언되어 있는 apartment Property에는 unit4A변수의 Apartment Instance를, unit4A 변수의 Apartment Instance 내부에 선언되어 있는 tenant Property에는 john 변수의 Person Instance를 연결해줍니다.

// ! 는 john 및 unit4A 선택적 변수 내에 저장된 인스턴스를 풀고 액세스하는 데 사용되었습니다.
john!.apartment = unit4A
unit4A!.tenant = john

 

현재까지 강한 참조를 도식화하면 아래와 같습니다.

이 상태에서 john 변수와 unit4A 변수에 nil을 할당하여 각각의 강한 참조를 해제시켜보겠습니다.

john = nil
unit4A = nil

 

이렇게 되면 결론적으로 아래 그림과 같이 Person Instance와 Apartment 사이에 강한 참조가 남아있어 메모리가 누수(Memory Leak)되어 메모리가 해제되지 않는 현상을 보실 수 있습니다. 이러한 현상을 강한 순환 참조(Strong Reference Cycles)라고 일컬어 집니다.

 

강한 순환 참조 해결하는 방법

Weak Reference와 Unowned Reference를 사용하면 메모리 할당 시 Reference Counting(강한 참조 개수)를 증가시키지 않아 강한 순환 참조(Strong Reference Cycle)을 방지해 줄 수 있습니다.

Weak Reference

보통 다른 Instance가 더 짧은 Lifetime을 가졌을 때 사용됩니다.

 

위에 Apartment 예시(ARC 예시)를 다시 예로 들면, 세입자가 아파트에 거주하지 않는 기간도 있기 때문에 Apartment Instance 내부의 tenant Property를 weak와 함께 선언해주면 기존 발생하던 강한 순환 참조를 방지할 수 있습니다.

Unowned Reference

보통 다른 Instance의 Lifetime이 같거나 더 길 때 사용됩니다.

 

Weak Reference 예시

위에서 예시로 들었던 아파트와 세입자를 예(강한 순환 참조 예시)로 다시한번 들어보겠습니다.

 

아래 코드를 보시면 대부분 기존 예시와 동일하지만 Apartment의 tenant Property 부분만 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? // 달라진 부분
    deinit { print("Apartment \(unit) is being deinitialized") }
}

 

그리고 두 변수(john 및 unit4A)의 강한 참조(Strong Reference)와 두 Instance 간의 링크는 이전과 같이 생성하였습니다.

var john: Person?
var unit4A: Apartment?

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

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

 

지금까지 참조(Reference) 상태를 그림으로 확인해보면 아래와 같습니다.

위 그림을 보시면 Person Instance는 Apartment Instance에 대해 강한 참조를 하고 있지만, Aparment Instance는 Person Instance에 대해 약한 참조(Weak Reference)를 하고 있다는 것을 알 수 있습니다. 이 말은 john 변수가 가지고 있는 Person Instance에 대한 강한 참조를 nil 세팅함으로써 끊게 만들면, 더 이상 Person Instance에 대한 강한 참조가 존재하지 않게 된다는 의미입니다.

john = nil
// "John Appleseed is being deinitialized" 프린트 됩니다.
아래 그림은 Person Instance에 대한 모두 해제된 후, 모습을 도식화한 것입니다.

 

이제 Apartment Instance에 대한 강한 참조는 오직 unit4A 변수만 남았으므로, 아래 코드와 같이 unit4A 변수에 nil을 할당하면 모든 강한 참조가 사라지게 됩니다.

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

 

아래 그림은 모든 참조가 해제된 후 모습을 도식화한 그림입니다.

 

Unowned Reference 예시

비소유 참조(Unowned Reference)는 약한 참조(Weak Reference)와 마찬가지로 강한 참조(Strong Reference)를 피하기 위한 용도로 사용됩니다.
하지만 비소유 참조는 약한 참조와 달리 다른 Instance(참조하고 있는 Instance)가 생존기간(Lifetime)이 같거나 더 긴 경우에 사용된다는 차이가 있습니다.
이와 더불어 비소유 참조는 항상 값을 소유(hold)하고 있어야 하므로 옵셔널(Optional) 타입으로 선언될 수 없습니다.
 
 
이제 예를 들어 한번 비소유 참조에 관해 설명해보겠습니다.
 
아래 코드를 보시면 Customer Class와 CreditCard Class가 선언되어 있는데요. 여기선 Customer Class는 '신용카드하는 고객' 을 나타내고, CreditCard는 '고객에게 사용될 카드' 라고 가정해 보겠습니다.
 
CreditCard Class 선언부를 살펴보시면 unowned 키워드로 선언된 customer를 확인해 보실 수 있습니다. 이는 카드는 항상 사용자에 의해 사라지고 없어지는 관계이므로 Customer Instance를 가리킬 customer 프로퍼티(Property)는 비소유 참조 키워드인 unowned로 선언되는 것을 알 수 있습니다.
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
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

 

자 이제 john 이라는 고객이 카드를 새로 발급받는다고 가정해보겠습니다.

 

이는 코드로 작성하면 아래와 같이 작성될 수 있습니다.

var john: Customer?

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

 

위 코드를 보시면 john은 카드번호가 '1234_5678_9012_3456' 인 카드를 새로 발급받았다는 것을 알 수 있습니다.

 

그리고 위 코드를 도식화해보자면 아래와 같습니다.

위 그림을 살펴보시면 Customer Instance는 CreditCard에 대한 강한 참조를 가지고 있으며, 반대로 CreditCard는 Customer Instance에 대한 비소유 참조를 가지고 있다는 것을 알 수 있습니다.

 

여기서 CreditCard Instance에 있는 customer 프로퍼티(Property)는 Customer Instance에 대해 비소유 참조를 가지고 있기 때문에, 만약 john 변수에 nil을 할당하여 Customer Instance에 대한 강한 참조를 해제한다면, 더 이상 Customer Instance에 대한 강한 참조가 남아 있지 않게 됩니다.

 

이를 도식화하면 아래와 같습니다.

 

아래 코드와 같이 john 변수에 nil을 할당하게 되면 Customer Instance와 CreditCard Instance가 모두 메모리에서 해제되어 각각의 Instance에 선언되어 있는 Deinitializer가 실행되는 것을 알 수 있습니다.

john = nil
// "John Appleseed is being deinitialized" 가 콘솔에 프린트됩니다.
// "Card #1234567890123456 is being deinitialized" 가 콘솔에 프린트됩니다.

 

클로저에서의 강한 순환 참조

위에서처럼 두 인스턴스(Instance) 사이에서 강한 순환 참조(Strong Reference Cycle)가 발생하는 것처럼 한 인스턴스와 클로저(Closure) 사이에서도 클래스(Class) 인스턴스의 프로퍼티(Property)가 클로저를 할당하고 클로저 바이(Body)에서 해당 인스턴스를 캡처(Capture)하는 경우, 강한 순환 참조가 발생할 수 있습니다.

 

이는 클로저 역시 클래스와 마찬가지로 Reference Type이기 때문입니다.

 

더 쉬운 이해를 위해 예시를 한번 들어보겠습니다.

 

아래 예시는 HTML 요소(Element)를 생성해주는 HTMLElement 클래스의 선언부입니다.

 

아래 코드에서 주목하실 부분은 lazy로 선언된 asHTML 클로저 프로퍼티(Closure Property)가 정의된 부분입니다.

class HTMLElement {
    let name: String
    let text: String?
    
    // 클로저 프로퍼티가 정의된 부분인입니다.
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

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

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

 

이제 HTML을 생성하고, HTML tag로 출력해보도록 하겠습니다.

 

아래 코드와 같이 HTML 요소를 생성하면 paragraph 변수에 HTMLElement 인스턴스에 대한 강한 참조가 생성되고, 이후 이 HTMLElement 인스턴스 내부에 선언되어 있는 클로저 프로퍼티인 asHTML을 실행해주면, 이 순간 HTMLElement 인스턴스와 클로저가 서로 강한 참조를 하게 됩니다.

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// "<p>hello, world</p>" 가 프린트 된다는 것을 알 수 있습니다.

 

위 코드를 그림으로 표현해보면 아래와 같습니다.

 

아래 그림을 보시면 HTMLElement 인스턴스(Instance)와 클로저(Closure) 사이에 강한 순환 참조가 형성되어 있는다는 사실을 알 수 있습니다.

여기서 또 알 수 있는 사실은 만약 위 그림의 상태에서 아래 코드와 같이 paragraph 변수에 nil을 할당하여 HTMLElement 인스턴스에 대한 강한 참조를 해제시키더라도 HTMLElement 인스턴스와 클로저 사이에 강한 순환 참조로 인해 HTMLElement 인스턴스에 선언되어 있는 Deinitializer가 실행되지 않다는 것을 알 수 있습니다.

paragraph = nil

캡처 리스트 정의

반환 타입(Return Type)이 존재하는 경우

lazy var someClosure = {
        [unowned self, weak delegate = self.delegate]
        (index: Int, stringToProcess: String) -> String in
    // 클로저 바디
}

반환 타입(Return Type)이 존재하지 않는 경우

lazy var someClosure = {
        [unowned self, weak delegate = self.delegate] in
    // 클로저 바디
}

캡처 리스트에서의 약한 참조와 비소유 참조

캡처 리스트에서 비소유 참조는 항상 클로저와 클로저가 캡처하고 있는 인스턴스가 서로를 참조하고 있을 때, 항상 동시에 메모리 해제가 일어납니다.
반면에 약한 참조는 캡처된 참조가 메모리에서 해제될 때 nil 을 할당합니다. 이는 약한 참조의 존재를 클로저 바디 내부에서 확인할 수 있도록 해줍니다.
여기서 주목해야할 점은 비소유 참조를 사용하는 경우는 오직 캡처된 참조가 nil이될 경우가 확실히 발생하지 않을 경우에만 사용되어야 한다는 점입니다.