Swift에서 클로저는 외부의 변수나 상수를 캡처해서 내부에서 사용할 수 있어요. 클로저는 변수들을 복사하거나 참조할 수 있고, Swift에서는 이 과정을 자동으로 처리해 줍니다. 값 타입과 참조 타입에 따라 클로저가 변수와 상호작용하는 방식도 달라집니다.
struct
와 enum
은 값 타입이에요.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
위 예시에서는 total
과 amount
가 클로저에 의해 캡처됩니다. 클로저는 이 변수들을 내부에서 수정하거나 참조할 수 있게 해 주고요. amount
는 상수로 고정된 값으로 캡처되지만, total
은 호출될 때마다 계속 변경될 수 있습니다.
Swift에서 클로저가 변수를 캡처하는 방식은 C++에서 포인터로 변수를 참조하는 방식과 비슷합니다. 하지만 중요한 차이점이 있어요. Swift는 ARC(Automatic Reference Counting)라는 시스템을 통해 자동으로 메모리를 관리하는 반면, C++에서는 개발자가 직접 포인터로 메모리를 관리해야 합니다. Swift의 클로저는 외부 변수를 캡처할 때 변수의 메모리 주소를 저장하고, 이를 통해 클로저 내부에서 참조하거나 값을 변경할 수 있습니다.
#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에서는 클로저가 클래스 인스턴스의 self
를 자동으로 캡처하여 객체의 상태에 접근할 수 있어요. 다만 Swift에서는 클로저가 self
를 캡처할 때 기본적으로 강한 참조(Strong Reference)를 사용하기 때문에 참조 순환 문제가 발생할 수 있습니다.
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;
}
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
를 기본적으로 강하게 참조하므로 참조 순환 문제가 발생할 수 있음을 주의해야 합니다.
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는 여전히 메모리에서 해제되지 않음
약한 참조와 unowned self는 탈출 클로저를 설명할때 언급한 적이 있는데요. 좀 더 자세히 알아보도록 할게요.
이전에도 말했듯이 강한 순환 참조 문제를 해결하려면 클로저가 self
를 약하게 참조(weak self)하도록 만들어야 합니다. 약한 참조는 참조는 하지만 객체를 소유하지 않고, 참조 카운트를 증가시키지 않습니다. 또한 unowned self는 약한 참조처럼 객체를 소유하지 않지만, 옵셔널이 아니라는 점에서 차이가 있습니다. 객체가 해제된 후에 unowned 참조를 사용하면 런타임 에러가 발생할 수 있습니다.
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는 정상적으로 메모리에서 해제됨
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를 사용해 메모리 누수를 예방할 수 있습니다.
self
를 강하게 참조할 경우, 강한 순환 참조가 발생할 수 있습니다. 이로 인해 ARC가 참조 카운트를 0으로 만들지 못해 객체가 메모리에서 해제되지 않습니다.self
를 약하게 참조하여 순환 참조 문제를 해결합니다. 약한 참조는 참조 카운트를 증가시키지 않으며, 객체가 해제되면 nil
이 됩니다.unowned
참조에 접근하면 런타임 에러가 발생할 수 있으므로 주의가 필요합니다.출처
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