ARC란? 클래스 인스턴스간 순환 참조 [Part-2]

클래스 인스턴스간 강한 순환 참조 (Strong Reference Cycles Between Class Instances)

Part-1 에서 ARC원리와 작동방식에대해 알아 보았습니다.
ARC는 생성된 인스턴스를 추적하고 더이상 사용하지 않는 인스턴스는 자동으로 메모리에서 해제되게 합니다.
하지만 절대로 메모리에서 해제 되지 않는 메모리 누수(memory leak)의 상황이 발생할수도 있습니다.
이는 두 개 이상의 클래스 인스턴스가 서로 강한참조를 할 경우 발생합니다.
이를 강한순환참조*순환참조 라고 합니다.

강한참조 : strong reference
약한참조: weak reference
미소류 참조: unowned reference
강한순환참조: strong reference cycle

참조 횟수 : reference count

다음은 개발자의 실수로 강한순환참조을 만들게 되는 상황의 예제입니다.
이 예제에서는 Apartment 와 Person 이라는 두개의 클래스를 정의 합니다.

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") }
}

Person 인스턴스는 String 타입의 name 이라는 퍼로퍼티를 가지고 옵셔널형의 apartment 프로퍼티를 가지고 있습니다. (모든 사람이 아파트를 가지고 있지는 않으니까요)

Apartment 인스턴스는 String 타입의 unit 이라는 프로퍼티를 가지고 있습니다. 또한 옵셔널 Person 타입의 tenant 도 가지고 있습니다. tenant 가 옵셔널인 이유는 모든 아파트에 세입자가 있지는 않기 때문입니다.

이 두 클래스 모두 deinitializer(deinit)를 정의하여 해당 클래스의 인스턴스가 해제되고 있다는 사실을 프린트합니다. 이를 통해 Person 및 Apartment의 인스턴스가 예상대로 메모리에서 해제되는지 여부를 확인할 수 있습니다.

다음 코드는 john 및 unit4A라는 옵셔널유형의 두 변수를 정의하며, 아래의 특정 Apartment 및 Person 인스턴스로 설정됩니다. 이 두 변수는 옵셔널이기 때문에 초기 값이 nil입니다.

var john: Person?
var unit4A: Apartment?

이제 Person 인스턴스와 Apartment 인스턴스를 만들고 이러한 새 인스턴스를 john 및 unit4A 변수에 할당 할 수 있습니다.

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

아래는 강한참조가 어떻게 두 인스턴스에 할당 되는지 볼수 있습니다.
john 변수에는 Person 인스턴스에 대한 강한 참조가 생기고, unit4A 변수에는 Apartment 인스턴스에 대한 강한 참조가 생기는 것을 볼수 있습니다.

이제 두 인스턴스를 함께 연결하여 아파트를 소유하고 아파트에 세입자를 들일수 있습니다.
john 은 옵셔널형이기 때문에 !를 붙여 강제로 값을 꺼내어*언랩(unwrap) john 인스턴스 내부의 값에 접근하게 합니다.
[옵셔널 바인딩 알아보기]

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

두 인스턴스를 함께 연결 한 후 강한참조가 되는 방식은 다음과 같습니다.

불행히도 두 인스턴스를 연결하면 두 인스턴스간 강한순환참조가 생성됩니다. Person 인스턴스는 Apartment 인스턴스에 대한 강한 참조 발생하고 Apartment 인스턴스는 Person 인스턴스에 대한 강한 참조가 생성됩니다.
즉 서로가 서로를 강한참조 하고 있다는 것입니다.

john = nil
unit4A = nil

실제로 john 및 unit4A 변수를 nil로 설정하여도 deinitializer가 호출되지 않습니다.
강한순환참조는 Person 및 Apartment 인스턴스가 메모리에서 해제되는 것을 방지하여 앱에서 메모리 누수(memory leak)를 유발합니다.

다음은 john 및 unit4A 변수를 nil로 설정 한 후에도 강한 참조가 발생하게 되는 상황을 표현한 모습입니다.

john 은 Person 인스턴스에 대한 강한 참조를 끊었고 unit4A 또한 Apartment 인스턴스와 강한 참조를 끊었지만 Person 인스턴스와 Apartment 인스턴스는 서로를 강한참조를하고 있어서 서로의 참조가 유지되며 끊을 수 없게 됩니다.

