Swift에서는 앱의 메모리 사용을 관리하기 위해 ARC(Automatic Reference Counting)을 사용합니다. 자동으로 참조 횟수를 관리하기 때문에 대부분의 경우에 개발자는 메모리 관리에 신경쓸 필요가 없고 ARC가 알아서 더이상 사용하지 않는 인스턴스를 메모리에서 해지합니다. 하지만 몇몇의 경우 ARC에서 메모리 관리를 위해 코드의 특정 부분에 대한 관계에 대한 정보를 필요로 합니다. 참조 횟수는 클래스 타입의 인스턴스에만 적용되고 값 타입인 구조체 열거형 등에는 적용되지 않습니다.
ARC의 동작 (How ARC Works)
클래스의 새 인스턴스를 만들 때마자 ARC는 인스턴스 정보를 담는데 필요한 적정한 크기의 메모리를 할당합니다. 이 메모리는 그 인스턴스에 대한 정보와 관련된 저장 프로퍼티 값도 갖고 있습니다. 추가적으로 인스턴스가 더이상 사용되지 않을 때 ARC는 그 인스턴스가 차지하고 있는 메모리를 해지해서 다른 용도로 사용할 수 있도록 공간을 확보해 둡니다. 하지만 만약 ARC가 아직 사용중인 인스턴스를 메모리에서 내렸는데 인스턴스의 프로퍼티에 접근한다면 앱은 아마 크래시가 발생하게됩니다. ARC에서는 아직 사용중인 인스턴스를 해지하지 않기 위해 얼마나 많은 프로퍼티, 상수 혹은 변수가 그 인스턴스에 대한 참조를 갖고 있는지 추적합니다. 그래서 ARC는 최소 하나라도 그 인스턴스에 대한 참조가 있는 경우 그 인스턴스를 메모리에서 해지 하지 않습니다.
ARC의 사용 (ARC in Action)
// 아래 코드는 하나의 클래스를 선언하고 클래스의 인스턴스가 생성될 때와 해지될때 print로 로그를 찍게 구현한 클래스 입니다.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
// 위에서 선언한 Person클래스 타입을 갖는 reference 변수 3개를 선언합니다. 이 변수는 모두 옵셔널 변수입니다. 그래서 초기값으로 모두 nil을 갖고 있습니다.
var reference1: Person?
var reference2: Person?
var reference3: Person?
// 하나의 변수에 Person 인스턴스를 생성에 참조하도록 합니다.
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
// 나머지 두 변수를 첫 번째 변수를 참조하도록 합니다.
reference2 = reference1
reference3 = reference1
/* 이 경우 reference2, reference3모두
처음에 reference1이 참조하고 있는 같은 Person인스턴스를 참조하게 됩니다.
이 시점에 Person인스턴스에 대한 참조 횟수는 3이 됩니다.
그리고 나서 reference1, reference2두 변수의 참조를 해지합니다.
그렇게 되면 Person 인스턴스에 대한 참조 횟수는 아직 1이어서 Person인스턴스는 해지되지는 않습니다. */
reference1 = nil
reference2 = nil
// Person인스턴스를 참조하고있는 나머지 변수 reference3의 참조를 해지하면 더이상 Person인스턴스를 참조하고있는 것이 없으므로 ARC가 Person인스턴스를 메모리에서 해지하게 됩니다.
reference3 = nil
// Prints "John Appleseed is being deinitialized"
클래스 인스턴스간 강한 참조 순환 (Strong Reference Cycles Between Class Instances)
앞선 예제에서 보았다시피 ARC에서 기본적으로 참조 횟수에 대해 추적하고 있기 때문에 더이상 사용하지 인스턴스는 자동으로 메모리에서 해제되게 됩니다. 하지만 절대로 메모리에서 해제 되지 않는 경우도 있습니다. 예를들어, 클래스의 인스턴스간 강하게 상호 참조를 하고 있는 경우가 바로 그경우 입니다. 이 경우는 강한 참조 순환이라 알려져 있습니다. 예를 통해 어떻게 강한 참조 순환이 발생하는지 알아 보겠습니다.
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이라는 클래스는 변수로 Apartment클래스의 인스턴스를 소유하고 있고
그 Apartment클래스에서는 변수로 Person형의 인스턴스를 소유하고 있습니다.
만약 다음과 같이 변수를 선언하고 인스턴스를 생성하면 어떤 일이 발생할까요?
Person과 Apartment형의 변수를 각각 선언합니다. */
var john: Person?
var unit4A: Apartment?
// 선언한 변수에 각각에 맞는 타입의 인스턴스를 생성합니다. 지금 여기까지는 아무 문제가 없습니다.
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
// 이 상황에서 john의 apartment 변수에 unit4A를 unit4A.tenant (세입자)에 john을 할당해 보겠습니다.
john!.apartment = unit4A
unit4A!.tenant = john
클래스 인스턴스간 강한 참조 순환 문제의 해결 (Resolving Strong Reference Cycles Between Class Instances)
앞에서 살펴본 강한 참조 순환 문제를 해결하기 위해서는 두가지 방법이 있습니다. 하나는 weak 참조, 다른 하나는 unowned 참조를 사용하는 것입니다. weak 참조, unowned 참조 모두 ARC에서 참조 횟수를 증가시키지 않고 인스턴스를 참조합니다. 그래서 강한 참조 순환 문제를 해결할 수 있습니다. 약한 참조는 참조하고 있는 인스턴스가 먼저 메모리에서 해제될때 사용하고 미소유 참조는 반대로 참조하고 있는 인스턴스가 같은 시점 혹은 더 뒤에 해제될때 사용합니다. 위 Apartment 예제에서 Apartment의 tenant는 없는 상태가 될 수 있기 때문에 (먼저 해지되기 때문에) 약한 참조를 사용하는 것이 적절합니다.
// 약한 참조 (Weak References)
/* 약한 참조로 선언하면 참조하고 있는 것이 먼저 메모리에서 해제되기 때문에
ARC는 약한 참조로 선언된 참조 대상이 해지 되면 런타임에 자동으로 참조하고 있는 변수에 nil을 할당합니다. */
// 예제를 살펴 보겠습니다. 아래 예제에서 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") }
}
// 앞선 예제처럼 Person 인스턴스와 Apartment 인스턴스의 변수에서 각각 인스턴스를 상호 참조하도록 할당합니다.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
// 미소유 참조 (Unowned References)
/* 미소유 참조는 약한 참조와 다르게 참조 대상이 되는 인스턴스가
현재 참조하고 있는 것과 같은 생애주기(lifetime)를 갖거나 더 긴 생애 주기(longer lifetime)를 갖기 때문에
항상 참조에 그 값이 있다고 기대됩니다. 그래서 ARC는 미소유 참조에는 절대 nil을 할당하지 않습니다.
다시말하면 미소유 참조는 옵셔널 타입을 사용하지 않습니다. */
// 예제를 통해 미소유 참조의 동작을 확인해 보도록 하겠습니다. 우선 Customer와 CreditCard 두개의 클래스를 선언합니다.
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") }
}
'Swift Language' 카테고리의 다른 글
Swift Language - 24 - 접근제어 (Access Control) (0) | 2023.02.06 |
---|---|
Swift Language - 23 - 메모리 안정성 (Memory Safety) (0) | 2023.02.06 |
Swift Language - 21 - 제네릭 (Generics) (0) | 2023.02.06 |
Swift Language - 20 - 프로토콜 (Protocols) (0) | 2023.02.06 |
Swift Language - 19 - 익스텐션 (Extensions) (0) | 2023.02.04 |