오늘은 탈출 클로저에 대해 알아보겠습니다.
탈출 클로저는 함수가 끝난 뒤에도 실행될 수 있는 클로저를 말해요.
원래 함수의 매개변수로 전달된 클로저는 함수가 종료되면 메모리에서 사라지는데, 탈출 클로저는 함수가 끝나도 메모리에 남아 있어서 외부에서 계속 사용할 수 있습니다.
이 특징 때문에 주로 비동기 작업에서 많이 사용되고, @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
참조를 사용하는 것이 안전합니다.
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이 될 가능성을 대비하고 있어요. 이를 통해 메모리 누수를 예방할 수 있죠.
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