ARC #2

dvhuni·2021년 5월 1일
0

iOS

목록 보기
4/15

안녕하세요, dvHuni 입니다 !

ARC #1에 이어 ARC에 대해 알아보는 두번째 포스트 입니다!

ARC를 정복하기 위해 다시한번 힘차게 !!

고고 🤜

  • 본 글은 swift docs를 참조하여 개인의 이해를 적은 글입니다. 🤓

Strong Reference Cycle

ARC Action의 예시로 미루어보아 Strong Reference는 참조가 모두 사라진 뒤에 ARC에서 deallocate 시킵니다.

그렇다면 예를들어 서로 다른 class의 instance가 서로를 Strong Reference하고있다면 어떻게 될까요 ?

코드를 통해 해당 상황을 재현 해 보겠습니다.

class Room {
    let number: String
    var hotel: Hotel?

    init(number: String) {
        self.number = number
    }
    deinit {
        print("\(number) room is being deinitialized")
    }
}

class Hotel {
    let name: String
    var room: Room?

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

Room class와 Hotel class가 있습니다.

Room class는 Hotel class property를 가질 수 있고,

Hotel class는 Room class property를 가질 수 있습니다.

var myRoom: Room?
var myFirstVisitHotel: Hotel?

myRoom = Room(number: "1101")
myFirstVisitHotel = Hotel(name: "GrandHotel")

/*
 myRoom은 Room class instance를 Strong Reference 하고있으며,
 myFirstVisitHotel 역시 Hotel class instance를 Strong Reference 하고 있습니다. 
*/

여기 까지는 이전 코드와 크게 다르지 않습니다.

각각의 property에 class instance를 Strong Reference하게 지정했습니다.

자 그러면 이렇게 해보면 어떻게 될까요 ?

// Room class는 Hotel instance를 property로 가질 수 있습니다.
myRoom!.hotel = myFirstVisitHotel

// Hotel class는 Room instance를 property로 가질 수 있습니다.
myFirstVisitHotel!.room = myRoom

/*
    myRoom의 hotel property는 Hotel class instane를 Strong Reference 하고있는 myFirstVisitHotel을 Strong Reference 하고 있습니다.
    또한, myFirstVisitHotel의 room property는 Room class instance를 Stong Reference 하고 있는 myRoom을 String Reference 하고 있습니다.
*/

주석을 통해 간략히 설명 했지만, 그림을 보면서 한번 더 설명을 드리겠습니다.

myRoom의 hotel이 Hotel instance를 참조하면서 Strong Reference가 생겼고,

myFirstVisitHotel의 room이 Room instance를 참조하면서 Strong Reference가 생겼습니다.

자 그렇다면 이제 myRoom과 myFirstVisitHotel에 nill을 할당하여 Room instance의 Strong Reference를 제거한다면 어떻게 될까요 ??

myRoom = nil
myFirstVisitHotel = nil

여기서 기대되는 상황은 Room, Hotel instance의 Strong Reference가 사라졌으므로, 각 class의 deinitalizer의 호출 입니다.

하지만, 각 instance의 property의 Strong Reference가 남아있으므로 deinitalizer가 호출되지않습니다.

이렇게 Strong Reference가 남아있기 때문에 두 instance는 ARC에서 deallocate 시키지 않고, 메모리에 남아있게 되며 이는 Memory Leak(메모리 누수)을 유발 합니다!!

위와같은 상황을 Strong Reference Cycle(강한 참조 순환)이라고 하며, Strong Reference를 사용할 때 항상 염두해 두어야 하는 상황입니다.

Resolving Strong Reference Cycles Between Class Instances

그렇다면 이러한 class instance간의 참조 순환을 해결하기 위해서는 어떻게 해야 할까요??

Swift에서는 두가지의 방법을 제공합니다.

  1. Weak Reference
  2. Unknown Reference

Weak Reference

Weak Reference란 연관된 instance를 Strong hold 하지 않는 참조를 의미합니다. 그러므로 ARC는 참조된 instnace의 deinit을 멈추지 않습니다.
이는 Reference가 Strong Reference Cycle의 일부가 되는것을 막습니다.

Weak Reference는 instance를 Strong hold하지 않기 때문에 instance가 deallocated될 때, ARC는 자동으로 nil을 set 합니다. 그리고, nil이 되어야 하기 때문에 Weak Reference는 항상 constants(let)이 아닌 variables(var)이며,

optional tpye 입니다.

Weak Reference의 값이 존재하는지 확인할 때는 다른 optional값의 확인 방법과 동일합니다.

위에서 Strong Reference Cycle이 발생한 상황을 Weak Reference로 해결 해 보겠습니다.

class Room {
    let number: String
    var hotel: Hotel?

