ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Swift Closure Capture Semantics
    Swift 2025. 11. 15. 09:07

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 Swift Closure의 Capture Semantics에 대해 알아보겠습니다 🙋🏻

     

    기본 문법은 모두 알고 계실테니, 실제로 헷갈리는 심화 내용들만 집중적으로 다뤄볼게요!


    Reference vs Value: 캡처 시점의 차이

    기본 동작

    Swift 클로저는 기본적으로 외부 변수에 대한 참조를 캡처하며, 이 참조는 클로저가 실제로 실행되는 시점에 평가됩니다.

    var counter = 0
    let closure = { print(counter) }
    
    counter = 10
    closure() // 10 출력 (0이 아님!)

     

     

    클로저 생성 시점이 아닌 실행 시점의 값이 사용됩니다.


    Capture List로 값 복사하기

    캡처 리스트를 사용하면 클로저 생성 시점의 값을 캡처할 수 있습니다.

    var counter = 0
    let closure = { [counter] in print(counter) }
    
    counter = 10
    closure() // 0 출력
    

     

     


    Value Type의 예기치 않은 동작

    Value type은 외부 스코프로 복사본으로 전달되기 때문에 클로저 캡처 시 다소 복잡할 수 있습니다.

    struct User {
        var name: String
    }
    
    class ViewModel {
        var user = User(name: "Green")
        lazy var displayName: () -> String = {
            // ⚠️ struct는 값 복사!
            return self.user.name
        }
        
        func updateUser() {
            user.name = "Updated"
            print(displayName()) // 여전히 "Green" 출력될 수 있음
        }
    }
    

     

    데이터 레이스가 일어나거나 멀티 스레드 환경에서 정말 극히 드물겠지만 발생할 여지는 있죠.

     

    해결방법 - 캡처 리스트에서 명시적으로 참조 캡처

    lazy var displayName: () -> String = { [self] in
        return self.user.name
    }

     

     


    Escaping Closure와 메모리 관리

    언제 @escaping이 필요한가?

    클로저가 함수의 인자로 전달되었지만 함수가 반환된 후에 호출되는 경우, 해당 클로저는 escaping합니다.

    class NetworkManager {
        var handlers: [() -> Void] = []
        
        // ✅ 저장되므로 @escaping 필요
        func register(handler: @escaping () -> Void) {
            handlers.append(handler)
        }
        
        // ✅ 즉시 실행되므로 non-escaping (기본값)
        func execute(block: () -> Void) {
            block()
        }
    }
    

    Self 명시의 숨은 의미

    non-escaping 클로저에서는 self 키워드를 문제없이 사용할 수 있는데, 클로저가 함수가 반환되기 전에 실행되므로 self가 확실히 존재하기 때문입니다.

    class ViewController {
        var name = "Green"
        
        // Non-escaping: self 생략 가능
        func updateUI(with block: () -> Void) {
            print(name) // OK
            block()
        }
        
        // Escaping: self 명시 필수
        func fetchData(completion: @escaping () -> Void) {
            DispatchQueue.main.async {
                print(self.name) // self 필수!
            }
        }
    }
    

     


    Weak vs Unowned 선택 기준

    Weak: Optional의 안전성

    [weak self] 캡처 리스트를 사용할 때, 클로저 내부의 self 참조는 옵셔널이 되므로 옵셔널 체이닝으로 언래핑해야 합니다.

    class DataManager {
        func fetchData() {
            networkCall { [weak self] result in
                guard let self else { return }
                self.process(result)
            }
        }
    }
    

     


    Unowned: 다소 위험할 수 있는 최적화

    unowned로 캡처된 객체에 접근할 때 객체가 nil이면 런타임 에러가 발생합니다.

    class Parent {
        var child: Child?
        
        func setup() {
            child = Child(parent: self)
        }
    }
    
    class Child {
        unowned let parent: Parent // Parent가 먼저 해제되면 크래시!
        
        init(parent: Parent) {
            self.parent = parent
        }
    }
    

     

    Weak 사용: delegate, completion handler 등 대부분의 경우

    Unowned 사용: 부모-자식 관계에서 자식이 부모보다 먼저 해제되는 것이 보장될 때만

     


    클로저가 Heap에 저장되는 이유

    클로저는 참조 타입으로 간주되며 스택이 아닌 힙에 저장됩니다.

    캡처된 모든 value type은 클로저의 생명주기 동안 접근할 수 있도록 힙으로 이동됩니다.

    여기서 사실 정확한건 value가 heap에 저장된다라고 표현하기 보다는 capture context가 힙 영역에 이동된다고 볼 수 있습니다!

    func makeCounter() -> () -> Int {
        var count = 0 // Value type이지만...
        
        return {
            count += 1 // 클로저가 힙에 있으므로 count도 힙으로 이동!
            return count
        }
    }
    
    let counter = makeCounter()
    print(counter()) // 1
    print(counter()) // 2 - count가 유지됨
    

     


    Capture List 패턴

    여러 변수 동시 캡처

    class ViewModel {
        var user: User
        var settings: Settings
        
        func setupHandler() {
            someAsyncWork { [weak self, user, settings] in
                guard let self else { return }
                // self는 weak, user/settings는 strong 캡처
                self.process(user: user, settings: settings)
            }
        }
    }
    

     

    Alias를 통한 명확한 의도 표현

    class Manager {
        func fetchData() {
            api.request { [weak weakSelf = self] in
                weakSelf?.handleResponse()
            }
        }
    }

     

     

    최적화 메커니즘

    최적화로서, Swift는 클로저에 의해 값이 변경되지 않고 클로저가 생성된 후에도 값이 변경되지 않는 경우, 값의 복사본을 캡처하고 저장할 수 있습니다.

    let constant = 42
    
    let closure = {
        print(constant)
    }
    
    // Swift가 자동으로 값 복사 최적화 수행
    

     


    안티패턴과 해결 방법

    ❌ 잘못된 패턴: Nested Closure에서 self 중복 캡처

    class ViewController {
        func fetchAndProcess() {
            networkManager.fetch { [weak self] data in
                guard let self else { return }
                
                // ⚠️ 내부 클로저에서 다시 캡처 필요!
                DispatchQueue.main.async {
                    self.updateUI(with: data) // 강한 참조 발생!
                }
            }
        }
    }
    

     

    ✅ 해결 패턴

    func fetchAndProcess() {
        networkManager.fetch { [weak self] data in
            guard let self else { return }
            
            DispatchQueue.main.async { [weak self] in
                guard let self else { return }
                self.updateUI(with: data)
            }
        }
    }
    

     

     


    Conclusion

    Closure Capture Semantics의 핵심 포인트들을 정리해볼게요.

     

    1️⃣ 기본 동작: 참조 캡처 + 실행 시점 평가
    2️⃣ Value Type: 의도와 다르게 동작할 수 있으니 주의
    3️⃣ Escaping: self 명시 + 메모리 관리 필수
    4️⃣ Weak/Unowned: 안전한 weak를 기본으로, unowned는 신중하게
    5️⃣ 최적화: Swift가 알아서 해주지만, 명시적 캡처로 의도를 분명히

     

    프로젝트에서 메모리 릭의 대부분은 클로저 캡처 때문이니, 항상 신경써서 작성해야겠습니다! 🔥

    References

     

    Documentation

     

    docs.swift.org

     

    [swift-evolution-announce] [Accepted] SE-0103: Make non-escaping closures the default

     

    lists.swift.org

    'Swift' 카테고리의 다른 글

    NSAttributedString Performance Optimization  (0) 2025.12.06
    Swift Build Technologies  (0) 2025.11.01
    Swift SDK for Android - Android 앱을 Swift로 개발하기  (0) 2025.10.26
    Iterate Over Parameter Packs  (0) 2025.10.18
    Swift Profile Recorder  (0) 2025.10.12
Designed by Tistory.