Swift - 클로저에서 캡처란 무엇일까?

이재원·2024년 9월 11일
0

Swift

목록 보기
12/13
post-thumbnail
post-custom-banner

1. 클로저의 기본 개념: 캡처란 무엇일까요?

Swift에서 클로저는 외부의 변수나 상수를 캡처해서 내부에서 사용할 수 있어요. 클로저는 변수들을 복사하거나 참조할 수 있고, Swift에서는 이 과정을 자동으로 처리해 줍니다. 값 타입참조 타입에 따라 클로저가 변수와 상호작용하는 방식도 달라집니다.

  • 값 타입(Value Type): 클로저 내부에서 해당 변수의 복사본을 사용합니다. Swift의 structenum은 값 타입이에요.
  • 참조 타입(Reference Type): 클로저는 변수의 메모리 주소를 참조해, 변수의 상태를 직접 변경할 수 있습니다. Swift의 class는 참조 타입입니다.

클로저 캡처 예시

func makeIncrementer(amount: Int) -> () -> Int {
    var total = 0
    let incrementer: () -> Int = {
        total += amount  // 클로저가 total과 amount를 캡처
        return total
    }
    return incrementer
}

let incrementByTen = makeIncrementer(amount: 10)
print(incrementByTen())  // 10
print(incrementByTen())  // 20

위 예시에서는 totalamount가 클로저에 의해 캡처됩니다. 클로저는 이 변수들을 내부에서 수정하거나 참조할 수 있게 해 주고요. amount상수로 고정된 값으로 캡처되지만, total은 호출될 때마다 계속 변경될 수 있습니다.

2. 클로저 캡처와 C++ 포인터의 비교

C++ 포인터와 클로저의 차이점

Swift에서 클로저가 변수를 캡처하는 방식은 C++에서 포인터로 변수를 참조하는 방식과 비슷합니다. 하지만 중요한 차이점이 있어요. Swift는 ARC(Automatic Reference Counting)라는 시스템을 통해 자동으로 메모리를 관리하는 반면, C++에서는 개발자가 직접 포인터로 메모리를 관리해야 합니다. Swift의 클로저는 외부 변수를 캡처할 때 변수의 메모리 주소를 저장하고, 이를 통해 클로저 내부에서 참조하거나 값을 변경할 수 있습니다.

  • C++ 포인터는 변수가 저장된 메모리 주소를 관리하여 직접 값을 변경할 수 있습니다.
  • Swift 클로저는 외부 변수를 참조할 수 있지만, ARC 덕분에 참조 카운트가 자동으로 관리됩니다. 즉, 개발자가 메모리 주소를 직접 다루지 않아도 되죠.

C++ 포인터 예시

#include <iostream>

void increment(int *value, int amount) {
    *value += amount;
}

int main() {
    int total = 0;
    increment(&total, 10);  // total의 주소를 넘겨서 값 변경
    std::cout << total << std::endl;  // 10

    increment(&total, 10);  // 다시 total 값을 10 증가
    std::cout << total << std::endl;  // 20
    return 0;
}

C++에서 포인터는 total 변수의 메모리 주소를 저장하고, 이를 통해 값을 직접 변경합니다. Swift의 클로저도 외부 변수를 캡처할 때 비슷하게 동작하지만, C++과 달리 Swift는 자동 참조 카운팅을 사용하여 메모리를 관리하므로 개발자가 직접 메모리 관리를 할 필요가 없습니다.

C++의 this 포인터와 Swift 클로저의 유사성

C++에서는 클래스의 멤버 함수가 호출될 때 this 포인터를 통해 객체의 주소를 참조합니다. Swift에서는 클로저가 클래스 인스턴스의 self를 자동으로 캡처하여 객체의 상태에 접근할 수 있어요. 다만 Swift에서는 클로저가 self를 캡처할 때 기본적으로 강한 참조(Strong Reference)를 사용하기 때문에 참조 순환 문제가 발생할 수 있습니다.

C++에서 this 포인터 예시

class MyClass {
public:
    int value;

    MyClass(int val) : value(val) {}

    void increment() {
        this->value += 1;  // this 포인터를 통해 value 값을 변경
    }
};

int main() {
    MyClass obj(5);
    obj.increment();
    std::cout << obj.value << std::endl;  // 출력: 6
    return 0;
}

