클로저 캡쳐란? (Closure Capture)

2020. 7. 25. 16:04iOS

Swift코드를 작성하다 보면 [unowned self][weak self]를 사용할 때가 많습니다.
unowned와 weak는 강한 순환 참조(Strong Reference Cycle)을 방지할 때 주로 사용하는 키워드인데 클로저에서는 어떤 포인트에서 강한 순환 참조가 일어나는지 항상 궁금했습니다.

매번 미루다가 이제야 알아보게 되었네요...ㅎㅎ
자! 이제 클로저 안에서 강한 순환 참조가 왜 일어나는지 알아보겠습니다. 그 전에 먼저 알아야할 개념이 있습니다. 바로 클로저 캡처입니다.

클로저 캡처란?

클로저는 정의된 context내에 있는 상수나 변수에 대한 참조를 캡처(Capturing by reference)하고 저장할 수 있습니다. 캡처를 하면 원본값이 사라져도 클로저 body 안에서는 그 값을 참조하고 수정도 할 수 있습니다.
코드예시는 Swift 공식 문서에서 확인할 수 있습니다.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer()내부에는 nested function인 incrementer()이 정의되어 있고, incrementer을 클로저로 반환합니다. incrementer()runningTotalamount 변수를 캡쳐하고 있습니다. 따라서 makeIncrementer의 호출이 끝이 나도 incrementer안에 있는 변수들은 계속 남아있습니다.

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

위의 예시에서 incrementByTenmakeIncrementer()로 반환된 클로저를 저장하고 있습니다. makeIncrementer이 호출되고 반환된 상태이면 그 안에 있는 변수 runningTotalamount도 사라져야하지만 incrementByTen을 호출하면 계속 살아있습니다. 클로저가 그 변수 둘을 캡처하고 있어서 클로저를 호출할 때마다 값이 계속 update됩니다.

 

캡쳐하는 값이 Reference Type이라면?

클로저가 캡처하고 있는 값이 value type이라면 문제가 없겠지만, reference type이라면 이야기가 달라집니다. 바로 강한 순환 참조(Strong Reference Cycle)를 발생시키기 때문입니다.

예를 들어, 클로저가 어떤 클래스의 프로퍼티로 할당되고, 클로저 본문에서 클래스의 변수에 접근하기 위해 self를 사용한다면 클로저는 클래스의 인스터스에 대한 강한 참조가 생깁니다.

 

클로저에서 강한 순환 참조

class ClosureTest {
    var id: Int

    lazy var closure: () -> Int  = {
        self.id += 1
        return self.id
    }

    init(id: Int) {
        self.id = id
    }

    deinit {
        print("ClosureTest is deallocated")
    }
}

var closureTest: ClosureTest? = ClosureTest(id: 1)
print(closureTest!.closure())
closureTest = nil
/// prints "2"

위의 예시가 대표적인 강한 순환 참조 코드예시입니다. 코드 출력 결과를 보면 ClosureTest 인스턴스가 메모리 해제가 되지 않고 있습니다.. ClouserTest 인스턴스와 클로저간의 강한 순환 참조로 인하여 생긴 결과인데 그림과 함께 보면 설명이 더 쉬울 것 같습니다.

그림을 보면 ClosureTest 인스턴스와 클로저가 서로 강한 참조를 하고 있습니다. 이는 클로저도 참조 타입이기 때문에 ClosureTest의 프로퍼티인 closure변수는 클로저에 대한 강한 참조를 가집니다. 클로저 또한 self접근을 통해 ClosureTest의 인스턴스를 캡처함으로써 강한 참조를 가집니다. 이로 인해 ClosureTest 인스턴스와 클로저는 서로 강한 참조를 하고 있으므로 강한 순환 참조가 발생합니다.

이로 인해 closureTest변수에 nil을 대입해도 강한 순환 참조가 남아있으므로 메모리 누수가 발생합니다. 이 문제는 캡처리스트를 사용하여 해결할 수 있는데, 바로 우리가 흔히 쓰는 [unowned self], [weak self]가 그 예시입니다. 캡처리스트에 대한 포스팅은 다음에 다뤄보겠습니다ㅎㅎ

참고

https://docs.swift.org/swift-book/LanguageGuide/Closures.html#

'iOS' 카테고리의 다른 글

unowned self vs weak self (캡쳐 리스트)-(1)  (1) 2021.01.11
프로퍼티 래퍼란? (Property Wrapper)  (0) 2020.12.09