ARC(Automatic Reference Counting)
- 자동 참조 카운팅-
ARC, weak, strong, unowned 더 나아가 weak self까지 살펴보고
개념을 확실히 잡아보고자 작성을 시작한 포스팅입니다.😃
서론
ARC는 Swift 프로그래밍 언어에서 메모리 관리를 자동으로 처리해주는 메커니즘입니다.
ARC는 애플이 제공하는 메모리 관리 시스템으로, 개발자가 수동으로 메모리를 할당하고 해제하는 번거로움을 줄여줍니다.
대부분의 경우에 Swift에서 메모리 관리는 “그냥 수행해라!”를 의미하고 메모리 관리에 대해서 생각할 필요가 없습니다.
ARC는 인스턴스가 더이상 필요치 않을 때 자동으로 클래스 인스턴스에 의해 사용된 메모리를 할당 해제합니다.
그러나, 몇몇 경우에 ARC는 메모리를 관리하기 위해 코드의 부분분간의 관계에 대한 추가 정보를 요구합니다.
이번 포스팅에서는 이러한 상황을 살펴보고 앱의 메모리를 관리하기 위해 ARC를 어떻게 사용하는지 살펴볼 예정입니다.
ARC는 클래스의 인스턴스에만 적용됩니다.
구조체와 열거형은 참조 타입이 아니고 값타입이므로 참조로 저장 되거나 전달 되지 않습니다.
ARC의 작동 원리
클래스의 새로운 인스턴스가 생성될 때마다 ARC는 인스턴스에 대한 정보를 저장하기 위해 메모리의 블록에 할당합니다.
이 메모리는 해당 인스턴스와 관련된 저장 프로퍼티의 값과 함께 인스턴스 타입에 대한 정보를 가지게 됩니다.
인스턴스가 더이상 필요하지 않을 때 ARC는 메모리가 다른 목적으로 사용될 수 있도록 사용된 메모리를 할당 해제합니다.
이렇게 하면! 클래스 인스턴스가 더이상 필요하지 않을 때 메모리 공간을 차지하지 않게 됩니다.
그러나! ARC가 아직 사용중인 인스턴스의 할당을 해제하면 더이상 인스턴스의 프로퍼티에 접근할 수 없거나 메서드를 호출할 수 없습니다. 실제로 인스턴스에 접근하려고 하면? 크래시가 발생합니다.
인스턴스가 여전히 필요한 동안 사라지지 않도록
ARC는 얼마나 많은 프로퍼티, 상수, 변수가 각 클래스 인스턴스에 참조하고 있는지 Counting 합니다.
ARC는 인스턴스에 참조가 하나라도 존재하는 한 인스턴스를 할당 해제 하지 않습니다.
이것을 가능하게 하려면 프로퍼티, 상수, 변수에 클래스 인스턴스를 할당할때마다 강한 참조를 만듭니다.
여기서 참조는 해당 인스턴스를 유지하고 강한 참조가 남아있는 한 할당 해제를 혀용하지 않기 때문에 강한(strong
) 참조라고 합니다.
ARC 동작
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(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1
reference1 = nil
reference2 = nil
reference3 = nil
// Prints "John Appleseed is being deinitialized"
ARC는 마지막 강한 참조를 중단할 때 까지 Person의 인스턴스 할당 해제를 하지 않습니다.
클래스 인스턴스 사이의 강한 참조 사이클 Strong!
클래스의 인스턴스가 강한 참조가 없는 지점에 도달하지 않는 코드를 작성할 수도 있습니다.
이는 두 클래스 인스턴스가 서로에 대한 강한 참조를 유지하여 각 인스턴스가 다른 인스턴스를 유지하는 경우 발생할 수 있습니다.
이것을 강한 참조 사이클이라고 부릅니다.
클래스 간의 일부 관계를 강한 참조 대신 weak나 unowned 참조로 정의하여 강한 참조 사이클을 해결할 수 있습니다.
해결 방법을 살펴보기전에 강한 참조 사이클이 발생하는 상황을 보면 좋을 것 같습니다 :)
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") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
위의 예제는 클래스간의 강한 참조가 발생하는 예제입니다.
위의 그림과 같이 클래스 인스턴스들이 서로를 참조하고 있을 때! `john`과 `unit4A`에 대한 강한 참조를 중단할 때
`Person`과 `Apartment`인스턴스가 서로를 강한 참조하고 있기 때문에
참조카운트는 0으로 떨어지지 않고 인스턴스는 ARC에 의해 `deinit`되지 않게 되는 상황이 발생합니다.
john = nil
unit4A = nil
이러한 상황이 발생될 때 `Person`과 `Apartment`인스턴스가 메모리에서 할당 해제되지 않아 메모리 누수를 유발하게 됩니다.
강한 참조 사이클 해결
그렇다면 강한 참조 사이클을 어떻게 해결할 수 있을까요?
Swift는 강한 참조 사이클 해결하기 위해 2가지 방법을 제공합니다.
바로 약한 참조(weak)와 미소유 참조(unowned).
약한 참조와 미소유 참조를 사용하면 참조 사이클의 한 인스턴스가 강한 유지 없이 다른 인스턴스를 참조할 수 있습니다.
그런 다음 인스턴스는 강한 참조 사이클을 만들지 않고도 서로를 참조할 수 있습니다.
약한 참조 (Weak References)
약한 참조는 참조하는 인스턴스를 강하게 유지하지 않는 참조 유형입니다.
이는 ARC가 참조된 인스턴스를 처리하는 데 영향을 미치지 않습니다.
프로퍼티나 변수를 선언할 때, 약한 참조임을 나타내기 위해 `weak` 키워드를 사용합니다.
약한 참조는 참조하는 동안에도 해당 인스턴스가 강제로 살아있게 하지 않기 때문에,
참조하는 인스턴스가 다른 곳에서 더 이상 필요하지 않다면 할당이 해제될 수 있습니다.
만약 약한 참조한 인스턴스가 할당 해제되면, ARC는 자동으로 해당 약한 참조를 nil로 설정합니다.
이는 약한 참조가 더 이상 유효한 참조를 갖지 않음을 나타냅니다.
따라서 약한 참조를 통해 참조하던 인스턴스가 메모리에서 해제될 때,
약한 참조는 자동으로 무효화되어 프로그램이 안전하게 동작할 수 있도록 합니다.
약한 참조는 런타임에 값을 nil로 변경하는 것이 가능해야 하므로 항상 옵셔널 타입의 상수가 아닌 변수로 선언된다!
프로퍼티 관찰자는 ARC가 약한 참조를 nil로 설정할 때 호출되지 않는다.
이렇게 글로만 설명하려니 이해가 잘 안되네요 🥲
Swift 공식문서 예제를 살펴보면 좋을 것 같아요.
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 }
// 약한 참조는 런타임에 nil값이 되어야 하므로
// 항상 옵셔널 타입의 var로 선언 된다.
weak var tenant: Person?
deinit { print("Apartment \\(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
여기서 john과 Person의 연결을 끊으면 어떻게 될까요?
john = nil
// Prints "John Appleseed is being deinitialized"
john에 nil을 할당하게 되면 Person의 RC는 0이 되므로 Person 객체는 deinit 되게 됩니다.
RC를 계산하는게 조금 어려웠는데 자신에게 향하는 strong 참조를 생각해보면 쉽더라구요.
또한 더이상 Person인스턴스에 관하여 강한 참조를 가지지 않기 때문에 deinit되고 tenant 프로퍼티는 nil로 설정 됩니다.
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
미소유 참조(Unowned References)
그렇다면 미소유 참조는 언제 사용할까요?
약한 참조와 마찬가지로 미소유 참조는 참조하는 인스턴스를 강하게 유지하지 않습니다.
그러나 약한 참조와 다르게 미소유 참조는 다른 인스턴스의 수명이 같거나 더 긴 경우에 사용합니다.
약한 참조와 달리 미소유 참조는 항상 값을 갖도록 예상됩니다.
결과적으로 미소유로 만들어진 값은 옵셔널로 만들어 지지 않고 ARC는 미소유 참조의 값을 nil로 설정하지 않습니다.
참조가 항상 할당 해제되지 않은 인스턴스를 참조한다고 확신하는 경우에만 미소유 참조를 사용합니다.
weak 와 공통점?
- 강한 순환참조를 해결할 수 있음
- RC 값을 증가시키지 않음
차이점?
- 인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신
- 참조하던 인스턴스가 만약 메모리에서 해제된 경우, nil을 할당 받지 못하고 해제된 메모리 주소값을 계속 들고 있음
- unowned로 선언된 변수가 가리키던 인스턴스가 먼제 메모리에서 해제된 경우 -> 접근할 때 에러가 발생함.
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") }
}
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\\(name) is being deinitialized") }
}
CreditCard 보다 “Customer가 오래 산다는 가정”하에 CreditCard의 Customer 프로퍼티를 unowned로 선언합니다.
이렇게 선언한 것은 메모리에 올라가는 방식이 weak와 똑같습니다.
unowned가 붙은 customer가 가리키는 Customer 인스턴스는 CreditCard가 죽기 전까지 절대 절대 살아있어야 한다..
둘 중에 수명이 더 긴 인스턴스를 가리키는 프로퍼티를! 미소유 참조로 선언해야한다!
결론
strong, weak, unowned에 대한 차이점은 객체의 라이프사이클에 대해서 차이점을 두고 있다고 생각이 되네요.
[Swift] strong, weak, unowned의 비밀 여기 포스팅에 굉장히 정리가 잘 되어있으니 한 번 살펴보시면 좋을것 같네용.
-완-
참고 :
'Swift' 카테고리의 다른 글
Swift - 컴플리션 핸들러를 async로 감싸기 (0) | 2024.01.22 |
---|---|
Swift - Optional과 Optional Binding (1) | 2023.12.29 |
Swift - 타입 캐스팅 톺아보기 (0) | 2023.12.29 |
Swift - [weak self] 톺아보기 (0) | 2023.12.21 |
iOS 공부하는 중🌱