Swift 클로저에서 self 참조 예시

class Counter {
    var count = 0

    func startCounting() {
        let closure = {
            self.count += 1  // 클로저가 self를 캡처
        }
        closure()
    }
}

let counter = Counter()
counter.startCounting()
print(counter.count)  // 출력: 1

Swift에서 클로저는 self를 캡처하여 객체의 상태를 참조할 수 있습니다. 다만, Swift에서는 클로저가 self를 기본적으로 강하게 참조하므로 참조 순환 문제가 발생할 수 있음을 주의해야 합니다.

3. ARC와 강한 참조 문제

ARC(Automatic Reference Counting)는 Swift에서 메모리를 관리하는 시스템으로, 객체가 더 이상 참조되지 않으면 자동으로 메모리에서 해제합니다. 하지만 클로저가 외부 변수를 강하게 참조할 경우, 참조 카운트가 남아 있어 메모리에서 해제되지 않고 남을 수 있습니다.

참조 카운트의 기본 동작

class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
}

var person1: Person? = Person(name: "John")  // 참조 카운트 = 1
var person2 = person1  // 참조 카운트 = 2

person1 = nil  // 참조 카운트 = 1
person2 = nil  // 참조 카운트가 0이 되어 메모리에서 해제

ARC는 참조 카운트가 0이 될 때 메모리에서 객체를 해제합니다. 하지만 클로저가 강하게 참조하면 서로가 객체의 메모리 주소에 대한 소유권을 갖고 있어 참조 카운트가 0이 되지 않기 때문에 메모리 해제가 일어나지 않습니다.

클로저와 강한 순환 참조 문제

클로저는 외부 변수를 강하게 참조할 수 있는데, 특히 self를 강하게 참조할 경우 강한 순환 참조가 발생하여 메모리에서 객체가 해제되지 않는 문제가 생길 수 있습니다. 이 경우 참조 카운트가 0이 되지 않으므로 객체가 계속 남아 있게 됩니다.

예제 코드를 보기에 앞서 참조가 무엇인지 정확히 알아보겠습니다.

참조한다는 것은 한 객체가 다른 객체의 메모리 주소를 알고 있어 해당 객체로 접근하거나 그 객체의 상태를 사용할 수 있는 관계를 말합니다.

즉, 서로 참조한다는 것은 객체 A가 객체 B를 참조하고, 이를 통해 객체 B에 접근할 수 있다는 뜻입니다. 동시에 객체 B도 객체 A를 참조하고 있어 서로를 참조하고 있는 상태입니다.

참조가 무엇인지 알았으니 이번에는 강한 참조 예시를 봐 보겠습니다.

강한 순환 참조 예시

class ViewController {
    var closure: (() -> Void)?

    func setupClosure() {
        closure = {
            print(self)  // 클로저가 self를 강하게 참조
        }
    }
}

var viewController: ViewController? = ViewController()
viewController?.setupClosure()

이 코드에서 ViewController가 클로저를 참조하고, 클로저가 다시 self를 참조하면서 강한 순환 참조가 발생합니다. Swift에서는 ARC(Automatic Reference Counting)가 객체의 참조 카운트를 관리하며, 참조 카운트가 0이 되어야 객체가 메모리에서 해제됩니다. 하지만 이 경우 참조 카운트가 0으로 줄어들지 않아 메모리에서 해제되지 않습니다.

여기까지 이해했다면 viewController를 nil로 강제로 지정해주면 어떻게 될지 궁금증이 생길 수 있습니다. 결과적으로는 강한 순환 참조가 발생하면 객체를 nil로 설정해도 참조 카운트가 0이 되지 않아 메모리에서 해제되지 않습니다. 클로저가 self를 계속 강하게 참조하고 있기 때문입니다.

class ViewController {
    var closure: (() -> Void)?

    func setupClosure() {
        closure = { print(self) }  // 클로저가 self를 강하게 참조
    }
}

var viewController: ViewController? = ViewController()
viewController?.setupClosure()

viewController = nil  // 하지만 ViewController는 여전히 메모리에서 해제되지 않음

약한 참조(weak self)와 unowned self로 강한 순환 참조 방지

약한 참조와 unowned self는 탈출 클로저를 설명할때 언급한 적이 있는데요. 좀 더 자세히 알아보도록 할게요.