따라서 john 및 unit4A 변수가 보유한 강한 참조를 끊어도 Person 인스터스와 Apartmenr 인스턴스는 레퍼런스 카운트가 0으로 떨어지지 않고 1로 남아 ARC가 해당 인스턴스를 메모리에서 해제하지 않게 되는 것입니다.


그럼 어떻게 순환참조의 문제를 해결할 수 있는지 알아보겠습니다.

클래스 인스턴스간 순환참조 문제의 해결 (Resolving Strong Reference Cycles Between Class Instances).

Swift는 클래스 유형의 속성으로 작업 할 때 순환참조를 해결하는 두 가지 방법, 즉 약한 참조(weak references)미소유 참조(unowned references)를 제공합니다.

약한 참조와 미소유 참조를 사용하면 순환참조의 한쪽 인스턴스가 강한 참조없이 다른 인스턴스를 참조 할 수 있습니다. 그런 다음 인스턴스는 강한순환참조 상황을 만들지 않고도 서로를 참조 할 수 있습니다.

약한 참조는 다른 인스턴스의 수명이 더 짧은 경우 즉, 다른 인스턴스가 먼저 메모리에서 해제 될수있는 경우 사용합니다. 위의 아파트 예제에서 Apartment 인스턴스는 tenant가 없을 수도 있기 때문에 (먼저해지) tenant 를 약한 참조로 사용하는것이 더 적절할것입니다. 반대로 다른 인스턴스의 수명이 동일하거나 수명이 더 긴 경우는 미소유 참조를 사용합니다.

약한참조 (Weak References)

약한 참조로 선언하면 참조하고 있는 것이 먼저 메모리에서 해제되기 때문에 ARC는 약한 참조로 선언된 참조 대상이 해지 되면 런타임에 자동으로 참조하고 있는 변수에 nil을 할당합니다.
또한 약한 참조는 런타임에 값이 nil로 변경되도록 허용해야하므로 항상 옵셔널 유형으로 상수가 아닌 변수로 선언되어야합니다.

NOTE
ARC에서 약한 참조에 nil을 할당하면 프로퍼티 옵저버는 실행되지 않습니다.

Property Observers(프로퍼티 옵저버)를 정의해서 프로퍼티 값의 변경을
모니터링 할 수 있습니다
var value: Int = 0 {
    willSet(newValue) { print("willSet") }
    didSet(oldValue) { print("didSet")  }
}

예제를 살펴 보겠습니다. 아래 예제에서 이전 예제와 다른 점은 Apartment 클래스의 tenant변수가 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)의 강한 참조와 두 인스턴스 간의 링크는 이전 예제와 같이 생성됩니다.

var john: Person?
var unit4A: Apartment?

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

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

다음은 두 인스턴스를 함께 연결 한 참조의 모습입니다.

Person 인스턴스에는 여전히 Apartment 인스턴스에 대한 강한 참조가 되어 있지만 Apartment 인스턴스에서는 Person 인스턴스에 대해 약한 참조로 되어있습니다.
즉, john 변수가 보유한 Person 인스턴스에 대한 강한 참조를 nil로 설정하여 참조를 끊으면 Person 인스턴스에 대한 강한 참조가 더 이상 없어 ARC는 Person 인스턴스를 메모리에서 해제하게 됩니다.

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

Person 인스턴스에 대한 강한 참조가 더 이상 없기 때문에 메모리 해제가 되고 tenant 프로퍼티가 런타임에서 nil로 설정됩니다.

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

이제 unit4A 도 nil로 설정했으니 apartment 인스턴스도 더이상 강한참조가 없기 때문에 메모리에서 해제가 됩니다.

NOTE
가비지 콜렉션을 사용하는 시스템에서 weak pointer를 단순한 시스템 캐싱 목적으로 사용하기도 합니다. 왜냐하면 메모리가 소모가 많아지면 가비지 콜렉터를 실행해서 강한 참조가 없는 객체를 메모리에서 해제하는 식으로 동작하기 때문입니다. 하지만 ARC는 이 경우와 다르게 참조 횟수가 0이 되는 즉시 해당 인스턴스를 제거하기 때문에 약한 참조를 이런 목적으로 사용할 수 없습니다.

미소유 참조*소유하지 않는 참조(Unowned References)

미소유 참조는 약한참조와 같이 강한참조를 하지 않습니다. 그러나 약한 참조와 달리 미소유 참조는 다른 인스턴스의 수명과 같거나 수명이 더 길 때 사용합니다. 프로퍼티 또는 변수 선언 앞에 unowned 키워드를 붙여 미소유 참조를 나타냅니다.

