Neoself의 기술 블로그

Swift Closure 정리 본문

개발지식 정리/Swift

Swift Closure 정리

Neoself 2025. 4. 7. 20:55

클로저는 코드 내에서 전달되거나 사용할 수 있는 독립적인 기능 블록
1급 객체이기 때문에 변수, 함수의 인자, 함수의 반환값으로 사용할 수 있습니다.

*Kingfisher에서는 중복 다운로드 방지기능 구현을 위해 onCancelledCallback 클로저들을 배열에 보관한 후, 취소 요청이 발생하면, 해당 배열을 순회하며 콜백들을 사용합니다.


캡처

func makeAdder() -> (Int) -> Int {
    var base = 10
    
    // [base]는 캡처 리스트로, base의 값을 복사하여 캡처
    return { [base] num in
        return base + num
    }
}

func makeAdder() -> (Int) -> Int {
    var base = 10
    
    // 아래 클로저는 base 변수를 자동으로 캡처합니다
    return { num in
        return base + num  // base 변수 자동 캡처
    }
}

클로저는 클로저가 호출된 컨텍스트와 다른 생명주기를 가질 수 있습니다. 따라서 클로저 내부에서 외부 컨텍스트의 변수를 참조하게 되면, 클로저보다 외부 컨텍스트가 더 빨리 사라질 가능성이 있습니다.

때문에 클로저는 외부 컨텍스트의 변수나 상수를 사용할 때, 외부 컨텍스트의 객체들(상수 및 변수)을 힙 내부 자신의 내부 환경에 옮겨 관리(캡처)합니다.

때문에 참조한 객체가 외부에서 변경될 경우, 클로저 내부에서도 이러한 변경사항이 반영되며, 이로 인해 이후 함수가 종료되어 스택 프레임에서 변수가 제거되어도, 캡처된 변수는 메모리에 보존됩니다.

 

캡처 리스트

여기서 캡처 리스트는 클로저는 기본 캡처 동작을 재정의하는 도구입니다. 값 타입을 캡처리스트에 포함하면 클로저는 현재 값을 복사하여 캡처하며, 참조 타입을 캡처하면 참조 타입(약한, 무소유)을 제어하여 메모리 관리를 제어할 수 있습니다.

 

 

@escaping 클로저와 non-escaping 클로저의 차이점

매개변수에 @escaping을 작성하면 해당 클로저는 함수 종료 후에도 호출될 수 있음을 명시하는 것입니다. 

 

escaping 클로저는 non-escaping 클로저와 달리, 수명이 함수외부로 확장되어 순환 참조 위험이 있습니다.

- 함수 외부 변수에 저장되거나 다른 함수로 전달 될 수 있으며,

- self 사용 시 명시적으로 표기해야합니다.(self.property 형식)

*non-escaping 클로저도 순환참조가 발생할 수 있습니다. 하지만, 함수가 반환되기 전에 클로저 실행이 완료되는 것이 보장되기 때문에, 일시적으로 순환참조가 생겨도 함수 종료 시 메모리에서 해제됩니다. 하지만, 재귀적 호출이나 복잡한 참조 체인로 설계될 경우, 순환참조의 가능성이 있기에 이때에도 캡처 리스트를 통해 참조 타입을 재지정해주는 것이 좋습니다.


self가 참조 타입일 경우
- 만약 탈출 클로저 내부에서 클래스 인스턴스 self를 참조하게 된다면, 두 참조 타입이 서로를 참조하는 순환 참조가 발생하게 됩니다.
- 이를 방지하기 위해 캡처 리스트에서 weak 또는 unowned 키워드를 사용해 레퍼런스 카운트를 증가시키지 않도록 방지해야 합니다.
- 클로저의 생명주기가 self보다 긴 경우를 대비하기 위해 weak 키워드를 사용해주는 것이 좋습니다.
self가 값 타입일 경우
- 탈출 클로저에서 값 타입 self에 접근할 경우 메모리 접근 충돌 방지를 위해 컴파일 에러가 발생합니다.

    - 값 타입 self와 다른 context에서 실행될 가능성이 있기 때문에, 구조체 내부와 탈출 클로저 문맥 각각에서 self에 읽기 및 쓰기 동작을 동시에 수행하면 코드의 다른 부분이 하나의 메모리 주소에 동시에 접근할 가능성이 있습니다.

이를 방지하기 위해 캡처 리스트를 활용해, self의 immutable한 복사본을 생성해 읽기 작업을 수행할 수 있습니다. 다만, 어떤 경우에도 탈출 클로저 내에서 값 타입 self를 수정할 수는 없습니다.

// 값 타입(구조체)에서의 탈출 클로저 - 컴파일 에러 발생 예시:
struct Counter {
    var count: Int = 0
    
    mutating func increment() {
        count += 1
    }
    
    func performAsync(completion: @escaping () -> Void) {
        DispatchQueue.main.async {
            print(self.count)  // 컴파일 에러: 값 타입 self를 탈출 클로저에서 참조할 수 없음
            completion()
        }
    }
}
// 읽기 전용 접근을 위한 해결책:
struct Counter {
    var count: Int = 0
    
    mutating func increment() {
        count += 1
    }
    
    func performAsync(completion: @escaping () -> Void) {
        // self의 복사본을 캡처
        let currentSelf = self
        
        DispatchQueue.main.async {
            print(currentSelf.count)  // 복사본을 사용하여 안전하게 접근
            completion()
        }
    }
    
    // 또는 캡처 리스트 사용
    func performAsyncAlt(completion: @escaping () -> Void) {
        DispatchQueue.main.async { [self] in
            print(self.count)  // 컴파일 타임에 복사된 self를 사용
            // self.count += 1  // 에러: 복사본은 수정할 수 없음
            completion()
        }
    }
}

트레일링 클로저(Trailing Closure) 문법

함수의 매개변수로 함수를 전달 및 호출하는 것이 콜백 함수인데요, 트레일링 클로저 문법을 활용하면 콜백 함수를 손쉽게 작성할 수 있습니다.

- 트레일링 클로저 문법은 콜백 함수를 작성할 때 유용합니다.