Swift - 탈출 클로저(Escaping Closure)

이재원·2024년 8월 19일
0

Swift

목록 보기
10/13
post-thumbnail

탈출 클로저(Escaping Closure)

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

weak 참조

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 참조

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 참조 선택 가이드

마지막으로, weakunowned 참조를 언제 사용해야 할지 간단히 정리해볼게요:

  • weak 참조: 객체가 해제될 가능성이 있는 경우 사용해요. 객체가 해제되면 자동으로 nil로 설정됩니다.
  • unowned 참조: 객체가 해제되지 않을 것이라고 확신할 수 있을 때 사용해요. nil이 될 가능성이 없어서 Optional 언래핑이 필요하지 않아요.

만약 weakunwoned 중에서 어떤 것을 사용될지 고민된다면, 일반적으로는 weak로 작성하는 것이 좋습니다. 메모리 관리에 있어 안정성을 제공하고 디버깅이 좀 더 쉽기 때문이죠.

내용 요약

  1. 탈출 클로저(Escaping Closure):
    탈출 클로저는 함수 종료 후에도 실행 가능한 클로저로, 비동기 작업에서 주로 사용되며 @escaping 키워드를 사용해 선언.
  2. 클로저의 self 캡처:
    클로저는 내부 변수나 객체를 캡처하며, 탈출 클로저는 self를 명시적으로 캡처해 강한 참조 사이클을 방지해야 함.
  3. 참조 사이클과 메모리 누수:
    클로저가 self를 강하게 참조하면 메모리 누수가 발생할 수 있으며, 이를 방지하기 위해 weak 또는 unowned 참조를 사용.
  4. weak 참조 사용 사례:
    델리게이트 패턴과 탈출 클로저에서 self가 해제될 가능성이 있을 때 weak 참조 사용.
    - 타이머와 같은 비동기 작업에서 weak 참조로 안전하게 메모리 관리.
  5. unowned 참조 사용 사례:
    생명주기가 일치하는 객체 관계나, 타이머와 같은 비동기 작업에서 self가 반드시 존재할 때 unowned 참조 사용.
  6. weakunowned 참조 선택 가이드:
    weak은 참조 대상이 해제될 가능성이 있을 때, unowned는 참조 대상이 해제되지 않을 때 사용.

출처
https://bbiguduk.gitbook.io/swift/language-guide-1/closures#escaping-closures

profile
20학번 새내기^^(였음..)

0개의 댓글