🔥 클로저로 인한 강한 참조 순환
클로저를 사용할 때 강한 참조 순환이 발생할 수 있다는 점을 앞서 살펴봤어요. 이번에는 클로저로 인해 강한 참조 순환이 발생하는 상황과 이를 해결하는 방법에 대해 자세히 알아보도록 할게요.
클로저를 클래스 인스턴스의 속성에 할당하고, 해당 클로저의 본문에서 인스턴스를 캡처하면 강한 참조 순환이 발생할 수 있어요. 이러한 캡처는 클로저의 본문에서 self.someProperty
와 같이 인스턴스의 속성에 접근하거나, self.someMethod()
와 같이 인스턴스의 메서드를 호출할 때 발생하죠. 이 경우 클로저는 self
를 캡처하여 강한 참조 순환을 만들게 된답니다.
강한 참조 순환이 발생하는 이유는 클로저가 클래스와 마찬가지로 참조 타입
이기 때문이에요. 클로저를 속성에 할당할 때, 실제로는 해당 클로저에 대한 참조
를 할당하는 거예요. 이는 앞서 살펴본 문제와 본질적으로 동일한데, 두 개의 강한 참조가 서로를 살아있게 만드는 거죠. 다만 이번에는 두 개의 클래스 인스턴스 대신, 클래스 인스턴스와 클로저가 서로를 살아있게 만드는 상황이랍니다.
Swift는 이 문제를 해결하기 위해 클로저 캡처 리스트
라는 우아한 방법을 제공해요. 하지만 클로저 캡처 리스트로 강한 참조 순환을 해결하는 방법을 배우기 전에, 어떻게 이런 순환이 발생하는지 이해하는 것이 도움될 거예요.
다음 예제는 self
를 참조하는 클로저를 사용할 때 강한 참조 순환이 발생하는 방식을 보여준답니다. 이 예제는 HTML 문서 내의 개별 요소에 대한 간단한 모델을 제공하는 HTMLElement
라는 클래스를 정의하고 있어요:
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") } }
swift
HTMLElement
클래스는 요소의 이름을 나타내는 name
속성과 해당 HTML 요소 내에 렌더링할 텍스트를 나타내는 문자열로 설정할 수 있는 옵셔널 text
속성을 정의하고 있죠.
이 두 가지 간단한 속성 외에도, HTMLElement
클래스는 asHTML
이라는 지연 속성을 정의하고 있어요. 이 속성은 name
과 text
를 HTML 문자열 조각으로 결합하는 클로저를 참조하고 있답니다. asHTML
속성은 () -> String
타입, 즉 "매개변수를 받지 않고 String
값을 반환하는 함수" 타입이에요.
기본적으로 asHTML
속성에는 HTML 태그의 문자열 표현을 반환하는 클로저가 할당돼요. 이 태그는 text
값이 존재하면 해당 값을 포함하고, text
가 존재하지 않으면 텍스트 내용이 없어요. 문단 요소의 경우, text
속성이 "some text"
와 같거나 nil
이냐에 따라 클로저는 "<p>some text</p>"
또는 "<p />"
를 반환하게 되죠.
asHTML
속성은 인스턴스 메서드처럼 명명되고 사용되지만, 사실 asHTML
은 인스턴스 메서드가 아닌 클로저 속성랍니다. 그래서 특정 HTML 요소에 대한 HTML 렌더링을 변경하고 싶다면 asHTML
속성의 기본값을 사용자 정의 클로저로 대체할 수 있어요.
예를 들어, text
속성이 nil
인 경우 기본 텍스트를 사용하도록 asHTML
속성을 설정하여 빈 HTML 태그가 반환되는 것을 방지할 수 있답니다:
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>" 출력
swift
HTMLElement
클래스는 새 요소를 초기화하기 위해 name
인자와 (원하는 경우) text
인자를 받는 단일 이니셜라이저를 제공해요. 또한 이 클래스는 HTMLElement
인스턴스가 할당 해제될 때 메시지를 출력하는 디이니셜라이저를 정의하고 있죠.
다음은 HTMLElement
클래스를 사용하여 새 인스턴스를 생성하고 출력하는 방법이에요:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") print(paragraph!.asHTML()) // "<p>hello, world</p>" 출력
swift
강한 참조 순환 발생
그런데 위에서 작성한 HTMLElement
클래스는 HTMLElement
인스턴스와 기본 asHTML
값에 사용되는 클로저 사이에 강한 참조 순환을 만들고 있어요. 이 순환은 다음과 같이 보인답니다:
인스턴스의 asHTML
속성은 클로저에 대한 강한 참조를 가지고 있죠. 하지만 클로저의 본문에서 self.name
과 self.text
를 참조하는 방식으로 self
를 참조하기 때문에, 클로저는 self
를 캡처하여 HTMLElement
인스턴스에 대한 강한 참조를 다시 가지게 된답니다. 이로 인해 둘 사이에 강한 참조 순환이 생기는 거예요. (클로저에서 값을 캡처하는 것에 대한 자세한 내용은 Capturing Values를 참고해 보세요.)
paragraph
변수를 nil
로 설정하여 HTMLElement
인스턴스에 대한 강한 참조를 끊더라도, 강한 참조 순환으로 인해 HTMLElement
인스턴스와 클로저가 모두 할당 해제되지 않아요:
paragraph = nil // "<p>hello, world</p>" 출력
swift
HTMLElement
디이니셜라이저의 메시지가 출력되지 않는 것을 확인할 수 있는데, 이는 HTMLElement
인스턴스가 할당 해제되지 않았음을 보여주죠.
클로저 캡처 리스트로 강한 참조 순환 해결하기
강한 참조 순환이 존재하는 경우에도 메모리 관리를 편리하게 하고자 한다면 클로저 캡처 리스트(closure capture list)
를 사용할 수 있어요. 캡처 리스트는 클로저 내에서 하나 이상의 참조 타입을 약한(weak)
참조나 미소유(unowned)
참조로 선언하여 강한 참조 순환을 방지하는 기능이랍니다.
약한
참조는 참조하고 있는 인스턴스가 먼저 해제되어 nil
이 될 수 있어요. 따라서 약한 참조로 선언된 참조는 항상 옵셔널 타입이어야 해요.
반면에 미소유
참조는 참조하고 있는 인스턴스의 생명주기를 클로저와 동일하게 보장할 때 사용해요. 즉, 참조하는 인스턴스가 클로저보다 먼저 해제되지 않을 것임을 확신할 때 미소유 참조를 사용할 수 있죠.
클로저의 캡처 리스트는 클로저의 매개변수 목록 전, in
키워드 앞에 대괄호([]
)로 작성해요. 예를 들면 다음과 같아요:
lazy var someClosure = { [unowned self, weak delegate = self.delegate] in // 클로저 본문 }
swift
이 예제의 경우 unowned self
와 weak delegate = self.delegate
로 캡처 리스트를 정의했어요. 약한(weak)
참조로 캡처할 때는 등호(=
)를 사용하여 약한 참조의 초기값을 지정해야 한다는 점 잊지 마세요!
클로저 캡처 리스트 적용 예제
앞선 HTMLElement
예제에서 발생한 강한 참조 순환을 클로저 캡처 리스트를 사용하여 해결해 볼게요. HTMLElement
클래스의 asHTML
속성을 다음과 같이 정의해요:
lazy var asHTML: () -> String = { [unowned self] in if let text = self.text { return "<\(self.name)>\(text)</\(self.name)>" } else { return "<\(self.name) />" } }
swift
이번에는 [unowned self]
캡처 리스트를 사용하여 self
를 미소유 참조로 캡처했어요. asHTML
클로저와 HTMLElement
인스턴스의 생명주기가 동일하다는 것을 보장할 수 있기 때문에 미소유 참조를 사용할 수 있죠. HTMLElement
가 해제되면 asHTML
클로저 역시 해제될 거예요.
이제 HTMLElement
인스턴스를 생성하고 asHTML
속성을 사용하는 코드를 실행해 볼게요. 이전과 마찬가지로 <p>hello, world</p>
가 출력될 거예요.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") print(paragraph!.asHTML()) // "<p>hello, world</p>" 출력
swift
하지만 이번에는 paragraph
변수를 nil
로 설정한 후에도 HTMLElement
인스턴스와 asHTML
클로저가 제대로 할당 해제된답니다.
paragraph = nil // "p is being deinitialized" 출력
swift
디이니셜라이저의 메시지가 출력되는 것을 확인할 수 있죠? 클로저 캡처 리스트를 사용하여 강한 참조 순환을 끊었기 때문에 HTMLElement
인스턴스가 정상적으로 할당 해제되었어요.
이렇게 Swift의 클로저 캡처 리스트
를 사용하면 클로저로 인한 강한 참조 순환을 우아하게 해결할 수 있습니다.