*중요
미소유 참조는 자신보다 먼저 메모리에서 해제되지 않는 인스턴스를 참조한다고 “확신”하는 경우에만 사용하여야 합니다.
인스턴스가 메모리 해제 된 후 미소유 참조 인스턴스의 값에 접근하려고 하면 런타임 오류가 발생할것입니다.

다음 예제는 은행 고객과 해당 고객에 대한 신용 카드를 모델링하는 두 개의 클래스 Customer 및 CreditCard를 정의합니다. 이 두 클래스는 각각 다른 클래스의 인스턴스를 속성으로 저장합니다. 이 관계는 강한순환참조를 만들 가능성이 있습니다.

고객과 신용 카드의 관계는 위의 약한 참조 예제에서 본 아파트와 사람의 관계와 약간 다릅니다. 이 데이터 모델에서 고객은 신용 카드가있을 수도 있고 없을 수도 있지만 신용 카드는 항상 고객과 연결됩니다. CreditCard 인스턴스는 참조하는 고객보다 오래 지속되지 않습니다. 이를 나타 내기 위해 Customer 클래스에는 옵셔널 card 프로퍼티가 있지만 CreditCard 클래스에는 customer 프로퍼티가 미소유 참조로 되어 있습니다.

또한 새로운 CreditCard 인스턴스는 number와 Customer 인스턴스로 초기화하는 사용자 정의 이니셜 라이저에 전달해야만 생성 할 수 있습니다. 이렇게하면 CreditCard 인스턴스가 생성 될 때 CreditCard 인스턴스에 항상 연관된 Customer 인스턴스가 있을것입니다.

신용 카드에는 항상 고객이 있으므로 강한순환참조를 피하기 위해 customer 프로퍼티를 미소유 참조로 정의합니다.

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") }
}

NOTE
 CreditCard에서 카드 번호인 number는 충분히 긴 숫자를 저장할 수 있도록 하기 위해 Int가 아닌 UInt로 사용합니다. UInt64형은 32비트, 64비트 시스템 모두에서 16자리 숫자를 저장할 수 있습니다.

다음 코드는 특정 고객에 대한 참조를 저장하는데 사용되는 john이라는 옵셔널 Customer 변수를 정의합니다. 이 변수는 옵셔널이기 때문에 초기 값이 nil입니다.

var john: Customer?

이제 고객 인스턴스를 만들고 이를 사용하여 새 CreditCard 인스턴스를 해당 고객의 card 프로퍼티로 초기화하고 할당 할 수 있습니다.

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

두 인스턴스를 연결 했으므로 참조는 다음과 같습니다.

이제 Customer 인스턴스에는 CreditCard 인스턴스에 대한 강한 참조가 있고 CreditCard 인스턴스에는 Customer 인스턴스에 대한 미소유 참조가 있습니다.

미소유 customer 참조로 인해 john 변수가 보유한 강한 참조를 깨면 Customer 인스턴스에 대한 강한 참조도 없어집니다.

왜냐하면 Customer 인스턴스에 대한 강한 참조가 더 이상 없기 때문에 메모리에서 해제 됩니다. 이런 일이 발생하면 CreditCard 인스턴스도 더 이상 강한 참조가 없어져 메모리에서 해제되게 되는것입니다.

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

위의 마지막 코드는 john 변수가 nil로 설정된 후 Customer 인스턴스 및 CreditCard 인스턴스에 대한 메모리 초기화가 되었다는 문구를 프린트하는것을 확인 할 수 있습니다.

NOTE
위의 예제는 안전한 미소유 참조를 사용하는 방법을 보여줍니다. 또한 Swift는 성능상의 이유로 런타임에 안전 검사를 비활성화해야하는 경우에 안전하지 않은 미소유 참조를 제공합니다. 모든 안전하지 않은 작업과 마찬가지로 해당 코드의 안전을 확인하는 책임은 개발자에게 있습니다.

unowned (unsafe)를 작성하여 안전하지 않은 unowned 참조를 나타냅니다. 참조하는 인스턴스가 할당 해제 된 후 안전하지 않은 미소유 참조에 액세스하려고하면 프로그램이 인스턴스가 있었던 메모리 위치에 액세스하려고 시도합니다. 이는 안전하지 않은 작업입니다.

옵셔널 미소유 참조 Unowned Optional References

