오늘의 주제는 그야말로 악소리나는 ARC(Automatic Reference Counting).
ARC automatically frees up the memory used by class instances when those instances are no longer needed.
Swift는 메모리 관리를 자동으로 해주는데, 더 이상 필요하지 않은 클래스의 인스턴스를 메모리에서 할당 해제한다.
즉 더는 필요하지 않다는 것을 판단하는 기준이 필요할 것인데, 이것이 참조 카운팅이다.
우선 참조가 뭔지는 지난번에 알아보았는데, 어떤 클래스 인스턴스를 참조하고 있는 곳이 몇 군데나 있는지 세는 것이 바로 참조 카운트!
그럼 카운팅은?
당연히 카운트를 세는 것이 카운팅이므로 이 참조 카운트를 자동으로 세어서 관리해주는 것이 자동 참조 카운팅, ARC이다.
그래서 기존에 개발자가 직접 참조 카운트를 관리하던 것을 MRC(Manual Reference Counting)라고 한다.
앞서 말했지만 지난번 게시글에서 참조를 설명하면서 값 타입은 참조가 발생하지 않음을 설명했는데, 구조체나 열거형이 바로 그 값 타입에 해당한다.
이 메모리에는 인스턴스 타입에 대한 정보와 해당 인스턴스와 관련된 저장 프로퍼티의 값이 들어 있다.
이렇게 하면 클래스 인스턴스가 더 이상 필요하지 않을 때 메모리 공간을 차지하지 않는다.
하지만 만약 ARC가 아직 사용 중인 인스턴스를 할당 해제하면 더 이상 해당 인스턴스의 프로퍼티에 액세스하거나 해당 인스턴스의 메서드를 호출할 수 없다. 실제로 인스턴스에 액세스하려고 하면 앱이 충돌할 가능성이 높다. 이를 해결하기 위해 3번의 기능이 필요하다.
ARC는 해당 인스턴스에 대한 활성 참조가 하나 이상 존재하는 한 인스턴스 할당을 취소하지 않는다.
이를 가능하게 하기 위해 클래스 인스턴스를 프로퍼티, 상수 또는 변수에 할당할 때마다 해당 프로퍼티, 상수 또는 변수가 인스턴스에 대한 강력한 참조를 만든다. 이 참조는 해당 인스턴스를 확고하게 유지하고 강력한 참조가 남아 있는 동안 할당 취소를 허용하지 않기 때문에 "강한" 참조라고 한다.
그것은 바로 할당 해제될 때 호출되는 deinitializer
를 작성해보는 것으로 명확한 실험이 가능하다.
class Person {
let name: String
var friend: Person?
init(name: String, friend: Person? = nil) {
self.name = name
self.friend = friend
}
deinit {
print("사람이 언제 죽는다고 생각하나?")
}
}
오랜만에 등장한 샘플 코드
let chopper = Person(name: "토니토니 쵸파", friend: Person(name: "닥터 히루루크"))
이렇게 chopper
라는 상수의 friend
프로퍼티에 히루루크를 할당한다면 히루루크 인스턴스에 대한 참조 카운트가 1일 것이다. 따로 변수나 상수에 할당돼있지 않고 오직 쵸파의 friend
프로퍼티만이 참조하고 있기 때문에 1이다.
chopper.friend = nil // "사람이 언제 죽는다고 생각하나?" 출력.
이 때 쵸파의 친구 프로퍼티를 nil
로 할당한다면 기존 히루루크 인스턴스는 더 이상 참조하는 곳이 없으므로 참조 카운트가 0이 되고 메모리에서 할당 해제된다.
그야말로 사람들에게서 잊혀지는 것이 곧 죽음인 것이다.
분명 ARC는 사용하고 있는 녀석을 할당 해제하지는 않는다. 다만 개발자의 의도와 다르게 메모리가 낭비되는 경우도 있다.
그것이 바로 강한 참조 사이클, 일명 순환 참조이다.
var chopper: Person? = Person(name: "토니토니 쵸파")
var hiluluk: Person? = Person(name: "닥터 히루루크")
chopper?.friend = hiluluk
hiluluk?.friend = chopper
아까와 비슷하지만 이번엔 히루루크를 따로 변수에 선언해서 friend
프로퍼티에 각각 서로를 할당해주자.
chopper = nil
hiluluk = nil
그러면 놀랍게도 아까 설정해둔 디이니셜라이저가 호출되지 않는다.
쵸파라는 변수와 히루루크라는 변수를 모두 nil
로 바꿨음에도 불구하고.
쵸파가 참조하고 있던 인스턴스의 friend
프로퍼티가 히루루크가 참조하고 있던 인스턴스를 참조하고, 그 인스턴스의 friend
프로퍼티는 쵸파가 참조하고 있던 인스턴스를 참조하고 있기 때문에 각각의 변수와의 연결고리는 끊어졌어도 서로가 서로를 강하게 참조하고 있기 때문에 메모리에 남아있는 것이다.
Apple의 공식 문서에서 나온 예시도 딱 그런 형태다.
서로의 프로퍼티가 서로의 인스턴스를 참조하는 사이클이 있어서 할당 해제되지 않는 것이다.
물론 이건 서로 다른 두 클래스 타입의 일이고, 우리가 확인한 예시는 한 타입이지만 사이클의 모양은 똑같다.
클래스 인스턴스의 프로퍼티에 클로저를 할당하고, 해당 클로저의 바디가 인스턴스를 캡처하는 경우에도 강한 순환 참조가 발생할 수 있다.
class Protagonist {
let name: String
let job: String
lazy var line: () -> String = {
return "제 이름은 \(self.name), \(self.job)이죠."
}
init(name: String, job: String) {
self.name = name
self.job = job
}
deinit {
print("\(name) 할당 해제")
}
}
이번에는 Protagonist
라는 클래스가 있다고 해보자. line
이라는 프로퍼티가 name
과 job
을 필요로 하기 때문에 지연 저장 프로퍼티로 선언되었다.
var conan: Protagonist?
conan = Protagonist(name: "코난", job: "탐정")
conan = nil // "코난 할당 해제" 출력.
deinit
에 설정한 대로 name
과 함께 정상적으로 문구가 출력되면서 할당 해제됨을 알 수 있다.
그렇지만 다음과 같이 바꿔보면 얘기가 달라진다.
var conan: Protagonist?
conan = Protagonist(name: "코난", job: "탐정")
if let conan {
print(conan.line()) // "제 이름은 코난, 탐정이죠." 출력.
}
conan = nil // 아무 일 없음.
즉 lazy var
인 line
이 활성화됨에 따라 클래스 인스턴스와 클로저 간의 순환 참조가 발생하게 되는 것이다.
이번에도 Apple의 공식 문서에서 나온 예시와 같은 형태다.
두 가지 경우(클래스&클래스, 클래스&클로저)의 강한 참조 사이클 케이스를 알게 되었는데, 이 문제를 해결하는 방법은 무엇일까?
아까 "강한" 참조에 대한 설명이 있었는데, 반대로 "약한" 참조 또한 존재한다.
A weak reference is a reference that doesn’t keep a strong hold on the instance it refers to, and so doesn’t stop ARC from disposing of the referenced instance. This behavior prevents the reference from becoming part of a strong reference cycle.
즉 ARC가 메모리에서 할당 해제하는 것을 막지 않는, 카운트를 올리지 않는 참조인 것이다.
프로퍼티에 사용하는 약한 참조의 경우 런타임 동안 언제든 할당 해제가 되면 nil
을 자동으로 할당해준다. 그러므로 변경의 여지가 있고, nil
일 수 있기 때문에 옵셔널 타입의 변수로 선언해야 한다.
class Person {
let name: String
weak var friend: Person?
init(name: String, friend: Person? = nil) {
self.name = name
self.friend = friend
}
deinit {
print("사람이 언제 죽는다고 생각하나?")
}
}
var chopper: Person? = Person(name: "토니토니 쵸파")
var hiluluk: Person? = Person(name: "닥터 히루루크")
chopper?.friend = hiluluk
hiluluk?.friend = chopper
아까와 같은 예시에서 friend
프로퍼티만 weak
로 변경해보았다.
chopper = nil // "사람이 언제 죽는다고 생각하나?" 출력.
hiluluk = nil // "사람이 언제 죽는다고 생각하나?" 출력.
할당이 해제됨을 알 수 있다.
그렇다면 클로저에서의 약한 참조는?
캡처에서 약한 참조임을 정의할 수 있다.
class Protagonist {
let name: String
let job: String
lazy var line: () -> String = { [weak self] in
guard let self else { return "" }
return "제 이름은 \(self.name), \(self.job)이죠."
}
init(name: String, job: String) {
self.name = name
self.job = job
}
deinit {
print("\(name) 할당 해제")
}
}
var conan: Protagonist?
conan = Protagonist(name: "코난", job: "탐정")
if let conan {
print(conan.line()) // "제 이름은 코난, 탐정이죠." 출력.
}
아까의 예시에서 클로저에 [weak self] in
을 추가하고, 클로저의 경우에도 약한 참조의 캡처는 옵셔널이기 때문에 옵셔널 바인딩을 사용했다.
conan = nil // "코난 할당 해제" 출력
마찬가지로 lazy var
인 line
이 활성화됐음에도 할당 해제가 됨을 알 수 있다.
약한 참조와 비슷한 역할의 소유하지 않은 참조라는 개념도 있다.
Like a weak reference, an unowned reference doesn’t keep a strong hold on the instance it refers to. Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.
약한 참조에서 언제든 해당 프로퍼티가 nil
이 될 수 있다는 것은 참조하는 쪽의 수명이 더 길다는 의미인데, 이 무소유 참조는 반대로 상대방의 수명이 더 길거나 혹은 같을 때 사용하는 방법이다.
그렇기 때문에 항상 해당 프로퍼티에 값이 있을 것이라고 생각하므로 옵셔널 타입이 아니다.
Use an unowned reference only when you are sure that the reference always refers to an instance that hasn’t been deallocated.
If you try to access the value of an unowned reference after that instance has been deallocated, you’ll get a runtime error.
그렇기 때문에 꼭 할당 해제되지 않으리란 확신이 있을 때만 이 무소유 참조를 쓰라고 강조한다.
물론 이 무소유 참조를 옵셔널로 사용하는 방법도 가능한데, 이 경우에는 약한 참조와 동일한 컨텍스트에서 사용할 수 있다.
The difference is that when you use an unowned optional reference, you’re responsible for making sure it always refers to a valid object or is set to nil.
약한 참조에서 weak
키워드를 사용한 것과 마찬가지로 프로퍼티에 적용할 때는 상수, 변수 선언 앞에 unowned
키워드를 붙여주고, 클로저에 적용할 때는 캡처 리스트에 정의한다.
class Protagonist {
let name: String
let job: String
lazy var line: () -> String = { [unowned self] in
return "제 이름은 \(self.name), \(self.job)이죠."
}
init(name: String, job: String) {
self.name = name
self.job = job
}
deinit {
print("\(name) 할당 해제")
}
}
var conan: Protagonist?
conan = Protagonist(name: "코난", job: "탐정")
if let conan {
print(conan.line()) // "제 이름은 코난, 탐정이죠." 출력.
}
conan = nil // "코난 할당 해제" 출력.
클로저에서 사용할 때도 마찬가지로 해당 클로저가 캡처할 인스턴스의 수명이상의 수명을 가질 때 무소유 참조를 할 것을 권장한다.
사실 순환 참조는 엑셀에서도 찾아볼 수 있을만큼 주변에서 쉽게 볼 수 있다.
또 흔히 말하는 닭이 먼저냐 달걀이 먼저냐 논쟁도 순환 참조가 아닐까?
달걀에서 태어난 닭이 낳은 달걀에서 태어난 닭이 낳은...