티스토리 뷰

클로저(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를 캡처하는 경우에는 "이게 강한 참조 순환을 만들지 않는가?"
한 번 더 생각하는 습관을 들이면 좋다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함