미소유로된 클래스 인스턴스에 대해 옵셔널 참조로 표시 할 수 있습니다. ARC는 소유권 모델 측면에서 옵셔널 미소유 참조와 약한 참조는 모두 동일한 컨텍스트(contexts)*컨텐츠를 담은 그 무언가에서 사용될 수 있습니다. 차이점은 옵셔널 미소유 참조를 사용할 때 항상 유효한 객체를 참조하거나 nil로 설정되어 있는지 확인해야한다는 것입니다.

약한 참조, 미소유 참조의 구분을 해당 참조가 nil이 될 수 있느냐 없느냐로 구분할 수 있습니다. 하지만 이 두경우를 제외한 제 3의 경우도 발생할 수 있습니다. 두 프로퍼티가 항상 값을 갖지만 한번 초기화 되면 절대 nil이 되지 않는 경우 입니다. 이 경우에는 미소유 프로퍼티를 암시적 옵셔널 프로퍼티 언래핑을 사용해 참조 문제를 해결할 수 있습니다.

즉 이번 내용은 값이 할당될 경우는 무조건 값이 있지만 값이 할당되지 않을 수 있는 경우의 내용입니다

다음은 특정 학교의 학과에서 제공하는 강의를 추적하는 예입니다.

class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
}

class Course {
    var name: String
    weak var department: Department
    unowned var nextCourse: Course?
    init(name: String, in department: Department) {
        self.name = name
        self.department = department
        self.nextCourse = nil
    }
}

Department는 학과에서 제공하는 강의에 대한 강한 참조를 유지합니다. ARC 소유권 모델에서는 학과는 과정(course)들을 소유합니다. Course에는 두 개의 약한 참조가 있습니다. 하나는 department에 대한 것이고 다른 하나는 학생이 수강해야하는 nextCourse에 대한 것입니다. Course는 이러한 객체를 소유하지 않습니다. 모든 과정은 학과의 일부이므로 department 프로퍼티는 옵셔널이 아닙니다. 그러나 일부 과정(course)에는 권장 후속 과정(course)이 없기 때문에 nextCourse 속성은 옵셔널입니다.

아래는 이 클래스들로 사용된 예제입니다.

let department = Department(name: "Horticulture")

let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)

intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]

위의 코드는 학과와 3 개의 과정를 생성합니다. 인트로과정 및 중급 과정은 모두 nextCourse 프로퍼티에 저장된 다음 과정을 제안하며,이 강의를 완료 한 후 학생이 수강해야하는 과정에 대한 참조는 옵셔널 미소유 참조를 유지합니다.

옵셔널 미소유 참조는 래핑하는 클래스의 인스턴스를 강력하게 유지하지 않으므로 ARC가 인스턴스 할당을 해제하는 것을 방해하지 않습니다. 옵셔널 미소유 참조가 nil 일 수 있다는 점을 제외하면 미소유 참조가 ARC에서 수행하는 것과 동일하게 작동합니다.

옵셔널이 아닌 미소유 참조와 마찬가지로 nextCourse가 항상 메모리 해제되지 않은 과정(course)을 참조하도록 해야합니다. 이 경우 예를 들어, department.courses에서 코스를 삭제할 때 다른 코스에 있을 수 있는 모든 참조도 제거해야합니다.

NOTE
옵셔널 값의 기본 유형은 Optional이며 Swift 표준 라이브러리의 열거형입니다.
옵셔널값 유형을 미소유로 표시 할 수 없다는 규직에서 Optional은 예외됩니다.
클래스를 래핑하는 옵셔널은 레퍼런스카운팅*참조 계산을 사용하지 않으므로 옵셔널에 대한 강한 참조를 유지할 필요가 없습니다.

미소유 참조와 암시적 옵셔널 프로퍼티 언래핑 (Unowned References and Implicitly Unwrapped Optional Properties)

위의 약한 참조 및 미소유 참조의 예는 강한 순환참조를 중단해야하는 일반적인 두 가지 시나리오를 다룹니다.

Person 및 Apartment 예제는 둘 다 nil이 될 수있는 두 속성이 강한 순환참조를 유발할 가능성이있는 상황을 보여줍니다.
이 시나리오는 약한 참조로 가장 잘 해결할 수 있습니다.

Customer 및 CreditCard 예는 nil이 허용되는 속성과 nil이 될 수 없는 다른 속성이 강한 순환참조를 유발할 수있는 상황을 보여줍니다.
이 시나리오는 미소유 참조로 가장 잘 해결할 수 있습니다.

