ARC 공식문서에서
Strong Reference Cycles for Closures
Resolving Strong Reference Cycles for Closures
를 다뤄보겠습니당
클로저의 순환참조와 해결..
스따뜨..!!!
약간의 의역이 포함될 예정이니 오역이나 업데이트가 필요한 부분은 꼭 댓글로 알려 주세요..! :)
Strong Reference Cycles for Closures (클로저의 강한 순환참조)
두 개의 클래스 인스턴스 프로퍼티가 서로를 강한 참조하고 있을 때 순환참조가 일어난다는 것을 위에서(지난 글 참고) 보셨을 겁니다. 당신은 이런 순환참조를 깨뜨리기 위해 약한(weak) 참조와 미소유(unowned) 참조를 사용하는 방법도 알고 있습니다.
순환참조는 클래스 인스턴스의 프로퍼티에 클로저를 할당할 때도 발생할 수 있고, 그 클로저의 바디는 그 인스턴스를 캡처합니다. 이 캡처는 self.someProperty처럼 클로저가 그 인스턴스의 프로퍼티에 접근하거나, 아니면 self.someMethod()처럼 클로저가 그 인스턴스의 메서드를 호출할 때 발생할 수 있습니다. 둘 중 어느 경우든, 이러한 접근은 클로저가 self를 "캡처"하도록 만들고, 강한 순환참조를 발생시킵니다.
이러한 순환참조는, 클로저가 클래스처럼 참조 타입이기 때문에 발생합니다. 당신이 클로저를 프로퍼티에 할당하면, 당신은 그 클로저에 대한 참조를 할당하는 것입니다. 본질적으로 위의 상황(지난 글 참고) 과 동일한 문제인 것입니다 — 두 개의 강한 참조가 서로를 살려 두고 있는 거죠. 하지만, 이번에는 서로를 살려 두는 게 두 개의 클래스 인스턴스가 아니라, 하나의 클래스 인스턴스와 하나의 클로저인 것이죠.
Swift는 이 문제에 대해 클로저 캡쳐 리스트라는 명쾌한 해결책을 제공합니다. 하지만 클로저 캡처 리스트를 통해 순환참조를 깨뜨리는 방법을 배우기 전에, 어떻게 그런 순환이 발생하는지를 이해하는 것이 도움이 됩니다.
아래의 예시는 self를 참조하는 클로저를 사용할 때 어떻게 순환 참조가 발생하는지를 보여 줍니다. 아래의 예시는 HTMLElement라는 클래스를 정의하는데, 이것은 한 HTML 도큐먼트 내의 각 요소를 위한 간단한 모델을 제공합니다.
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")
}
}
HTMLElement 클래스는 헤더의 "h1"이나 문단의 "p"나 라인 브레이크의 "br" 과 같은 요소의 이름을 나타내는 name 프로퍼티를 정의합니다. HTMLElement은 해당 HTML 요소 내에서 렌더링되는 텍스트를 나타내는 옵셔널 text 프로퍼티 또한 정의합니다.
이렇게 간단한 두 프로퍼티에 더해서, HTMLElement는 asHTML이라는 lazy 프로퍼티도 정의합니다. 이 프로퍼티는 name과 text를 HTML 문자열로 병합시키는 클로저를 참조합니다. asHTML 프로퍼티는 () -> String 타입, 혹은 "파라미터를 받지 않고 String 값을 리턴하는 함수"입니다.
기본적으로 asHTML 프로퍼티는 HTML 태그의 문자열 표시를 리턴하는 클로저를 할당받습니다. 이 태그는 text 값이 존재한다면 옵셔널 text 값을 포함하게 되고, text 값이 존재하지 않는다면 텍스트는 포함하지 않게 됩니다. 문단 요소에 대해서는, text 프로퍼티가 "some text"냐 nil이냐에 따라 "<p>some text</p>"나 혹은 "<p />"를 리턴하겠죠.
asHTML 프로퍼티는 이름이 있고 인스턴스 메서드처럼 사용되기도 합니다. 하지만 asHTML은 인스턴스 메서드가 아니라 클로저 프로퍼티이기 때문에, 특정 HTML 요소에 따라 HTML 렌더링을 바꾸고 싶다면 asHTML 프로퍼티의 초기값을 커스텀 클로저로 바꿀 수 있습니다.
예를 들어, text 프로퍼티가 nil일 때, 비어있는 HTML 태그가 리턴되는 것을 방지하기 위해서, asHTML 프로퍼티는 '초기값으로 특정 텍스트를 가지는 클로저'로 설정될 수 있습니다.
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// "<h1>some default text</h1>" 출력
주의
asHTML 프로퍼티는, 만약 요소가 실제로 특정 HTML 아웃풋 타깃에 맞게 스트링 값으로 렌더링 되어야 한다면 그 때에만 필요하기 때문에 lazy 프로퍼티로 선언되어 있습니다. asHTML이 lazy 프로퍼티라는 사실은 디폴트 클로저 내부에서 self를 참조할 수 있다는 것을 의미하는데, 이것은 lazy 프로퍼티는 초기화가 완료된 이후 self가 존재할 때까지 접근될 수 없기 때문입니다.
HTMLElement 클래스는 새로운 요소를 초기화하기 위해서, name과 text(필요하다면) 인자를 받는 이니셜라이저 하나만을 제공합니다. 이 클래스는 디이니셜라이저도 정의하는데, 이것은 HTMLElement 인스턴스가 메모리에서 해제될 때 보여 줄 메시지를 출력합니다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// "<p>hello, world</p>" 출력
주의
위의 paragraph는 아래에서 강한 순환참조의 존재를 설명하기 위해 nil로 설정될 수 있도록 옵셔널 HTMLElement로 정의되어 있습니다.
안타깝게도 HTMLElement 클래스는 위에 적혀 있는 것처럼 HTMLElement 인스턴스와 asHTML의 초기값에 사용된 클로저 사이에 강한 순환참조를 발생시킵니다. 순환참조의 형태는 아래와 같습니다:
인스턴스의 asHTML 프로퍼티는 클로저를 강한 참조합니다. 하지만 클로저가 바디에서 self를 참조하기 때문에(self.name과 self.text에 참조하기 위해), 클로저는 self를 캡처하게 되고, 이것은 클로저가 HTMLElement 인스턴스를 강한 참조한다는 것을 의미합니다. 그렇게 이 두 가지 사이에 강한 순환참조가 생성됩니다. (클로저 내부에서 값을 캡처하는 것에 대해 더 많은 정보를 확인하시려면 Capturing Values를 확인하세요.)
주의
클로저가 self를 여러번 참조하더라도, 클로저는 HTMLElement 인스턴스에 대한 강한 참조를 한 개만 캡처합니다.
만약 당신이 paragraph 변수를 nil로 설정해서 HTMLElement 인스턴스에 대한 강한 참조를 깨뜨리더라도, HTMLElement 인스턴스와 그 것의 클로저 둘 다 순환 참조 때문에 메모리에서 해제되지 않습니다:
paragraph = nil
HTMLElement 디이니셜라이저의 메시지가 출력되지 않는 것으로부터 HTMLElement 인스턴스가 메모리에서 해제되지 않음을 확인하세요.
'Swift > 공식문서' 카테고리의 다른 글
[Swift 공식문서 번역] ARC (Automatic Reference Counting) - 5. (마지막) (1) | 2024.10.30 |
---|---|
[Swift 공식문서 번역] ARC (Automatic Reference Counting) - 3. (0) | 2023.02.20 |
[Swift 공식문서 번역] ARC (Automatic Reference Counting) - 2. (0) | 2023.02.16 |
[Swift 공식문서 번역] ARC (Automatic Reference Counting) - 1. (0) | 2023.02.06 |