티스토리 뷰

이전 글에서는 ARC의 동작 방법과 strong, weak unowned 참조에 대해 알아봤습니다. 공식 문서를 보면 미소유 참조를 사용하는 객체를 Optional 타입으로 선언하는 경우에 대해 추가적으로 설명하고 있는데요. 이번 글에서는 이 부분을 다뤄보려고 합니다. 

 


 

Unowned Reference

먼저 미소유 참조를 복습해볼게요. 미소유 참조는 약한 참조와 함께 인스턴스를 참조하지만 참조 카운트는 증가시키지 않아서 인스턴스를 참조하는 객체가 남아있더라도 ARC가 인스턴스를 메모리에서 해제시킬 수 있었습니다. 그리고 이런 동작 때문에 강한 참조 순환 문제를 해결할 수 있었습니다.

 

그리고, 미소유 참조는 서로 다른 두 인스턴스 사이에 강한 참조 순환이 발생할 수 있는 상황에서 두 인스턴스에 대한 참조의 생명 주기가 같은 경우에 사용할 수 있다고 했었습니다. 그래서 Customer와 CreditCard를 예로 들면서, 두 인스턴스 간의 순환 참조 문제를 해결하기 위해 CreditCard가 가진 customer 프로퍼티가 Customer 인스턴스를 미소유 참조하도록 만들었습니다. CreditCard의 입장에서 customer는 반드시 존재해야 하기 때문에 CreditCard 인스턴스가 소멸되는 순간에는 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") }
}

 

그래서 Customer 객체를 메모리에서 해제시키면 Customer 인스턴스가 메모리에서 해제되면서 자연스럽게 CreditCard의 인스턴스까지 모두 해제되어 순환 참조가 발생하지 않았습니다.

 

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

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

 

Unowned Optional References

이번에는 미소유 참조가 옵셔널 타입 객체에 선언되었을 때를 살펴보겠습니다. 약한 참조와 미소유 참조 객체는 모두 옵셔널 타입으로 선언될 수 있습니다. 약한 참조 객체는 참조하던 인스턴스가 메모리에서 해제되면 컴파일러가 자동으로 nil을 할당하지만, 미소유 참조는 항상 값이 있을 것이라고 가정하기 떄문에 자동으로 nil을 할당하지 않습니다. 그래서, 미소유 참조 객체를 옵셔널 타입으로 선언할 때는 이 객체가  항상 유효하고(값을 가지고 있고), 그렇지 않을 경우 nil이 할당됨을 보장해야 합니다. 

 

다음 Department와 Course 클래스의 예시에서 Cousre는 두 개의 미소유 참조 객체를 프로퍼티로 갖고 있습니다. Course는 반드시 Department의 한 부분이 되어야 하므로 department 프로퍼티는 옵셔널 타입이 아니지만, nextCourse는 존재하지 않을 수 있으므로 옵셔날 타입으로 선언되었습니다. 그리고 옵셔널 타입의 미소유 참조 객체는 nil이 할당되지 않기 때문에 생성자에서 직접 nil을 할당하고 있습니다.

 

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

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

 

다음 예시에서는 이 두 클래스를 어떻게 사용할 수 있는지 보여줍니다. 하나의 Department와 3개의 Course 인스턴스를 생성하고 있습니다. 

 

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개의 강한 참조 형성

 

그리고, 생성한 Department와 Course 인스턴스들은 다음과 같은 관계를 갖게 됩니다.

여기서 department의 courses에서 하나를 삭제하면, 해당 Course 인스턴스에 대한 강한 참조가 해제되고 . 그러면 그 Course 인스턴스를 참조하고 있는 다른 옵셔널 미소유 참조 객체(nextCourse)에도 nil을 직접 할당하여 참조를 해제시켜야 합니다. 미소유 참조는 값이 있을 것이라고 가정하기 때문에 Course 인스턴스가 해제된 후에도 nextCourse는 참조하는 인스턴스가 있는 것 처럼 접근할 것이기 때문입니다.

 

Growing Common Herbs Course 를 예시로 생각해 보겠습니다. department.courses에서 intermediate를 삭제하면 해당 인스턴스의 강한 참조가 해제될 것이고, RC가 0이 되면서 Growing Common Herbs에 해당하는 인스턴스가 메모리에서 해제될 것입니다. 이 때, Survey of Plants 인스턴스(intro)의 nextCourse는 Growing Common Herbs 인스턴스를 참조하고 있었지만 미소유 참조이므로 nil이 할당되지 않고 이미 없어진 주소를 가리키게 됩니다. 그러면 intro?.nextCourse에 접근하려고 할 때 런타임 에러(dangling pointer)가 발생할 것이므로, nil을 할당하여  오류를 막야아 합니다.

 

if let index = department.courses.firstIndex(of: intermediate) {
    department.courses.remove(at: index)  // intermediate가 참조하던 인스턴스가 메모리에서 해제됨
    intro.nextCourse = nil  // intermediate의 인스턴스를 더 이상 참조하지 않도록 nil 할당
}

 

이 과정은 nextCourse가 약한 참조로 선언되었다면 컴파일러에 의해 자동으로 이루어집니다. 미소유 참조 대신 약한 참조를 사용해도 되겠지만, 어떤 이유로 미소유 참조를 사용하기로 했다면 nil을 할당하는 작업을 직접 해야 한다는 것입니다.

 

정리

지금까지 알아본 weak, unowned 참조의 동작을 정리해보겠습니다.

 

인스턴스가 할당되는 객체는 해당 인스턴스를 강한 참조(strong reference) 합니다. 이 때, 발생할 수 있는 강한 참조의 순환 문제를 해결하기 위해 약한 참조(weak reference)와 미소유 참조(unowned reference)를 사용했습니다.

 

강한 참조 순환 관계에 놓인 두 인스턴스가 있을 때, 둘 중 하나의 프로퍼티가 nil이 할당될 수 있는 경우(Person과 Apartment)에는 weak를 사용해서 문제를 해결했고, nil이 할당될 수 없는 경우(Customer와 CreditCard)에는 unowned를 사용해서 문제를 해결했습니다.

 

그리고, 만약 미소유 참조 객체가 옵셔널 타입으로 선언되어 nil이 할당될 수 있게 하려면 인스턴스가 소멸하는 시점에 nil을 명시적으로 할당해야 합니다.

 


 

이번 글에서는 unowned를 조금 더 살펴봤는데요. 정리를 할수록 '불안하면 weak를 써라'라는 말이 떠오르네요.. ㅎㅎ. 미소유 참조의 개념은 꽤 어려운 것 같습니다.

 

그리고, weak와 unowned 중 어떤 것을 쓸지 결정하는 것은 OOP 관점에서 객체가 갖는 의미에 따라 달라지는 것 같습니다. Swift가 값이 없을 수도 있다는 것을 옵셔널이라는 개념을 통해 별도로 나누고 있는 것 처럼, nil이 할당될 수 있느냐 없느냐에 따라 weak와 unowned가 갈리고, 또 이것을 어떤 객체에다 선언해 줄 것이냐가 갈린다고 생각됩니다.

 

즉, 설계에 따라 다르게 사용해야 하겠지만 확실하지 않으면 weak를 쓰는 것이 더 안전하겠습니다.

댓글
댓글쓰기 폼