    init(number: String) {
        self.number = number
    }
    deinit {
        print("\(number) room is being deinitialized")
    }
}

class Hotel {
	let name: String
	weak var room: Room? // weak keyword를 사용하여 Weak Reference라는 것을 명시했습니다.
	
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) hotel is being deinitialized")
    }
}

var myRoom: Room?
var myFirstVisitHotel: Hotel?

myRoom = Room(number: "1101")
myFirstVisitHotel = Hotel(name: "GrandHotel")

myRoom!.hotel = myFirstVisitHotel
myFirstVisitHotel!.room = myRoom

위에서 두 부분으로 나누어져 있던 코드를 하나의 블록으로 합쳤습니다.

여기 까지의 진행상황을 그림으로 나타내보면 다음과 같습니다.

이제 myRoom의 참조를 해제해보면?

myRoom = nil
// "1101 room is being deinitialized"

Weak Reference인 myFirstVisitHotel의 room property는 상관 없이 deallocated 되는 것을 확인 할 수 있습니다.

myFirstVisitHotel이 Room instance를 Strong Hold 하지 않고 있기 때문입니다.

이제 myFirstVisitHotel의 참조도 해제 해 보겠습니다.

myFirstVisitHotel = nil
// "GrandHotel hotel is being deinitialized"

Hotel instance의 Reference가 더이상 존재 하지 않으므로, deallocated됩니다.

NOTE
In systems that use garbage collection, weak pointers are sometimes used to implement a simple caching mechanism because objects with no strong references are deallocated only when memory pressure triggers garbage collection. However, with ARC, values are deallocated as soon as their last strong reference is removed, making weak references unsuitable for such a purpose.

Garbage Collection을 사용하는 시스템에서는 weak pointers는 strong reference가 없는 memory pressure로 인해 Garbage Collection을 trigger시킬 때만 object들을 deallocate시키기 위해서 간단한 caching mechanism을 사용합니다.
하지만 ARC는 마지막 strong reference가 제거 되었을 때 deallocate 시키므로
weak reference는 이러한 목적에 적합하지 않습니다.

⇒ 즉, Garbage Collection과 다르게 ARC는 Strong Reference가 없어졌을 때, instance를 deallocate 시키고, weak는 strong hold하지 않기 때문에, reference가 없어져도 남아있어야 하는 경우엔 적합하지 않습니다!

Unowned References

Weak Reference와 마찬가지로 Unowned Reference 역시 instacne를 Strong hold 하지 않습니다. 또한 unowned" keyword를 사용하여 명시합니다.

그러나, Weak Reference와 다르게 instance가 동일한 lifetime 혹은 더 긴 lifetime을 가질 때 사용합니다.

또한 Unowned Reference는 언제나 값을 가지고 있습니다. 따라서 unowned value는 optional 하지 않게 하십시오.

ARC 또한 nil을 set하지 않을 것 입니다.

IMPORTANT
Use an unowned reference only when you are sure that the reference always refers to an instance that hasn’t been deallocated.
If you try to access the value of an unowned reference after that instance has been deallocated, you’ll get a runtime error.

unowned reference는 instance가 deallocated 되면 안된다는 것을 확신하고 사용하십시오.
instance가 deallocated 된 후 reference에 접근하면 runtime error가 발생할 것입니다.

⇒ 즉, unowned를 사용할 때에는 deallocated 되는 상황을 만들어서는 안됩니다!

Unowned Reference를 통해 Strong Reference Cycle을 해결하는 과정을 함께 보도록 하겠습니다 🤓

class Bookcase {
    let number: String
    var book: Book?
    
    init(number: String) {
        self.number = number
    }
    deinit {
        print("\(number) bookcase is being deinitialized")
    }
}

class Book {
    let name: String
    unowned let bookcase: Bookcase
    
    init(name: String, bookcase: Bookcase) {
        self.name = name
        self.bookcase = bookcase
    }
    deinit {
        print("\(name) book is being deinitialized")
    }
}

var blueBookcase: Bookcase?
blueBookcase = Bookcase(number: "001")
blueBookcase?.book = Book(name: "Swift 5", bookcase: blueBookcase!)

현재 까지의 코드를 그림으로 나타내보면 다음과 같습니다.

이번 코드는 잘 이해가 안되실 수 있어 설명을 추가하겠습니다!

blueBookcase?.book = Book(name: "Swift 5", bookcase: blueBookcase!)

위 코드에서 Bookcase instance인 blueBookcase에 book instance를 할당 하려 합니다.

이 때, 새로운 Book instance를 생성하는데, 해당 instance는 initailizer로 String과 Bookcase를 가집니다.

따라서 String에는 "Swift 5"를, bookcase에는 Bookcase type인 blueBookcase를 parameter로 전달 했습니다.

