Swift DispatchQueue에서 [weak self]를 사용하지 않아도 되는 이유

작성일 :

Swift DispatchQueue에서 [weak self]를 사용하지 않아도 되는 이유

Swift에서 코드의 비동기 실행을 위해 DispatchQueue를 자주 사용하게 됩니다. 많은 개발자가 클로저를 사용할 때 [weak self]를 사용하는 습관이 있으며, 이는 메모리 누수를 방지하는 데 도움이 됩니다. 하지만 DispatchQueue를 사용할 때는 이 지정자가 항상 필요하지 않을 수 있습니다. 이 글에서는 그 이유와 안전하게 이를 사용하는 방법에 대해 설명합니다.

메모리 관리와 클로저

Swift에서는 객체가 메모리에서 해제되지 않는 메모리 누수를 방지하기 위해 ARC(Automatic Reference Counting)를 사용합니다. 클로저는 캡쳐 리스트를 통해 외부 변수에 대한 참조를 가질 수 있으며, 이로 인해 강한 참조 순환(strong reference cycle)이 발생할 수 있습니다. 이것이 바로 클로저 내에서 [weak self]를 사용해야 하는 주된 이유입니다.

강한 참조와 약한 참조

Swift의 weakunowned는 클로저 내부에서 외부 객체를 참조할 때 다양한 방식으로 객체를 참조할 수 있게 합니다. 강한 참조(strong reference)는 참조되는 동안 객체가 메모리에 유지되도록 합니다. 반면, 약한 참조(weak reference)는 객체가 해제되더라도 참조를 유지하지 않습니다.

DispatchQueue와 메모리 관리

DispatchQueue를 사용한 비동기 작업이 완료된 후에도 객체가 해제되기를 원할 때 [weak self]를 사용하는 것이 이상적으로 보일 수 있습니다. 그러나 대부분의 경우 이는 불필요하거나 오히려 코드의 가독성을 떨어뜨릴 수 있습니다.

주 큐 vs 비동기 큐

가장 일반적으로 사용하는 주 큐(Main Queue)에서는 클로저가 실행될 때 해당 객체가 이미 메모리에 남아있을 가능성이 높습니다. 예를 들어, 네트워크 요청 후 UI 업데이트를 메인 큐에서 실행하는 경우입니다. 또한, 비동기 큐에서는 클로저의 실행 시점에 객체가 해제되는 것이 흔하지 않습니다.

예제 코드 분석

다음은 [weak self]를 사용하는 경우와 사용하지 않는 경우의 비교입니다.

swift
class MyViewController: UIViewController {
    var data: [String] = []

    func fetchData() {
        DispatchQueue.global().async { [weak self] in
            guard let self = self else { return }
            // 네트워크 요청 또는 긴 작업 수행
            let fetchedData = ["Apple", "Banana", "Cherry"]

            DispatchQueue.main.async {
                self.data = fetchedData
                self.tableView.reloadData()
            }
        }
    }
}

위의 코드는 DispatchQueue에서 [weak self]를 사용한 예제입니다. 반면, 다음은 [weak self]를 사용하지 않은 경우입니다.

swift
class MyViewController: UIViewController {
    var data: [String] = []

    func fetchData() {
        DispatchQueue.global().async {
            // 네트워크 요청 또는 긴 작업 수행
            let fetchedData = ["Apple", "Banana", "Cherry"]

            DispatchQueue.main.async {
                self.data = fetchedData
                self.tableView.reloadData()
            }
        }
    }
}

위의 코드는 두 예제의 동작이 거의 유사함을 보입니다. 중요한 차이점은 [weak self]가 불필요하게 코드의 복잡성을 더할 수 있다는 것입니다. DispatchQueue.global().async 클로저가 실행될 때 객체가 메모리에서 해제되는 상황은 흔하지 않기 때문입니다.

안전한 사용을 위한 팁

애플의 가이드라인

애플의 공식 문서에서도 [weak self]의 사용이 항상 필요하지 않음을 언급하고 있으며, 필요할 때만 사용하는 것이 좋다고 조언합니다. 예를 들어, 주 큐에서 실행되는 클로저의 경우 [weak self]가 거의 필요하지 않습니다.

상황에 맞는 판단

코드의 컨텍스트를 잘 이해하고 상황에 맞게 판단하는 것이 중요합니다. 예를 들어, 장시간 실행되는 작업이 있고, 이 작업이 완료된 후 객체가 해제되더라도 문제가 없다면 [weak self]를 사용하여 강한 참조 순환을 방지할 수 있습니다.

swift
DispatchQueue.global().async { [weak self] in
    guard let self = self else { return }
    // 장시간 작업 수행

    DispatchQueue.main.async {
        self?.updateUI()
    }
}

이처럼 긴 작업이 있는 경우에는 [weak self]가 더 안전할 수 있습니다. 그러나 대부분의 간단한 작업에서는 이를 생략해도 무방합니다.

결론

Swift의 DispatchQueue를 사용할 때 [weak self]를 사용하는 것이 항상 필요한 것은 아닙니다. 코드를 작성할 때는 작업의 복잡성과 지속 시간을 고려하고, 필요할 때만 지정자를 사용하는 것이 좋습니다. 이를 통해 코드의 가독성을 유지하면서도 메모리 누수를 방지할 수 있습니다.