일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- ios
- 운영체제
- 컨테이너
- 도커 이미지
- 데브옵스
- AWS
- docker
- centOS7
- 인프라
- 쿠버네티스
- 도커
- linux
- C++
- NGINX
- 리눅스
- Swift
- 프로세스
- 클라우드
- devops
- kubernetes
- k8s
- 도커 컨테이너
- swift 클로저
- boj
- 네트워크
- 부스트코스
- 도커 명령어
- centOS
- os
- Python
- Today
- Total
귀염둥이의 메모
[Swift] 자동 참조 카운트 정리 ARC (Automatic Reference Counting) 본문
ARC(Automatic Reference Counting)
Swift에서는 앱의 메모리 사용을 관리하기 위해 ARC(Automatic Reference Counting)을 사용합니다.
참조 횟수를 자동으로 관리하기 때문에 대부분의 경우에 개발자는 메모리 관리에 신경 쓸 필요가 없고 ARC가 알아서 더 이상 사용하지 않는 인스턴스를 메모리에서 해지합니다. 하지만 몇몇의 경우 ARC에서 메모리 관리를 위해 코드의 특정 부분에 대한 관계에 대한 정보를 필요로 합니다. 참조 횟수는 클래스 타입의 인스턴스에만 적용되고 값 타입인 구조체 열거형 등에는 적용되지 않습니다.
ARC의 동작
클래스의 새로운 인스턴스를 만들 때마다 ARC는 인스턴스 정보를 담는데 필요한 적정한 크기의 메모리를 할당합니다.
이 메모리는 그 인스턴스에 대한 정보와 관련된 저장 프로퍼티 값도 갖게 됩니다. 인스턴스가 더 이상 사용되지 않을 때 ARC는 인스턴스가 차지하고 있는 메모리를 해제하여 공간을 확보합니다. 만약에 ARC가 아직 사용 중인 인스턴스를 메모리에서 내리면 인스턴스의 프로퍼티에 접근할 때 앱은 크래시가 발생합니다. ARC에서는 사용 중인 인스턴스를 해지하지 않기 위해서 얼마나 많은 프로퍼티, 상수, 변수가 해당 인스턴스에 대한 참조를 갖고 있는지 추적합니다. ARC는 최소 하나라도 그 인스턴스에 대한 참조가 있으면 메모리에서 해제하지 않습니다.
ARC의 사용
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
Person 클래스를 선언하고 인스턴스가 생성될 때와 해제될 때 print를 통해 확인합니다.
var reference1: Person?
var reference2: Person?
var reference3: Person?
Person 클래스 타입을 갖은 reference 변수를 선언합니다. 모두 옵셔널 변수이며 초기값으로 nil을 갖습니다.
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1
reference1 변수에 Person 인스턴스를 참조시키고, 나머지 두 변수들이 reference1을 참조하도록 합니다.
이 경우 reference2, reference3모두 처음에 reference1이 참조하고 있는 같은 Person인스턴스를 참조하게 됩니다.
이 시점에 Person인스턴스에 대한 참조 횟수는 3이 됩니다.
reference1 = nil
reference2 = nil
reference1, reference2 두 변수의 참조를 해제합니다.
그러면 Person 인스턴스에 대한 참조 횟수는 아직 1이어서 Person인스턴스는 해제되지는 않습니다.
reference3 = nil
// Prints "John Appleseed is being deinitialized"
Person인스턴스를 참조하고 있는 나머지 변수 reference3의 참조를 해지하면 더 이상 Person인스턴스를 참조하고 있는 것이 없으므로 ARC가 Person인스턴스를 메모리에서 해제하게 됩니다.
클래스 인스턴스간 강한 참조 순환(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 클래스의 인스턴스를 소유하고 있습니다.
그리고 선언한 변수 각각에 맞는 타입의 인스턴스를 생성합니다.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
현재 변수와 인스턴스 상태를 그림으로 보면 다음과 같습니다. var john은 Person 인스턴스를 참조하고, unit 4A는 Apartment 인스턴스를 참조하고 있습니다.
현재 상황에서 john.apartment에 unit 4A를 unit4A.tenant에 john을 할당하겠습니다.
john!.apartment = unit4A
unit4A!.tenant = john
인스턴스 안의 apartment와 tenant가 각각 Apartment, Person 인스턴스를 참조하고 있는 상황이 됩니다.
Person 인스턴스의 참조 횟수는 2, Apartment 인스턴스 참조 횟수도 2가 됩니다.
이 시점에서 각 변수에 nil을 할당하여 참조를 해제하겠습니다. 각 변수가 참조하고 있던 Person과, Apartment인스턴스가 해지되어야 합니다. 그러나 이 두 인스턴스는 해지되지 않습니다.
john = nil
unit4A = nil
변수 john과 unit4A는 각 인스턴스에 대한 참조를 하고 있지 않지만 Person인스턴스와 Apartment인스턴스의 변수가 각각 상호 참조를 하고 있어 참조 횟수가 1이기 때문에 이 두 인스턴스는 해지되지 않고 메모리 누수가 발생합니다.
클래스 인스턴스간 강한 참조 순환 문제의 해결 (Resolving Strong Reference Cycles Between Class Instances)
강한 참조 순환 문제를 해결하기 위해서는 두 가지 방법이 있습니다.
weak참조와 unowned 참조를 사용하는 것입니다. 약한(weak) 참조, 미소유(unowned) 참조 모두 ARC에서 참조 횟수를 증가시키지 않고 인스턴스를 참조합니다. 그래서 강한 참조 순환 문제를 해결할 수 있습니다.
약한 참조 (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? // weak
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
앞선 예제와 다르게 Apartment의 tenant변수가 Person 인스턴스를 약한 참조(weak)로 참조하고 있습니다. 따라서 이 시점에서 Person 인스턴스에 대한 참조 횟수는 john이 참조하고 있는 1입니다.
john의 참조 대상을 nil로 할당하면 더 이상 Person 인스턴스를 참조하는 것이 없게 됩니다.
john = nil
// Prints "John Appleseed is being deinitialized"
그 결과 ARC에서 아래 그림과 같이 Person 인스턴스를 메모리에서 해제합니다.
그다음 unit4A에 nil을 할당하면
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
Apartment 인스턴스를 참조하는 개체도 사라져 Apartment 인스턴스도 메모리에서 해제됩니다.
미소유 참조(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 // unowned
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
Customer는 card 변수로 CreditCard 인스턴스를 참조하고 있고 CreditCard는 customer로 Customer 인스턴스를 참조하고 있습니다. customer는 미소유 참조 unowned로 선언합니다. 고객과 신용카드를 비교해 봤을 때 신용카드는 없더라도 사용자는 남아있을 것이기 때문입니다. 사용자는 항상 존재하기 때문에 CreditCard에 customer를 unowned로 선언합니다.
고객 변수 john을 옵셔널 타입으로 선언 후 인스턴스를 생성하고 고객의 카드 변수에도 카드 인스턴스를 생성해 할당합니다.
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
John이 Customer 인스턴스를 참조하고 있고 CreditCard 인스턴스도 Customer Instance를 참조하고 있지만 미소유(unowned) 참조를 하고 있기 때문에 Customer 인스턴스에 대한 참조 횟수는 1회가 됩니다.
john 변수의 Customer 인스턴스 참조를 끊으면 다음과 같습니다.
그러면 더 이상 Customer 인스턴스를 강하게 참조하고 있는 인스턴스가 없으므로 Customer 인스턴스가 해제되고 인스턴스가 해제됨에 따라 CreditCard 인스턴스를 참조하고 있는 개체도 사라지므로 CreditCard 인스턴스도 메모리에서 해제됩니다.
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
미소유 참조와 암시적 옵셔널 프로퍼티 추출(Unowned References and Implicitly Unwrapped Optional Properties)
약한 참조와 미소유 참조의 구분을 해당 참조가 nil이 될 수 있냐 없냐로 구분할 수 있습니다. 하지만 두 경우를 제외한 제3의 경우도 발생할 수 있습니다. 두 프로퍼티가 항상 값을 갖지만 한번 초기화되면 절대 nil이 되지 않은 경우입니다. 이 경우에는 미소유 프로퍼티를 암시적 옵셔널 프로퍼티 추출을 사용해 참조 문제를 해결할 수 있습니다.
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의 capitalCity는 초기화 단계에서 City 클래스에 초기화된 후 사용되게 됩니다. 즉 실제로 Country의 capitalCity는 옵셔널이 돼야 맞습니다. 하지만 여기서 느낌표 ! 를 이용하여 명시적으로 강제 언래핑을 시켰습니다. 암시적 언래핑이 되어 Country에서 name이 초기화되는 시점에 self를 사용할 수 있게 됩니다. 그리고 City에서는 강한 참조 순환을 피하기 위해 미소유 참조로 country를 선언해서 두 인스턴스를 문제없이 사용할 수 있습니다.
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"
클로저에서의 강한 참조 순환(Strong Reference Cycles for Closures)
강한 참조 순환은 클로저에서 발생할 수 있습니다. 클로저에서는 self를 캡쳐하기 때문입니다.
이러한 문제를 해결하기 위해서는 클로저 캡쳐 리스트를 사용합니다.
아래의 예제에서 HTMLElement 클래스의 클로저 asHTML은 입력 값을 받지 않고 반환 값이 String인 () -> String 클로저를 사용합니다. 이 클로저 안에서 self.text와 self.name과 같이 self를 캡쳐하게 됩니다.
*asHTML 클로저는 lazy로 선언이 되었습니다. 태그와 텍스트가 준비되고 나서 HTML이 필요하고, lazy 프로퍼티이기 때문에 프로퍼티 안에서 self를 참조할 수 있습니다.
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
asHTML 클로저는 다른 클로저로 변경될 수 있습니다.
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"
아래 코드는 인스턴스와 클로저 간에 강한 참조를 하게 되어서 강한 순한 참조에 빠지게 됩니다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
클로저 안에서 self를 여러 번 참조하더라도 실제로는 단 한 번의 강한 참조만 캡쳐합니다.
paragraph의 참조를 nil로 할당해도 HTMLElement 인스턴스는 해제되지 않습니다.
paragraph = nil
클로저에서 강한 참조 순환 문제의 해결(Resolving Strong Reference Cycles for Closures)
클로저에서 강한 참조 순환 문제를 해결하기 위해서 캡쳐 참조에 강한 참조 대신 약한 참조(weak) 혹은 미소유 참조(unowned)를 지정할 수 있습니다. 약한 참조인지 미소유 참조를 사용할지는 코드에서 상호 관계에 달려있습니다.
캡쳐리스트 정의(Defining a Capture List)
캡쳐리스트를 정의하기 위해서 클로저의 파라미터 앞에 대괄호 [ ]를 넣고 그 안에 각 캡쳐 대상에 대한 참조 타입을 적어 줍니다.
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
클로저의 파라미터가 없고 반환 값이 추론에 의해 생략 가능한 경우에는 캡처리스트 정의를 in 앞에 적어 줍니다.
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
약한 참조와 미소유 참조(Weak and Unowned References)
인스턴스 참조와 마찬가지로 참조가 먼저 해제되는 경우는 약한 참조를 같은 시점이나 나중 시점에 해제되는 경우에는 미소유 참조를 사용합니다.
asHTML 클로저의 self에 [unowned self]라고 캡쳐리스트를 아래 코드와 같이 적어줍니다.
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
클로저에서 HTMLElement 인스턴스를 미소유 참조로 참조하고 있습니다.
paragraph의 참조를 제거하면 HTMLElement 인스턴스가 바로 메모리에서 해제되는 것을 확인할 수 있습니다.
paragraph = nil
// Prints "p is being deinitialized"
<참고자료>
http://minsone.github.io/mac/ios/swift-automatic-reference-counting-summary
'iOS > Swift' 카테고리의 다른 글
[Swift] 클로저 (2) - Capturing Values, Escaping Closure, Auto Closure (0) | 2021.06.21 |
---|---|
[Swift] enum(열거형) 기초 (0) | 2021.05.17 |
[Swift] 클로저(Closure) (1) (0) | 2021.04.13 |
[Swift] 타입 캐스팅(Type Casting), 업 캐스팅(Up Casting), 다운 캐스팅(Down Casting) (0) | 2021.04.09 |
[Swift] 구조체(struct) vs 클래스(class) (1) | 2021.02.19 |