blueBookcase는 Optional Type으로 선언 되어있기 때문에 Optional Unwrapping을 위해 (!)을 붙였습니다.

또한 Book class의 Bookcase property는 unowned로 선언 되어 있기 때문에, 그림과 같이 Unowned Reference입니다.

이제 blueBookcase에 nil을 할당하여 Bookcase instance의 Strong Reference가 사라지게 되면 (1)

Bookcase를 더이상 Strong Reference하고 있지 않기 때문에 deallocated 되며, (2)
Book instace 또한 Strong Reference가 더이상 남아 있지 않기 때문에 마찬가지로 deallocated 됩니다. (3)

(1)

(2)

(3)

모든 instance가 deallocated 되었으니, deinitalizer도 함께 호출됩니다.

blueBookcase = nil
// "001 bookcase is being deinitialized"
// "Swift 5 book is being deinitialized"

이처럼 blueBookcasae에 nil을 할당하는 것으로 Strong Reference가 사라졌기 때문에 Strong Reference Cycle이 발생 하지 않습니다!

NOTE
The examples above show how to use safe unowned references. Swift also provides unsafe unowned references for cases where you need to disable runtime safety checks—for example, for performance reasons. As with all unsafe operations, you take on the responsibility for checking that code for safety.
You indicate an unsafe unowned reference by writing unowned(unsafe). If you try to access an unsafe unowned reference after the instance that it refers to is deallocated, your program will try to access the memory location where the instance used to be, which is an unsafe operation.

위의 예시는 safe unowned reference를 어떻게 사용하는지 보여줍니다.
Swift는 unsafe unowned reference를 제공하며, 이는 runtime시에 safety check를 할 필요 없게 해줍니다. - 예를들어 preformance의 측면에서 모든 unsafe operations는 code가 safe한지 check해야 할 책임을 가집니다.
당신은 (unsafe)키워드를 통해 unsafe unkowned reference를 나타낼 수 있습니다.
만약 당신이 deallocated된 unsafe unkowned reference instance에 접근할 때 프로그램은 instance가 존재 했던 memory location, 즉 unsafe operation에 접근 하려 시도 할 것입니다.

Unowned Optional References

Unowned Reference에 optional을 표시 할 수 있고, ARC 입장에서 unonwed optional referenece와 weak reference는 같은 문맥으로 이해됩니다.

차이점은 Unowned Reference는 nil을 set하는데에 있어 책임이 따른 다는 점입니다. (Unowned References의 IMPORTANT 참조)

코드와 그림을 통해 Unowned Optional Rerferences를 어떻게 활용하는지 알아봅시다.

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

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

let department = Department(name: "Swift")

let intro = Lecture(name: "About Swift", in: department)
let basic = Lecture(name: "The Basics", in: department)
let operators = Lecture(name: "Basic Operators", in: department)

intro.nextLecture = basic
basic.nextLecture = operators
department.lectures = [intro, basic, operators]

Department instance는 Lecture instance를 Array Type으로 Strong Reference 하고있습니다.

각 Lecture instacne는 Department instance를 Unowned Reference 하고있으며,

nextLecture Property를 통해 Lecture instance를 Unowned Optional Referecne 하고있습니다.

Unowned Optional Reference 역시 Strong hold 하지 않기 때문에, ARC에서 instance를 deallocate 하지 않으며, Unowned Reference와 다르게 nil이 될 수 있습니다.

만약 nextLecture Property를 Unowned Optional Reference가 아닌 Unowned Reference로 선언한다면, nextLecture는 nil을 할당 할 수 없으므로 initalizer에서 계속 할당해 주어야 합니다.

또한 nextLecture는 nil을 가질 수 없기 때문에 항상 deallocated 되지 않아야 합니다.

예를들어 "The Basics" Lecture가 Lectures에서 제거 된다면,

"About Swift" Lecture의 nextLecture는 nil이 될 수 없으므로 함께 제거 되어야 합니다.

요약하면 Optional이 아니라면, Lecture instance들은 항상 nextLecture를 가져야 하지만,
Optional을 통해 nextLecture의 유무를 상관 없게 해줍니다.

NOTE
The underlying type of an optional value is Optional, which is an enumeration in the Swift standard library. However, optionals are an exception to the rule that value types can’t be marked with unowned.
The optional that wraps the class doesn’t use reference counting, so you don’t need to maintain a strong reference to the optional.

optonal type의 근본적인 value는 Swift standard libray의 enumration인 Optional입니다.
그러나 optionals는 value type을 소유하지 않은 것으로 표시할 수 없다" 라는 규칙의 예외입니다.
optional은 reference counting을 사용하지않는 class로 감싸져 있기 때문입니다, 그래서 당신은 optional에 대한 strong reference를 유지 할 필요가 없습니다.

