본문 바로가기

Swift/공식문서

[Swift 공식문서 번역] ARC (Automatic Reference Counting) - 2.

 

 

이번에는 ARC 공식문서에서

 

Strong Reference Cycles Between Class Instances

Resolving Strong Reference Cycles Between Class Instances

Weak References

Unowned References

Unowned Optional References

Unowned References and Implicitly Unwrapped Optional Properties

를 다뤄보려고 합니다!

 

대충 읽어봐도 순환참조와 그 해결법(weak, unowned 참조)에 대한 것임을 알 수 있죠? ㅎㅎ

 

 

시작해 보겠습니다..!!

약간의 의역이 포함될 예정이니 오역이나 업데이트가 필요한 부분은 꼭 댓글로 알려 주세요..! :)

 

 

 

 

클래스 인스턴스 간의 강한 참조 사이클 (순환참조)

앞선 예시에서 볼 수 있듯이, ARC는 새로운 Person 인스턴스를 참조하는 횟수를 추적할 수 있고, Person 인스턴스가 더이상 필요하지 않을 때 메모리에서 해제할 수 있습니다.

 

하지만, 클래스 인스턴스의 강한 참조 횟수가 절대  0이 되지 않는 상황으로 코드를 짜게 될 수도 있어요. 이런 경우는 두 개의 클래스 인스턴스가 서로를 살려 두며(메모리에 유지되도록) 강한 참조할 때 일어날 수 있습니다. 이것은 강한 참조 사이클(순환참조)로 알려져 있습니다.

 

순환참조는 클래스 사이의 관계를 강한 참조 대신 weak(약한 참조)이나 unowned(미소유 참조)로 정의함으로써 해결될 수 있습니다. 이러한 과정은 클래스 인스턴스 간의 순환참조 해결에 서술되어 있습니다. 하지만, 순환참조를 어떻게 해결하는지 배우기 전에, 어떻게 그런 순환이 일어나게 되는지를 먼저 이해하는 것이 유용할 거예요.

 

어떤게 순환참조가 실수로 만들어질 수 있는지의 예시가 있어요. 이 예시는 아파트 블럭과 거주자들의 모델이 되는(형태/표본/견본이 되는) Person과 Apartment라는 두 개의 클래스를 정의합니다. 

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 인스턴스는 String 타입의 name 프로퍼티와, 초기값으로 nil을 갖는 옵셔널 apartment 프로퍼티를 갖습니다. 사람이 아파트를 갖고 있지 않는 경우도 있기 때문에 apartment 프로퍼티는 옵셔널입니다.

 

비슷하게, 각 Apartment 인스턴스는 String 타입의 unit 프로퍼티와, 초기값으로 nil을 갖는 옵셔널 tenant 프로퍼티를 갖습니다. 아파트에 세입자가 없을 수도 있기 때문에 tenant 프로퍼티는 옵셔널입니다.

 

두 클래스 전부 소멸자(디이니셜라이저)도 갖고 있는데, 각 클래스의 인스턴스가 deinitialize되고 있다고 출력해 줍니다. 이것을 통해서 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)이 아파트를 갖고, 아파트가 세입자를 가질 수 있게 두 인스턴스를 연결할 수 있어요. john과 unit4A 옵셔널 변수 안에 저장되어 있는 인스턴스들에 접근하기 위해, 느낌표 (!)로 옵셔널 언래핑이 된 점을 주의하세요:

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

 

 

두 인스턴스를 연결한 후 강한 참조의 모습은 아래와 같습니다:

 

 

 

안타깝게도 두 인스턴스를 연결하는 건 인스턴스 아이에 순환참조를 발생시킵니다. 이제 Person 인스턴스는 Apartment 인스턴스를 강한 참조하고, Apartment 인스턴스는 Person 인스턴스를 강한 참조합니다. 따라서 john과 unit4A 변수가 참조하는 강한 참조를 끊더라도, 참조 횟수가 0이 되지 않고, ARC에 의해 인스턴스가 메모리에서 해제되지도 않습니다.

john = nil
unit4A = nil

 

 

두 변수를 nil로 지정하더라도, 어떤 소멸자(디이니셜라이저)도 호출되지 않는다는 점을 주의하세요. 순환참조는 Person과 Apartment 인스턴스가 메모리에서 해제되는 것을 막고, 당신의 앱에서 메모리 누수를 일으킵니다.

 

john과 unit4A 변수를 nil로 지정 후 강한 참조의 모습은 다음과 같습니다:

 

 

Person 인스턴스와 Apartment 인스턴스 사이의 강한 참조들이 유지되고 끊길 수 없게 됩니다.

 

 

 

 

 

클래스 인스턴스 간의 순환참조 해결

Swift는 클래스 타입의 프로퍼티를 다룰 때 순환참조를 해결하는 방법 2 가지를 제공합니다: weak(약한 참조)와 unowned(미소유 참조).

 

