Swfit

Closure의 capture list 사용법 제대로 알기!

GGShin 2025. 11. 14. 14:16

Closure에서 사용하는 capture list를 제대로 사용하려면, 먼저 Closure 블록 내에서 사용(캡쳐)하는 객체의 타입을 알아야 한다.

 

1. Value type capture
Value type은 값이 capture 되는 시점의 copy를 만든다.  아래 두 예시를 보면 쉽게 이해할 수 있다. 

첫번째 예시를 보면, queue로 보낸 실행 block이 1초 이후에 실행되고, 그 안에서 'count' 변수를 사용한다. 
해당 block이 실행되기 이전에 count가 10만큼 증가했으므로, block이 실행될 때는 이미 10 증가가 반영된 count 값이 print문에 찍히게 된다. 

 

// Value type
func test() {
  var count = 0
  DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
    print(count)
  }
  count += 10
}
// Prints 10


반면에, 아래 예시에서는 queue block에서 count 변수를 'capture' 해주었다. 이렇게 명시하면, 캡쳐가 되는 시점의 count의 copy가 생기게 된다. 그렇기 때문에 print로는 10 증가 되기 전의 값인 0이 찍히게 된다.

func test() {
  var count = 0
  DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [count] in
   print(count)
  }
  count += 10
}
// Prints 0


2. Reference type capture 

Reference type 일 때는 capture list에 담겨 있어도 copy를 만들지 않는다. 주소를 참조하는 방식이 capture 시에도 동일하게 적용된다.

// Ref type
class A {
    var name: String
    init(name: String) {
        self.name = name
    }
}

func testWithClass() {
    var a: A = A(name: "A")
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [a] in
        print(a.name)
    }
    a.name = "A_changed"
    print(a.name)
}
// Prints "A_changed"



Value type을 capture 할 때는 상관 없지만, reference type을 capture할 때는 조심해야 하는 것이 있는데, 바로 reference cycle이다. Value type에서는 copy를 만들기 때문에 크게 신경 쓸 것이 없지만, ref type에서는 closure 내에서 참조될 때 기본적으로 '강하게' 참조를 한다.

 

강하게 참조한다는 말은 closure의 작업이 끝날 때까지 closure가 붙들고 있는 class 객체가 메모리에서 해제되지 않는 다는 의미이다 (Reference count가 증가한 상태 유지). 이 사실이 크게 상관이 없는 때도 있지만, 상황에 따라 클로저와 참조되는 객체를 메모리에서 해제되지 않게 하는 reference cycle을 야기하는 구간이 되기도 한다. 

A strong reference cycle can also occur if you assign a closure to a property of a class instance, and the body of that closure captures the instance. (https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/#Strong-Reference-Cycles-for-Closures)

Apple 문서에 위와 같은 부분이 있는데 번역해 보면, 'Closure를 class 객체의 변수에 할당하고, 그 closure가 해당 class를 capture 하는 경우에 강한 참조가 발생한다.' 라고 되어있다. 아래와 같은 경우가 그에 해당하는 예시이다.

class ViewController {
    var onButtonTap: (() -> Void)?
    
    init() {
        onButtonTap = {
            self.handleTap()   // Reference cycle 발생
        }
    }
    
    func handleTap() {
        print("Button tapped")
    }
}

var vc: ViewController? = ViewController()
vc?.onButtonTap?()
vc = nil // Deinit 되지 않음.



이런 일이 발생하는 이유는 closure 자체도 reference type이기 때문이다. 참조 타입끼리 서로 참조를 하고 있으니, count가 내려가지 않게 되는 것이다. 

이를 방지하기 위해 사용해야 하는 것이 약한 참조이다. Capture list를 쓸 때, weak와 unowned로 표시해주면 약한 참조를 만들어 줄 수 있다. 

class ViewController {
    var onButtonTap: (() -> Void)?
    
    init() {
        onButtonTap = { [weak self] in
            self?.handleTap()
        }
    }
    
    func handleTap() {
        print("Button tapped")
    }
}

var vc: ViewController? = ViewController()
vc?.onButtonTap?()
vc = nil // Deinit 정상적으로 됨.



이렇게 weak로 self를 약하게 참조하도록 capture list로 명시해주면 두 객체가 강하게 참조하지 않기 때문에 reference cycle이 발생하지 않는다.

이 얘기는 반대로 생각해보면, 모든 closure에서 [weak self]를 사용할 필요는 없다는 것을 알려주기도 한다. 대표적으로 DispatchQueue에 작업을 넘겨줄 때를 생각해보자. 

DispatchQueue.main.async {
  self.draw()
}

 


이런 경우는 self를 그냥 호출하여도 무방한 것이, closure가 class 객체의 변수에 할당되지 않기 떄 때문에 애초에 reference cycle 문제가 생기지 않기 때문이다. 

위의 내용을 간단하게 정리하자면!

weak 혹은 unowned 사용 여부를 결정하기 위해서는

1. Closure 내부에서 참조하는 변수가 reference type인지 value type인지 구분하기
2. 참조하는 변수가 reference type이라면, closure가 class의 변수에 할당되는 지 확인하기


이렇게 두 가지를 잘 체크한다면, 순환 참조를 야기하지 않고 또 불필요한 weak self를 쓰는 경우도 없을 것이다. 


참고자료

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/#Strong-Reference-Cycles-for-Closures
https://medium.com/hcleedev/swift-closure%EC%99%80-capturing-fe8c9659a139
https://www.swiftbysundell.com/articles/swifts-closure-capturing-mechanics/

반응형