오늘은 탈출 클로저에 대해 알아보겠습니다.
탈출 클로저는 함수가 끝난 뒤에도 실행될 수 있는 클로저를 말해요.
원래 함수의 매개변수로 전달된 클로저는 함수가 종료되면 메모리에서 사라지는데, 탈출 클로저는 함수가 끝나도 메모리에 남아 있어서 외부에서 계속 사용할 수 있습니다.
이 특징 때문에 주로 비동기 작업에서 많이 사용되고, @escaping
키워드를 사용해 선언합니다.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
여기서 보시면, 탈출 클로저가 completionHandlers
배열에 추가되었어요. 이 덕분에 함수가 종료된 후에도 클로저가 실행될 수 있는 거죠.
만약 @escaping
키워드를 사용하지 않으면 컴파일 에러가 나겠죠?
self
캡처클로저는 함수나 메서드 내부의 변수나 객체를 캡처해서 사용할 수 있어요.
예를 들어, 클로저가 self
를 캡처하면 그 객체의 상태를 유지하거나 변경할 수 있죠. 그런데 탈출 클로저는 함수 실행이 끝난 후에도 계속 살아있기 때문에, self
를 명시적으로 캡처해줘야 해요. 이렇게 해야 강한 참조 사이클, 즉 메모리 누수를 방지할 수 있습니다.
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
위 예제를 보면, 비탈출 클로저는 함수 내부에서만 실행되기 때문에 self
를 굳이 명시하지 않아도 됩니다. 하지만 탈출 클로저는 self
를 명시적으로 캡처해야 해요. 이렇게 해야 강한 참조 사이클을 피할 수 있답니다.
여기서 중요한 개념 하나가 더 나옵니다. 클로저가 self
를 강하게 잡고 있으면, 강한 참조 사이클이 발생할 수 있고, 이로 인해 메모리 누수가 생길 수 있어요. 그래서 이 문제를 해결하려면 self
를 약한 참조(weak
)나 비소유 참조(unowned
)로 선언해야 합니다.
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { [weak self] in
guard let self = self else { return }
self.x = 100
}
}
}
여기서는 weak
참조를 사용했어요. 이렇게 하면 클로저가 self
를 참조해도 강한 참조가 생기지 않아서 참조 사이클을 막을 수 있어요. weak
참조를 사용할 때는 self
가 해제될 가능성을 염두에 두고, 클로저 내부에서 self
가 nil이 아닌지 확인해주는 게 중요해요.
반면에 unowned
참조는 클로저가 실행될 때 self
가 반드시 존재할 것이라고 확신할 수 있을 때 사용해요. unowned
는 Optional이 아니라 성능이 조금 더 좋지만, 만약 객체가 해제된 후에 unowned
참조에 접근하면 런타임 에러가 발생할 수 있다는 점, 꼭 기억하세요.
weak
참조는 주로 객체가 해제될 가능성이 있을 때 사용해요. 예를 들어, 델리게이트 패턴에서는 주 객체와 델리게이트 객체 간의 강한 참조 사이클을 피하기 위해 weak
참조를 많이 사용해요.
protocol MyDelegate: AnyObject {
func didSomething()
}
class MyClass {
weak var delegate: MyDelegate?
func performAction() {
delegate?.didSomething()
}
}
class ViewController: UIViewController, MyDelegate {
let myClass = MyClass()
override func viewDidLoad() {
super.viewDidLoad()
myClass.delegate = self
}
func didSomething() {
print("Delegate method called")
}
}
이처럼 델리게이트 패턴에서는 weak
참조가 필수적이에요. 델리게이트 객체가 해제되더라도 주 객체가 이를 강하게 참조하지 않도록 하기 위해서죠.
또한, 비동기 작업에서 self
가 해제될 가능성이 있다면 weak
참조를 사용하는 것이 안전합니다.
좀 더 쉽게 이해하기 위해서 팀장(ViewController)이 비서(MyClass)를 고용하는 상황에 비유해 보겠습니다.
팀장은 비서를 강하게 소유하지만, 비서는 팀장을 약하게 기억합니다(weak).
그래서 팀장이 퇴사(ViewController 해제)하면 비서는 자연스럽게 팀장에 대한 기억(delegate)을 잃고(nil), 메모리 누수 없이 정리됩니다.
만약 비서도 팀장을 강하게 붙잡고 있었다면, 둘 다 서로를 놓지 못해 회사를 영영 떠나지 못하는 상황(순환 참조)이 벌어질 것입니다.
class SomeClass {
var x = 10
func performOperation() {
someFunctionWithEscapingClosure { [weak self] in
self?.x = 100
}
}
}
마찬가지로, 타이머와 같은 비동기 작업에서도 weak
참조를 활용해 메모리 관리가 필요합니다.
class TimerClass {
var timer: Timer?
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.doSomething()
}
}
func doSomething() {
print("Timer fired!")
}
deinit {
timer?.invalidate()
print("TimerClass is being deinitialized")
}
}
var timerInstance: TimerClass? = TimerClass()
timerInstance?.startTimer()
// 나중에 timerInstance를 nil로 설정하면 TimerClass가 해제됩니다.
timerInstance = nil
이 예제에서는 weak
참조를 사용해 self
가 nil이 될 가능성을 대비하고 있어요. 이를 통해 메모리 누수를 예방할 수 있죠.
캡처 클로저 역시 비유해서 설명해 보겠습니다.
SomeClass는 어떤 작업을 외주(탈출 클로저)에 맡기고, 클로저는 작업이 끝난 뒤 SomeClass에게 보고를 하기로 합니다.
하지만 서로가 서로를 강하게 붙잡고 있다면, 계약이 끝나도 집에 가지 못하고 함께 메모리에 남게 되죠. (순환 참조)
그래서 SomeClass는 클로저에게 이렇게 말합니다. “보고는 하되, 나를 붙잡지 말아줘.”
이게 바로 [weak self]로 캡처하는 이유입니다.
unowned
참조는 객체가 해제되지 않을 것이 확실할 때 사용해요. 예를 들어, 객체와 클로저의 생명주기가 동일하거나, self
가 반드시 존재한다고 확신할 수 있을 때 적합합니다.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
}
class CreditCard {
let number: String
unowned let customer: Customer
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
}
let john = Customer(name: "John Appleseed")
let card = CreditCard(number: "1234-5678-9012-3456", customer: john)
john.card = card
이 예제에서는 Customer와 CreditCard가 서로의 생명주기를 공유하므로 unowned
참조를 사용하는 것이 적절합니다.
또한, 타이머와 같은 비동기 작업에서도 self
가 반드시 존재한다고 확신할 수 있는 경우라면 unowned
참조를 사용할 수 있어요.
class TimerClass {
var timer: Timer?
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [unowned self] _ in
self.doSomething()
}
}
func doSomething() {
print("Timer fired!")
}
deinit {
timer?.invalidate()
print("TimerClass is being deinitialized")
}
}
var timerInstance: TimerClass? = TimerClass()
timerInstance?.startTimer()
// timerInstance를 nil로 설정하면 TimerClass가 해제됩니다.
timerInstance = nil
여기서는 unowned
참조를 사용했어요. self
가 클로저 실행 중에 해제될 가능성이 전혀 없다면, unowned
를 사용하는 것이 성능 면에서 더 효율적입니다.
마지막으로, weak
와 unowned
참조를 언제 사용해야 할지 간단히 정리해볼게요:
weak
참조: 객체가 해제될 가능성이 있는 경우 사용해요. 객체가 해제되면 자동으로 nil로 설정됩니다.unowned
참조: 객체가 해제되지 않을 것이라고 확신할 수 있을 때 사용해요. nil이 될 가능성이 없어서 Optional 언래핑이 필요하지 않아요.만약 weak
와 unwoned
중에서 어떤 것을 사용될지 고민된다면, 일반적으로는 weak
로 작성하는 것이 좋습니다. 메모리 관리에 있어 안정성을 제공하고 디버깅이 좀 더 쉽기 때문이죠.
@escaping
키워드를 사용해 선언.self
캡처:self
를 명시적으로 캡처해 강한 참조 사이클을 방지해야 함.self
를 강하게 참조하면 메모리 누수가 발생할 수 있으며, 이를 방지하기 위해 weak
또는 unowned
참조를 사용.weak
참조 사용 사례:self
가 해제될 가능성이 있을 때 weak
참조 사용.weak
참조로 안전하게 메모리 관리.unowned
참조 사용 사례:self
가 반드시 존재할 때 unowned
참조 사용.weak
와 unowned
참조 선택 가이드:weak
은 참조 대상이 해제될 가능성이 있을 때, unowned
는 참조 대상이 해제되지 않을 때 사용.출처
https://bbiguduk.gitbook.io/swift/language-guide-1/closures#escaping-closures