티스토리 뷰
[시작]
- 참조 타입의 경우 하나의 인스턴스가 참조를 통해 여러 곳에서 접근되므로 메모리 해제 시점이 매우 중요하다.
- 인스턴스가 적절한 시점에 메모리에서 해제되지 않으면 메모리 자원을 낭비하게 되며, 이는 성능 저하로 이어진다.
- Swift는 프로그램 메모리 사용을 관리하기 위해 ARC(Automatic Reference Counting)라는 것을 사용한다.
[ARC란]
- Automatic Reference Counting
- 자동으로 메로리를 관리해줌
- 더이상 필요하지 않은 클래스의 인스턴스를 메모리에서 해제하는 식으로 작동한다.
- 가바지 컬렉션 기법과의 차이
: ARC는 컴파일 시 참조를 계산하므로, 인스턴스가 언제 메모리에서 해제될지 예측하기 쉽고 메모리 관리를 위한 추가 자원을 할당할 필요가 없으나, ARC의 작동 규칙을 모르고 사용하면 인스턴스가 메모리에서 영원히 해제되지 않을 가능성이 있다.
: 가비지 컬렉션은 실행 시 동적으로 참조를 계산하므로, 복잡한 상황에서 인스턴스를 해제할 수 있는 가능성이 더 높으나, 메모리 감시를 위한 추가 자원이 필요하여 성능 저하가 일어날 수 있고, 정확히 언제 메모리에서 해제될지 알기 힘들다.
- 클래스의 인스턴스를 생성할 때마다 ARC는 그 인스턴스에 대한 정보를 저장하기 위한 메모리 공간을 따로 할당한다.
: 그 메모리 공간에는 인스턴스의 타입 정보, 인스턴스의 프로퍼티 등을 저장한다.
: 인스턴스가 더 이상 필요 없는 상태가 되면 ARC가 메모리에서 인스턴스를 해제한다.
- 더 사용해야 하는 인스턴스를 해제한다면 오류가 일어날 수 있으므로, 이러한 상황을 ARC는 계속해서 추적한다.
: ARC는 인스턴스가 메모리에서 해제되지 않도록 인스턴스 참조 여부를 계속해서 추적한다.
: 어느 한 곳에서 해당 인스턴스를 참조하고 있다면 ARC가 그 인스턴스를 해제하지 않고 유지해야 하는 명분이 된다.
: 인스턴스를 메모리에 유지시키려면 이러한 명분을 ARC에 제공해야 한다.
[강한참조]
- 인스턴스가 계속해서 메모리에 남아있어야 하는 명분을 만들어 주는 것.
- 강한참조를 사용하면 참조 횟수가 1 증가하며, 강한참조를 사용하는 프로퍼티, 변수, 상수 등에 nil을 할당하면 원래 자신에게 할당되어 있던 인스턴스의 참조 횟수가 1 감소한다.
: 인스턴스는 참조 횟수가 0이 되는 순간 메모리에서 해제되며, 메모리에서 해제되기 직전 소멸자를 호출한다. (deinit)
- 참조의 기본은 강한참조이므로 별도 식별자를 명시하지 않으면 강한참조를 한다.
[강한참조 - 강한참조 순환 문제]
- 인스턴스끼리 서로가 서로를 강한참조할 때, 강한참조 순환이 일어난다.
class Person {
let name: String
init() {
self.name = "Presto"
}
var room: Room?
deinit {
print("Person Deinitialized")
}
}
class Room {
let number: String
init() {
self.number = "ABC"
}
var host: Person?
deinit {
print("Room Deinitialized")
}
}
var presto: Person? = Person() //Person 인스턴스 참조 1
var room: Room? = Room() //Room 인스턴스 참조 1
presto?.room = room //Room 인스턴스 참조 2
room?.host = presto //Person 인스턴스 참조 2
presto = nil //Person 인스턴스 참조 1
room = nil //Room 인스턴스 참조 1
//소멸자 호출되지 않음
//메모리에서 해제되지 않음
이처럼 두 인스턴스가 서로를 참조하는 상황에서 강한참조 순환 문제가 발생할 수 있다.
class Person {
let name: String
init() {
self.name = "Presto"
}
var room: Room?
deinit {
print("Person Deinitialized")
}
}
class Room {
let number: String
init() {
self.number = "ABC"
}
var host: Person?
deinit {
print("Room Deinitialized")
}
}
var presto: Person? = Person() //Person 인스턴스 참조 횟수 1
var room: Room? = Room() //Room 인스턴스 참조 횟수 1
presto?.room = room //Room 인스턴스 참조 횟수 2
room?.host = presto //Person 인스턴스 참조 횟수 2
presto?.room = nil //Room 인스턴스 참조 횟수 1
room?.host = nil //Person 인스턴스 참조 횟수 1
presto = nil //Person 인스턴스 참조 횟수 0
room = nil //Room 인스턴스 참조 횟수 0
//Person 클래스의 소멸자 호출됨
//Room 클래스의 소멸자 호출됨
이처럼 일일히 참조 횟수를 계산해가면서 코드를 작성할 수도 있겠으나, 좋은 방법은 아닐 것이다.
[약한참조]
- 약한참조는 강한참조와 달리 자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다.
- weak 키워드를 사용하여 명시된 프로퍼티나 변수는 자신이 참조하는 인스턴스를 약한참조한다.
- 자신이 참조하는 인스턴스가 메모리에서 해제될수도 있음을 프로그래머가 예상할 수 있어야 한다.
: 그 인스턴스를 강한참조하던 프로퍼티나 변수의 참조 횟수를 감소시켜 0으로 만들면 그 인스턴스가 메모리에서 해제되기 때문.
- 약한참조에는 nil이 할당될 수 있어야 하므로 상수에서는 사용될 수 없다.
: 옵셔널 변수만 약한참조할 수 있다.
class Person {
let name: String
init() {
self.name = "Presto"
}
var room: Room?
deinit {
print("Person Deinitialized")
}
}
class Room {
let number: String
init() {
self.number = "ABC"
}
weak var host: Person?
deinit {
print("Room Deinitialized")
}
}
var presto: Person? = Person() //Person 인스턴스의 참조 횟수 : 1
var room: Room? = Room() //Room 인스턴스의 참조 횟수 : 1
room?.host = presto //Person 인스턴스의 참조 횟수 : 1 | host가 약한참조
presto?.room = room //Room 인스턴스의 참조 횟수 : 2 | room이 강한참조
presto = nil //Person 인스턴스의 참조 횟수 : 0, Room 인스턴스의 참조 횟수 : 1 | 메모리 해제
//인스턴스가 메모리에서 해제될 때, 자신의 프로퍼티가 강한참조를 하던 인스턴스의 참조 횟수를 1 감소시킨다!
print(room?.host) //nil
room = nil //Room 인스턴스의 참조 횟수 : 0 | 메모리 해제
인스턴스가 메모리에서 해제될 때, 해제되는 인스턴스의 프로퍼티가 강한참조를 하던 인스턴스의 참조 횟수를 1 감소시킨다.
[미소유참조]
- 미소유참조(Unowned Reference) 또한 인스턴스의 참조 횟수를 증가시키지 않는다.
- 약한참조와는 다르게 자신이 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반으로 작동한다.
: 인스턴스가 메모리에서 해제되더라도 스스로 nil을 할당해주지 않는다.
: 미소유참조를 하는 변수나 프로퍼티는 옵셔널이나 변수일 필요가 없다.
- 미소유참조를 하면서 인스턴스가 메모리에서 해제되고 그것에 접근하려고 하면 오류가 발생하므로 참조하는 동안 해당 인스턴스가 메모리에서 해제되지 않으리라는 확신이 있을 때만 사용해야 한다.
- unowned 키워드를 사용하여 명시된 프로퍼티나 변수(상수)는 자신이 참조하는 인스턴스를 미소유참조한다.
class Person {
let name: String
//카드를 소지할 수도, 소지하지 않을 수도 있다 -> 옵셔널
//카드를 한 번 가진 후 잃어버리면 안되므로 강한참조
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("\(name) Deinitialized")
}
}
class CreditCard {
let number: UInt
//카드는 소유자가 분명히 존재해야 한다.
unowned let owner: Person
init(number: UInt, owner: Person) {
self.number = number
self.owner = owner
}
deinit {
print("Card #\(number) Deinitialized")
}
}
//Person 인스턴스의 참조 횟수 : 1
var presto: Person? = Person(name: "Presto")
if case let person? = presto {
//CreditCard 인스턴스의 참조 횟수 : 1
person.card = CreditCard(number: 123, owner: person)
//Person 인스턴스의 참조 횟수 : 1 | 미소유참조
}
presto = nil
//Person 인스턴스의 참조 횟수 : 0
//CreditCard 인스턴스의 참조 횟수 : 0
사람이 카드를 소유하고 있다가 사람이 죽으면 카드도 없어지는 것이다!!!!!!
이를 미소유참조로 표현하면서 강한참조 순환 문제도 함께 해결하였다.
[미소유참조와 암시적 추출 옵셔널 프로퍼티]
class Company {
let name: String
var ceo: CEO!
init(name: String, ceoName: String) {
self.name = name
self.ceo = CEO(name: ceoName, company: self)
}
func introduce() {
print("\(name)의 CEO는 \(ceo.name)입니다.")
}
}
class CEO {
let name: String
unowned let company: Company
init(name: String, company: Company) {
self.name = name
self.company = company
}
func introduce() {
print("\(name)는 \(company.name)의 CEO입니다.")
}
}
let company: Company = Company(name: "회사", ceoName: "이름")
company.introduce()
company.ceo.introduce()
...어렵다...
미소유참조는 약한참조를 사용할 수 없는 경우(옵셔널이 아니어야 하거나 상수로 지정해야 하는 경우)에 강한참조를 피하기 위해 사용 가능하다.
[클로저의 강한참조 순환]
- 강한참조 순환 문제는 클로저가 인스턴스의 프로퍼티일 때나, 클로저의 값 획득 특성 때문에 발생하기도 한다.
: 클로저 내부에서 self.ㅁ 처럼 인스턴스의 프로퍼티에 접근하거나 인스턴스의 메소드를 호출할 때 값 획득이 발생할 수 있는데, 이 경우 클로저가 self를 획득하므로 강한참조 순환이 발생한다.
- 클로저의 획득 목록을 통해 클로저의 강한참조 순환 문제를 해결할 수 있다.
class Person {
let name: String
let hobby: String?
lazy var introduce: () -> String = {
var introduction: String = "My name is \(self.name)."
guard let hobby = self.hobby else { return introduction }
introduction += " "
introduction += "My hobby is \(hobby)."
return introduction
}
init(name: String, hobby: String? = nil) {
self.name = name
self.hobby = hobby
}
deinit {
print("\(name) is being deinitialized")
}
}
var presto: Person? = Person(name: "Presto", hobby: "What")
print(presto?.introduce())
presto = nil
introduce 프로퍼티를 지연 저장 프로퍼티로 정의했기 때문에 내부에서 self를 통해 접근할 수 있다.
(클로저 내부에서 호출하는 self 프로퍼티는 참조 횟수를 증가시키지 않는다.)
- 클로저는 자신이 호출되면 언제든지 자신 내부의 참조를 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지한다.
- 이 때 자신을 프로퍼티로 갖는 인스턴스의 참조 횟수도 증가시킨다.
- 이리하여 위의 코드에서 소멸자는 호출되지 않는다.
[클로저의 강한참조 순환 해결 - 획득목록]
- 위의 문제를 획득목록Capture List을 통해 해결할 수 있다.
: 클로저 내부에서 참조 타입을 획득하는 규칙을 제시해줄 수 있는 기능
- 획득목록은 클로저 내부의 매개변수 목록 이전 위치에 작성해준다.
- 참조 방식과 참조할 대상을 대괄호로 둘러싼 목록 형식으로 작성하며 획득목록 뒤에는 in 키워드를 써준다.
- 획득목록에 명시한 요소가 참조 타입이 아니라면 해당 요소들은 클로저가 생성될 때 초기화된다.
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
b = 20
}
a = 10
b = 10
closure() //0 10
print(b) //20
- 획득목록에 명시한 요소가 값 타입일 때 :
- 변수 a는 클로저의 획득목록을 통해 클로저가 생성될 때 값 0을 획득하였으나 변수 b는 따로 값을 획득하지 않았다.
- a는 클로저가 생성되었을 때 획득한 값을 갖지만, b는 변경된 값을 사용하고 있다.
- a 변수는 클로저가 생성됨과 동시에 획득목록 내에서 다시 a라는 이름의 상수로 초기화된 것이므로 외부에서 a의 값을 변경하더라도 클로저의 획득목록을 통한 a와는 별개가 된다.
class SimpleClass {
var value: Int = 0
}
var x = SimpleClass()
var y = SimpleClass()
let closure = { [x] in
print(x.value, y.value)
}
x.value = 10
y.value = 10
closure() //10 10
- 획득목록에 명시한 요소가 참조 타입일 때 :
- 값 타입과는 다르게 서로 같게 동작한다.
- 참조 타입은 획득목록에서 어떤 방식으로 참조할 것인지 정해줄 수 있다.
: 강한획득 / 약한획득 / 미소유획득
- 획득의 종류에 따라 참조 횟수를 증가시킬지 결정할 수 있다.
- 약한획득을 할 경우 획득목록에서 획득하는 상수가 옵셔널 상수로 지정된다.
class SimpleClass {
var value: Int = 0
}
var x: SimpleClass? = SimpleClass()
var y = SimpleClass()
let closure = { [weak x, unowned y] in
print(x?.value, y.value)
}
x = nil
y.value = 10
closure() //nil 10
x는 약한참조 하였으므로 인스턴스가 메모리에서 해제되면 클로저 내부에서도 참조가 불가능하다.
y는 미소유참조 하였으므로 클로저가 참조 횟수를 증가시키지는 않지만, 메모리에서 해제된 상태에서 사용하려 했다면 애플리케이션이 강제종료 될 것이다.
class Person {
let name: String
let hobby: String?
lazy var introduce: () -> String = { [unowned self] in
var introduction: String = "My name is \(self.name)."
guard let hobby = self.hobby else { return introduction }
introduction += " "
introduction += "My hobby is \(hobby)."
return introduction
}
init(name: String, hobby: String? = nil) {
self.name = name
self.hobby = hobby
}
deinit {
print("\(name) is being deinitialized")
}
}
var presto: Person? = Person(name: "Presto", hobby: "What")
print(presto?.introduce())
presto = nil
self를 미소유참조하여 클로저의 강한참조 순환 문제를 해결하는 코드.
self 프로퍼티를 미소유참조한 것은, 해당 인스턴스가 존재하지 않는다면 프로퍼티도 호출할 수 없으므로 self는 미소유참조를 하더라도 실행 중에 오류를 발생시킬 가능성이 거의 없다고 볼 수 있기 때문이다.
하지만 역시 문제가 발생할 수 있는 상황이 있으며, 그러므로 미소유참조는 신중히 사용해야 하고, 약한참조를 사용한 처리도 괜찮다.
아 어렵다!!!!!!!
'Swift > 야곰의 스위프트' 카테고리의 다른 글
2회독 정리 (0) | 2018.10.03 |
---|---|
26장 where 절 (0) | 2018.04.30 |
25장 패턴 (0) | 2018.04.28 |
24장 타입 중첩 (0) | 2018.04.28 |
23장 프로토콜 지향 프로그래밍 (0) | 2018.04.28 |