티스토리 뷰

Swift

[Swift] 클로저 강한 순환 참조

Hani_Levenshtein 2021. 5. 19. 14:23

안녕하세요 Hani입니다.

지난번 클래스의 강한 참조 순환 포스팅 에서는 weak unowned 키워드를 통해 문제를 해결했죠?

 

이번 주제는 대망의 클로저의 강한 참조 순환입니다.

제가 최근 ARC에 대해 공부를 시작하게 된 계기..!  [weak 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는 전달 인자를 받지 않고 출력 값이 String인 () -> String 클로저를 사용하고 있습니다.

클로저 내부에서는 HTMLElement의 name과 text를 얻어내기 위해 self를 캡쳐하고 있네요.

 

 

그럼 클래스의 인스턴스를 생성해보겠습니다.

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

 

paragraph 변수는 HTMLElement(name: "p", text: "hello, world") 인스턴스를 참조하고 있죠?

paragraph 변수가 참조하는 인스턴스의 asHTML는 () -> String 클로저를 참조하고 있고,

 

 

그림으로 나타내면 아래와 같은 모습입니다.

 

여기서 paragraph 변수의 인스턴스 참조를 끊어버리면 어떻게 될까요?

paragraph = nil

 

paragraph 변수가 인스턴스를 참조하는 것을 끊어도 인스턴스가 메모리에서 사라지지 않습니다.

왜냐면 인스턴스와 클로저간 강한 순환 참조가 발생하기 때문이에요.

소멸자 역시 실행되지 않은 모습을 볼 수 있습니다.

 

 

클로저의 강한 순환 참조 문제의 해결 방안은 캡쳐 리스트(Capture List)입니다.

캡쳐 리스트는 캡쳐할 대상 앞에 참조 타입을 적어서 정의할 수 있어요.

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")
    }

}

클로저의 참조 타입은 weak, unowned가 있어요. (디폴트는 역시 strong)

아까랑 달리 [unowned self] 가 들어갔죠?

self, 그러니까 HTMLElement클래스의 인스턴스를 앞으로는 미소유 참조하겠다는 얘기입니다.

 

 

이대로 인스턴스를 생성해볼게요.

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

약한 참조와 미소유 참조는 참조 횟수를 증가시키지 않죠?

인스턴스와 클로저의 참조 횟수는 각각 1인 상태입니다.

 

이제 paragraph 변수의 인스턴스 참조를 끊어보겠습니다.

 

paragraph = nil
// Prints "p is being deinitialized"

HTMLElement 인스턴스는 참조 횟수가 0으로 되니까 인스턴스의 클로저 참조도 끊어지게 됩니다.

결국 참조 횟수가 0이 된 클로저도 인스턴스와 같은 타이밍에 메모리에서 사라지게 되죠.

 

이 경우 클로저와 인스턴스의 수명이 같기 때문에 unowned를 사용해도 무방해 보입니다.

 

 

그럼 weak self는?

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        [weak 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")
    }

}

weak은 참조할 대상이 nil일 가능성을 포함하니까 참조 대상에 옵셔널을 붙여줍니다.

 

 

클로저에서 왜 weak self를 쓰는지 알기 위해 많은 시간이 걸렸지만 (무려 포스팅 3개.. 🔥)

건진 게 많아서 기분은 좋네요 ☺️


References

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

댓글