이전에도 말했듯이 강한 순환 참조 문제를 해결하려면 클로저가 self약하게 참조(weak self)하도록 만들어야 합니다. 약한 참조는 참조는 하지만 객체를 소유하지 않고, 참조 카운트를 증가시키지 않습니다. 또한 unowned self는 약한 참조처럼 객체를 소유하지 않지만, 옵셔널이 아니라는 점에서 차이가 있습니다. 객체가 해제된 후에 unowned 참조를 사용하면 런타임 에러가 발생할 수 있습니다.

weak self 사용 예시

class ViewController {
    var closure: (() -> Void)?

    func setupClosure() {
        closure = { [weak self] in  // self를 약하게 참조
            print(self?.name ?? "No name")
        }
    }
}

var viewController: ViewController? = ViewController()
viewController?.setupClosure()

viewController = nil  // 이제 ViewController는 정상적으로 메모리에서 해제됨

unowned self 사용 예시

class ViewController {
    var closure: (() -> Void)?

    func setupClosure() {
        closure = { [unowned self] in  // self를 unowned로 참조
            print(self.name)  // self가 해제된 후 접근 시 런타임 에러 발생 가능
        }
    }
}

[weak self]를 사용하면 클로저가 self를 강하게 참조하지 않아 강한 순환 참조가 발생하지 않게 할 수 있습니다.

[unowned self]는 약한 참조와 유사하지만 옵셔널이 아니므로, 안전하게 사용하려면 객체의 생애 주기에 대해 확실히 알고 있어야 합니다.

마무리

Swift에서 클로저는 외부 변수를 쉽게 캡처할 수 있고, 강하게 참조할 경우 강한 순환 참조 문제가 발생할 수 있습니다. ARC는 객체의 참조 카운트를 관리하지만, 클로저가 self를 강하게 참조하면 메모리에서 해제되지 않는 문제가 발생할 수 있어요. 이를 방지하기 위해 약한 참조(weak self)unowned self를 사용해 메모리 누수를 예방할 수 있습니다.

요약

  1. 클로저의 캡처 기능:
    • Swift에서 클로저는 외부 변수를 캡처하여 내부에서 사용할 수 있습니다.
    • 클로저는 값 타입참조 타입에 따라 외부 변수를 다르게 다룹니다. 값 타입은 복사본을 사용하고, 참조 타입은 메모리 주소를 참조하여 변경할 수 있습니다.
  2. C++ 포인터와의 비교:
    • Swift의 클로저는 C++ 포인터와 유사하게 변수의 메모리 주소를 참조하지만, Swift는 ARC(Automatic Reference Counting)를 통해 메모리를 자동으로 관리합니다.
    • 개발자가 직접 메모리를 관리해야 하는 C++과 달리, Swift는 참조 카운트를 통해 메모리 관리를 자동화합니다.
  3. 강한 참조와 순환 참조:
    • 클로저가 self강하게 참조할 경우, 강한 순환 참조가 발생할 수 있습니다. 이로 인해 ARC가 참조 카운트를 0으로 만들지 못해 객체가 메모리에서 해제되지 않습니다.
  4. weak self와 unowned self:
    • weak self는 클로저가 self를 약하게 참조하여 순환 참조 문제를 해결합니다. 약한 참조는 참조 카운트를 증가시키지 않으며, 객체가 해제되면 nil이 됩니다.
    • unowned self는 옵셔널이 아니며, 참조하고 있는 객체가 클로저보다 더 오래 살아 있을 것이 확실할 때 사용됩니다. 객체가 해제된 후 unowned 참조에 접근하면 런타임 에러가 발생할 수 있으므로 주의가 필요합니다.
  5. 메모리 누수 방지:
    • Swift에서 클로저는 외부 변수를 쉽게 캡처하지만, 강한 순환 참조로 인한 메모리 누수를 방지하기 위해 weak selfunowned self를 적절히 사용해야 합니다.

출처
https://bbiguduk.gitbook.io/swift/language-guide-1/closures#capturing-values
https://bbiguduk.gitbook.io/swift/language-guide-1/automatic-reference-counting
https://yesiamnahee.tistory.com/164

profile
20학번 새내기^^(였음..)
post-custom-banner

0개의 댓글