Unowned References and Implicitly Unwrapped Optional Properties

위에서 다룬 Weak References 와 Unkowned References는 일반적인 Strong Reference Cycle을 해결할 수 있습니다.

Room과 Hotel의 예시는 두 property가 nil을 허용하며, Strong Reference Cycle을 발생시킬 수 있는 상황에서 Weak Reference를 통해 해결할 수 있는 케이스 입니다.

BookCase와 Book의 예시는 한쪽이 nil을 허용하지 않는 경우에 Strong Reference Cycle을 발생 시킬 수 있는 상황이며, Unkowned References를 통해 해결 가능한 케이스 입니다.

세번째 케이스가 존재 하는데, 이는 두 property가 항상 value를 가지고있어야 하며 initalize가 완료 되었을때 한번만 nil을 허용하는 경우 입니다.

이러한 케이스에서는 Unkowned property와 Implicitly Unwrapped Optional를 함께 이용하는것이 좋습니다.

property가 서로 optional unwrapping없이 직접적으로 접근이 가능하며, intialize 이후에는 reference cycle을 항상 벗어나 있기 때문에 Strong Reference Cycle을 발생 시키지 않습니다.

예시를 함께 보시죠.

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

두개의 클래스를 정의합니다, Country와 City.

각각의 class는 서로의 instance를 property로 가지고있습니다.

데이터 모델로 미루어보아 모든 country는 capitalCity를 반드시 가지고 있어야 하며 모든 city는 country 반드시 country에 속해야 합니다.

두 클래스의 interdependency를 설정하기 위해서 City의 inintalizer는 Country instance를 취하며, 자신의 country property에 저장합니다.

또한 City의 initalizer는 Country의 내부 initailzer를 호출합니다.

하지만 country에 self는 Country가 완전히 initalized 되기 전까지는 전달 할 수 없습니다.

이에대한 설명은  Two-Phase Initialization를 참조해주시기 바랍니다.

해당 요구사항을 처리하기 위해서는 Country의 capitalCity property를 Implicitly Unwrapped Optional property로 정의해야합니다. (City!)

이것은 capitalCity property는 다른 Optional과 마찬가지로 기본값은 nill인것을 의미하지만, Optional Unwrapping 과정 없이 value에 접근 할 수 있습니다.

자세한 설명은 Implicitly Unwrapped Optionals를 참조해주시기 바랍니다.

capitalCity의 기본 value가 nil이기 때문에 Country instance는 완전하게 initalized되었다고 판단할 수 있으며, 동시에 Country instance는 name property를 내부 initalizer에 전달 된 값으로 초기화 할 것 입니다.

이는 Country initalizer가 참조를 시작하고 name property가 설정될 때 self를 전달 할 수 있음을 의미합니다.

그렇기 때문에 Country initalizer는 capitailCity property를 할당 할 때 City initialzer에 self를 전달 할 수 있습니다.

위의 설명들이 의미하는것은

  1. Strong Reference Cycle이 존재하지 않으며,
  2. Optional Unwarpping 과정 없이 capitalCity property에 직접 접근할수 있는

Country와 City instance를 single line에서 생성 할 수 있다는 것입니다.

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"

위의 활용 코드를 참고하면, capitalCity property는 City instance이지만, 내부 initalizer를 통해 초기화 되었고, Implicitly Unwrapped Optional(City!)이기 때문에 Optional unwrapping 과정 없이 property에 접근하여 name이라는 property까지 접근 할 수 있습니다.

가장 중요한 점은 Strong Reference Cycle이 존재하지 않는다는 점 입니다.

마무리하며...

이로써 ARC의 두번째 포스트가 마무리 되었습니다!!

금방 올리려고 했는데 핑계아니고 정말 바빴습니다.... 😱

아무튼 이번 포스트는 Strong Reference Cycle이 무엇이며, Instance간의 Strong Reference Cycle에 대해 알아보았고, 이를 해결하는 방법 까지 알아보있습니다.

이번 포스트는 많이 사용되지만 의미는 잘 모를 수도 있었던 weak 키워드에 대한 공부도 조금 된것 같아 뿌듯했고, 정말 한번도 사용해 보지 않았던 unowned 키워드에 대한 공부도 되어 좋았습니다!

하. 지. 만.

아직 한 단락 더 남았습니다!

바로 Closure에서의 Strong Reference Cycle 입니다.

포기하지 않고 끝까지 공부해서 ARC 마무리 짓도록 하겠습니다 !!

지켜봐주세요 🤠

틀린부분이 있다면 언제든지 지적 해주시면 정말 감사합니다!

읽어주셔서 감사합니다!! 🙇‍♂️

이전 글📙
ARC #1

다음 글📗
ARC #3

profile
hello, iOS

0개의 댓글