약한 참조와 미소유 참조는 참조 사이클에서 한 인스턴스가 다른 인스턴스를 강하게 붙들지 않으면서 참조할 수 있도록 해 줍니다. 그럼 인스턴스들은 강한 참조 사이클(순환참조)을 만들지 않으면서 서로를 참조할 수 있죠.

 

다른 인스턴스의 수명이 더 짧을 때 약한 참조를 사용하세요 — 즉 다른 인스턴스가 먼저 메모리에서 해제될 수 있을 때요. 위의 Apartment 예시에서, 아파트의 일생동안(아파트 인스턴스가 살아있는 동안을 의미하는 것임! 이하 그냥 일생이라고 하겠슴..) 세입자가 없는 때도 있는 것이 적절하기 때문에, 이 경우에는 순환 참조를 끊기 위해 약한 참조가 적절한 방법입니다. 반대로, 다른 인스턴스의 수명이 같거나 더 길 때 미소유 참조를 사용하세요. 

 

잠깐!!! 여기서 자꾸 '다른 인스턴스의 수명이 어쩌구' 나오는데, A → B 이렇게 참조가 일어날 경우 A와 B의 수명을 비교하라는 뜻입니다. 앞선 예시에서

unit4A!.tenant = john

머 이런 부분이 있었는데 이걸로 예를 들자면,
진짜 세입자 John이라는 사람 자체의 수명을 말하는 게 아니라...
"아파트의 입장"에서 아파트는 아직 있는데 '세입자'는 없는 상황!이 가능!!하기 때문에 아파트보다 세입자의 수명이 짧다는 겁니당..
나는 있는데 얘는 없어.. 인거죵..!!! 
내가 얘를 참조하는데 얘(다른 인스턴스)가 없을 수도 있어..
그래서 '다른 인스턴스의 수명'이 더 짧을 때 weak!!! 약한 참조를 하라는 말입니당..!!!

 

 

 

 

 

Weak References (약한 참조)

약한 참조는, 참조하는 인스턴스를 강하게 붙들지 않는 참조라서 ARC가 그 참조되는 인스턴스를 정리하는 것을 막지 않습니다. 이러한 작용은 참조가 순환참조의 일부분이 되는 것을 막아 줍니다. 당신은 프로퍼티나 변수 선언 앞에 weak 키워드를 붙여서 약한 참조를 명시할 수 있습니다.

 

약한 참조는 참조하는 인스턴스를 강하게 붙들지 않기 때문에(몇 번 나오는지 ㅎㅎ..), 약한 참조가 아직 그 인스턴스를 참조하고 있을 때에 인스턴스가 메모리에서 해제되는 것이 가능합니다. 따라서, ARC는 참조되는 인스턴스가 메모리에서 해제되면 약한 참조를 nil로 자동으로 설정해 줍니다. 그리고 약한 참조는 런타임 시 값이 nil로 바뀌는 것이 가능해야 하기 때문에, 상수 대신 늘 옵셔널 변수로 선언됩니다.

 

약한 참조에서 값의 유무는 그냥 다른 옵셔널 변수들처럼 확인해 볼 수 있고, 당신은 절대 더이상 존재하지 않는 무효한 인스턴스를 참조하는 일이 없을 것입니다. 

주의
ARC가 약한 참조를 nil로 설정할 때 프로퍼티 옵저버는 호출되지 않습니다.

 

 

아래 예시는 중요한 한 가지를 제외하고는 위의 Person과 Apartment 예시와 동일합니다. 이번에는 Apartment 타입의 tenant 프로퍼티가 약한 참조로 선언되어 있습니다:

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 변수가 붙드는 강한 참조를 nil로 지정함으로써 강한 참조를 끊을 때, 더이상 Person 인스턴스를 강한 참조하는 것이 없다는 것을 의미합니다. 

john = nil
// "John Appleseed is being deinitialized" 출력

 

 

Person 인스턴스를 강한 참조하는 것이 없으니, 메모리에서 해제되고, tenant 프로퍼티는 nil로 설정됩니다:

 

 

 

Apartment 인스턴스를 강한 참조하는 것은 unit4A 변수 뿐입니다. 강한 참조를 끊으면, 더이상 Apartment 인스턴스를 강한 참조하는 것은 없게 됩니다:

unit4A = nil
// "Apartment 4A is being deinitialized" 출력

 

 

Apartment 인스턴스를 강한 참조하는 것이 없기 때문에, 이것도 메모리에서 해제됩니다:

 

 

주의
가비지 컬렉션을 사용하는 시스템에서는, 강한 참조가 없는 객체들은 메모리 압박이 가비지 컬렉션을 유발할 때에만 메모리에서 해제되기 때문에, weak 포인터는 종종 단순한 캐싱 매커니즘을 실행하기 위해서 사용됩니다. 하지만 ARC에서는 가장 마지막에 남아 있던 강한 참조가 사라지자마자 값들이 메모리에서 해제되기 때문에, 약한 참조를 위와 같은 목적으로 생성하는 것은 적합하지 않습니다.

 

 

 

 

 

다음 번에는 Unowned References 미소유 참조를 다뤄 보겠습니다~~!