그러나 세 번째 시나리오가 있습니다. 두 속성 모두 항상 값이 있어야하고 초기화가 완료되면 두 속성 모두 nil이되어서는 안됩니다.
이 시나리오에서는 한 클래스에서의 unowned 프로퍼티를 다른 클래스에서
암시 적으로 언 래핑 된 옵셔널 프로퍼티와 결합하는 것이 유용합니다.

이렇게하면 순환참조를 피하면서 초기화가 완료되면 두 프로퍼티 모두에 직접 액세스 할 수 있습니다 (옵셔널 언랩핑없이). 이 섹션에서는 이러한 관계를 설정하는 방법을 보여줍니다.

아래 예제는 Country와 City라는 두 개의 클래스를 정의하며, 각 클래스는 다른 클래스의 인스턴스를 프로퍼티로 저장합니다. 이 데이터 모델에서 모든 국가에는 항상 수도가 있어야하며 모든 도시는 항상 국가에 속해야합니다. 이를 나타 내기 위해 Country 클래스에는 capitalCity 프로퍼티가 있고 City 클래스에는 country 프로퍼티가 있습니다.

class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

두 클래스 간의 상호 종속성을 설정하기 위해 City의 이니셜 라이저는 Country 인스턴스를 가져와서 이 인스턴스를 해당 country 프로퍼티에 저장합니다.

City에 대한 이니셜 라이저는 Country에 대한 이니셜 라이저 내에서 호출됩니다. 그러나 Country의 이니셜 라이저는 2 단계 초기화(Two-Phase Initialization)에 설명 된대로 새 Country 인스턴스가 완전히 초기화 될 때까지 City 이니셜 라이저에 self를 전달할 수 없습니다.

이 요구 사항을 처리하려면 Country의 capitalCity 프로퍼티 끝에 연산자(!)를 이용해 명시적으로 강제 언래핑을 해야 합니다.(City!)
즉, capitalCity 프로퍼티는 다른 옵셔널과 마찬가지로 기본값이 nil이지만 암시 적으로 래핑되지 않은 옵셔널(Implicitly Unwrapped Optionals)에 설명 된대로 값을 래핑 해제 할 필요없이 액세스 할 수 있습니다.

capitalCity는 기본 nil 값을 갖기 때문에 Country 인스턴스가 초기화 프로그램 내에서 name 프로퍼티를 설정하는 즉시 새 Country 인스턴스가 완전히 초기화 된 것으로 간주됩니다. 즉, Country 이니셜 라이저는 name 프로퍼티가 설정되는 즉시 암시 적 자체 프로퍼티를 참조하고 전달할 수 있습니다. 따라서 Country 이니셜 라이저는 Country 이니셜 라이저가 자체 capitalCity 프로퍼티를 설정할 때 City 이니셜 라이저의 매개 변수 중 하나로 self를 전달할 수 있습니다.

이 모든 것은 강한 순환참조를 만들지 않고 단일 문단에서 Country 및 City 인스턴스를 만들 수 있으며, 선택 값을 풀기 위해 느낌표를 사용하지 않고도 capitalCity 프로퍼티에 직접 액세스 할 수 있음을 의미합니다.

var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"

위의 예제에서 암시 적으로 언 래핑 된 옵셔널을 사용하면 모든 [2 단계 클래스 이니셜 라이저] 요구 사항이 충족됩니다. capitalCity 프로퍼티는 초기화가 완료되면 옵셔널 값이 아닌 값처럼 사용하고 액세스 할 수 있지만 여전히 강한 순환참조를 피할 수 있습니다.

이번 장에서는 클래스 인스턴스간의 강한 순환참조에 대해 알아 보았습니다.
강한순환참조가 발생하면 해당 인스턴스들은 서로를 강한게 참조해서 레퍼런스카운트가 0으로 되지 않기 때문에 ARC는 해당 인스턴스들을 메모리에서 해제하지 않게 되는 현상이었죠. Swift에서는 이런 상황을 피하게 하기 위해
약한참조(weak reference) 와 미소유 참조(unowned reference)를 제공하여 순환참조가 되지 않도록 한다는것도 알아볼 수 있었습니다.

다음장에서는 클로져(Closures) 의 강한순환참조에 대해 알아보겠습니다.

에 발행했습니다
iOS(으)로 분류되었습니다

aaron님이 작성

아무것도 안해도 시간은 흐른다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다