티스토리 뷰
클로저(Closure)를 쓰다 보면 가끔 예상치 못한 메모리 문제를 만난다.
"왜 self가 필요하지?"
"이 변수 값이 왜 바뀌지 않지?"
"메모리 해제가 안 되는 이유가 뭘까?"
이런 의문이 들었다면, 바로 "캡처 리스트(Capture List)"를 이해해야 할 때다.
오늘은 Swift에서 클로저가 변수를 어떻게 캡처하는지, 캡처 리스트가 없으면 어떤 문제가 생기는지, 실무에서는 어떻게 활용해야 하는지 알아보자
클로저는 변수를 어떻게 기억할까?
먼저 클로저(Closure)가 변수를 캡처(Capture)한다는 개념부터 이해해보자.
func makeIncrementer() -> () -> Int {
var total = 0
let increment: () -> Int = {
total += 1
return total
}
return increment
}
let counter = makeIncrementer()
print(counter()) // 1
print(counter()) // 2
print(counter()) // 3
클로저가 total 변수를 계속 기억하고 있다!
- total은 makeIncrementer() 함수가 끝나면 원래는 사라져야 한다.
- 하지만 클로저가 total을 캡처하고 있어서 계속 값을 유지할 수 있다.
- 즉, 클로저는 변수를 "포획(Capture)"해서 메모리에 유지한다.
이제 여기서 문제가 생길 수도 있다.
"클로저가 객체를 캡처하면, 메모리 해제는 어떻게 될까?"
클로저의 강한 참조 문제 – 메모리 누수(Leak) 위험
클로저가 객체를 캡처하면, "강한 참조 순환(Strong Reference Cycle)"이 발생할 수 있다.
class Person {
let name: String
var greeting: (() -> Void)?
init(name: String) {
self.name = name
}
func sayHello() {
greeting = {
print("안녕하세요, 저는 \(self.name)입니다.")
}
}
deinit {
print("\(name) 객체 해제됨")
}
}
var person: Person? = Person(name: "Alice")
person?.sayHello()
person = nil // ❌ Person이 메모리에서 해제되지 않음!
왜 person = nil을 해도 객체가 해제되지 않을까?
- sayHello()에서 클로저가 self.name을 캡처했다.
- 클로저는 greeting 프로퍼티에 저장되어 있고,
- greeting은 self(Person)를 강하게 참조하고 있다.
- 즉, Person이 클로저를 참조하고, 클로저가 다시 Person을 참조하는 순환 참조 발생!
이제 이 문제를 해결해야 한다.
여기서 등장하는 게 바로 캡처 리스트(Capture List) 다
캡처 리스트(Capture List)로 메모리 문제 해결하기
클로저가 강한 참조를 만들지 않게 하려면, 캡처 리스트를 사용해야 한다.
✅ weak self 사용하기 (약한 참조)
class Person {
let name: String
var greeting: (() -> Void)?
init(name: String) {
self.name = name
}
func sayHello() {
greeting = { [weak self] in
guard let self = self else { return }
print("안녕하세요, 저는 \(self.name)입니다.")
}
}
deinit {
print("\(name) 객체 해제됨")
}
}
var person: Person? = Person(name: "Alice")
person?.sayHello()
person = nil // "Alice 객체 해제됨" (정상적으로 해제됨!)
이제 person = nil이 되면 메모리에서 정상적으로 해제된다!
- weak self를 사용하면 클로저가 self를 강한 참조하지 않는다.
- 대신, self가 해제되면 nil이 되도록 만든다.
- 따라서 객체가 해제될 수 있도록 ARC가 정상적으로 동작한다.
✅ unowned self 사용하기 (비소유 참조)
unowned는 weak과 비슷하지만, nil을 허용하지 않고 항상 self가 살아 있다고 가정한다.
func sayHello() {
greeting = { [unowned self] in
print("안녕하세요, 저는 \(self.name)입니다.")
}
}
unowned는 객체가 무조건 살아 있다고 가정하기 때문에, 객체가 먼저 해제되면 크래시가 발생할 수도 있다.
따라서, self가 항상 존재할 거라고 확신할 때만 사용해야 한다.
캡처 리스트 문법 정리
캡처 리스트는 [캡처 방식 변수명] 형태로 사용된다.
{ [weak self] in ... }
{ [unowned self] in ... }
{ [weak a, unowned b] in ... } // 여러 개도 가능!
- weak → 객체가 없어질 수도 있는 경우 (nil 가능)
- unowned → 객체가 항상 살아 있다고 가정하는 경우 (nil 불가능)
정리 - 캡처 리스트가 필요한 이유
캡처 리스트를 사용해야 하는 이유
✅ 클로저가 변수를 캡처할 때 메모리 누수를 방지하기 위해
✅ self를 강하게 참조하지 않고, 안전하게 메모리를 해제하기 위해
✅ 강한 참조 순환(Strong Reference Cycle)을 해결하기 위해
결국, 캡처 리스트를 잘 사용하면?
- 메모리 누수 없이 안정적인 코드 작성 가능!
- 클로저가 강한 참조를 만들지 않도록 제어 가능!
- 앱 성능 최적화 & 예상치 못한 크래시 방지 가능!
Swift에서 클로저를 사용할 때는 항상 캡처 리스트를 고민해야 한다.
특히, self를 캡처하는 경우에는 "이게 강한 참조 순환을 만들지 않는가?"
한 번 더 생각하는 습관을 들이면 좋다.
'iOS' 카테고리의 다른 글
| OperationQueue - iOS에서 비동기 작업을 효율적으로 관리하는 방법! (0) | 2025.02.27 |
|---|---|
| Async/Await 완벽 정리 – 왜 써야 할까? 어떻게 써야 할까? (0) | 2025.02.26 |
| iOS에서 비동기 작업을 처리하려면? GCD(DispatchQueue)를 이해하자! (0) | 2025.02.25 |
| Run Loop – iOS 앱은 어떻게 끊기지 않고 동작할까? (0) | 2025.02.24 |
| RxSwift - Concat 과 Merge (0) | 2024.02.26 |