[Swift] ARC 3편 - 클로저에 대한 Strong Reference Cycle

어흥·2024년 6월 17일

Swift

목록 보기
21/28

클로저 캡처 현상

캡처현상이란 클로저는 힙의 영역에 존재하고 변수에 할당 혹은 클로저를 호출하는 순간, 지속적으로 외부 변수를 사용해야 하기 때문에 외부 변수를 캡처하는 현상을 의미한다.

  • 클로저 레퍼런스 타입
func calculateFunc() -> ((Int) -> Int) {
    
    var sum = 0

    func square(num: Int) -> Int { 
        sum += (num * num)
        return sum
    }

    return square 
}

var squareFunc = calculateFunc()

squareFunc(10)    // 100
squareFunc(20)    // 500
squareFunc(30)    // 1400
  • sum이 캡처된 것을 확인 가능

캡처 현상을 해결하는 방법은 바로 캡처리스트

캡처 리스트

value type 인 경우

  • 캡처리스트를 사용하지 않을 때 변수의 주소를 참조한다. → value type인 경우 일반적으로 stack에 저장되므로 stack의 변수 주소값을 복사해서 내부에 저장해서 사용
  • 캡처리스트를 사용할 때 값을 복사해서 내부에 저장해서 사용

Reference type 케이스

  • 캡처리스트를 사용하지 않을 때 stack에 있는 변수의 주소를 복사해서 내부에 저장해서 사용 → 인스턴스를 접근하려면 stack에 있는 변수를 방문하고 해당 변수가 담고 있는 주소를 참조해서 heap에 방문하여 값에 접근할 수 있음
  • 캡처리스트를 사용할 때 직접적으로 heap에 있는 객체의 주소를 복사해서 내부에 저장해서 사용
  • 클로저는 왜 참조 타입일까? 클로저는 보통 다른 쓰레드에서 실행된다. 쓰레드는 각각 다른 stack 영역을 할당받으므로 stack에 저장되면 여러번 복사되어야 한다. 그러므로 heap에 저장되어 다른 stack 영역에서도 공유가 가능할 수 있도록 해야할 필요가 있는 것

클로저가 객체의 변수로 선언된 경우, 클로저가 실행중인 동안 오래동안 객체를 사용해야하므로, 힙 영역에 저장하고 사용할 객체의 주소를 보관

→ self와 클로저 서로 참조하는 형태

클로저에 대한 strong reference cycle

  • 초기화가 완료되고 self 가 존재할 때까지 접근할 수 없으므로 기본 클로저 내에서 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")
    }
}

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
  • 인스턴스의 asHTML 프로퍼티는 클로저에 대해 강한 참조를 유지
    • 클로저는 본문 내에서 self.nameself.text 를 참조하는 방법 처럼 self 를 참조하므로 self 캡처 → 클로저가 HTMLElement 인스턴스에 강한 참조 유지

paragraph = nil

따라서 위와 같이 paragraph에 nil이 할당되도 클로저가 인스턴스에 대한 참조를 유지하고 있으므로 메모리에서 해제되지 않는다.

클로저의 strong reference cycle 해결 방안

🔥 **캡처리스트** + **weak / unowned 사용**
lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

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

올바르게 메모리에서 해제된다.

0개의 댓글