본문 바로가기
YAGOM CAREER STARTER

[TIL] 20230330: Escaping closure/Defer

by Rhode 2023. 4. 4.

본문은 야곰 아카데미 커리어 스타터 캠프를 통해 학습한 내용을 회고한 글입니다.


Escaping closure/Defer

둘의 차이를 잘 모르겠어서 조금 공부해봤다.

알아보니 완전 다른 것인..ㅎㅎ^^

escaping closure는 언제 실행될지 확신이 없는 것이고, defer는 함수 종료를 하면서 문 닫고 나가면서 실행하는 것이다.

escaping closure는 언제 실행될지 모르지만 언젠가 불린다는 것이다.

 

escaping closure

어떤 클로저가 메서드의 인자로 전달 되지만, 메서드가 반환된 후에 불릴 때 이것을 메서드를 escape한다고 말한다. 매개변수로 클로저를 취하는 메서드를 정의할 때, 그 클로저가 escape하게 만들기 위해서 @escaping을 매개변수의 앞에 쓴다. 

클로저가 escape할 수 있는 방법 중 하나는 메서드 외부에 정의된 변수에 저장되는 것이다. 예를 들어, 비동기 작업을 시작하는 많은 메서드는 completion handler로 클로저 인자를 취한다. 그 메서드는 작업을 시작한 후에 반환되지만, 클로저는 작업이 완료될 때까지 호출되지 않는다 - 클로저는 escape되어야하며 나중에 호출된다. 예를 들어보면 이렇다:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:) 메서드는 그것의 인자로 클로저를 취한다. 그리고 메서드 외부에서 정의된 배열에 그 클로저를 추가해준다. 만약에 @escaping이라고 매개변수에 써주지 않는다면, 컴파일 에러가 날 것이다.

self를 참조하는 escaping 클로저는, 만약 self가 클래스의 인스턴스를 참조하는 경우, 특별한 고려가 필요하다. 

escaping 클로저에서 self를 캡쳐하는 것은 강한 순환 참조를 실수로 만들곤 할 것이다. 강한 순환 참조에 대해 더 알아보고자 한다면, Automatic Reference Counting을 보자.

일반적으로, 클로저는 클로저의 바디 내부에서 사용함으로서 암묵적으로 변수를 캡쳐한다. 하지만, 이런 경우에는 명시적으로 해야한다. 만약 self를 캡쳐하고자한다면, self를 사용하는 곳에서 명시적으로 self를 써야한다, 혹은 클로저의 캡쳐 리스트에 self를 더해야한다. self를 명시적으로 적는 것은 의도를 명확하게 표현하도록 할 수 있으며, 순환 참조가 없다는 것을 확인할 수 있도록 한다. 예를 들어서, 다음의 코드에서, someFunctionWithEscapingClosure(_:)에게 전달 된 클로저는 self를 명시적으로 참조하고 있다. 이와 반대로, someFunctionWithNonescapingClosure(_:)에게 전달 된 클로저는 escaping 클로저가 아니다. 이것은 self를 암시적으로 참조할 수 있다는 것을 의미한다. 

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"
여기에 클로저의 캡쳐리스트에 self를 포함시킴으로서 self를 캡쳐하고, self를 암시적으로 참조하는 doSomething()의 사례가 있다:
class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}
만약에 self가 구조체나 열거형의 인스턴스라면, self를 항상 암시적으로 참조할 수 있다. 하지만, self가 구조체나 열거형의 인스턴스라면, escaping 클로저는 self의 변화하는 참조를 캡쳐할 수 없다. 구조체와 열거형은 가변성을 공유하도록 되어있지 않다. 이 내용은 Structures and Enumerations Are Value Types에 잘 나와있다.
struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

위의 예시에서의 someFunctionWithEscapingClosure의 호출은 에러가 날 것이다. 왜냐하면, 이게 가변적인 메서드 안에 들어있기 때문이다. 그래서 self가 가변적이기 때문이다. escaping 클로저가 구조체의 self에 대한 가변적인 참조를 캡쳐할 수 없다는 룰에 위배된다. 

 

 

여기까지가 공식 문서에 대한 내용이었다.

 

정리해보자면..

escaping 클로저를 사용할 때는 @escaping 키워드를 사용해줘야한다.

escaping이 아닌 클로저에 대해서도 그 키워드를 사용해줘도 되는 듯 하지만, 컴파일러의 퍼포먼스와 최적화에 영향을 끼치기 때문에 그렇게 쓰지 않는 듯 하다.

토미의 개발노트 - [Swift] Escaping 클로저 (@escaping)

escaping 클로저가 흘러가는 방식은 다음과 같다

다음의 코드를 분석해보자:

final class NetworkManager {
    static let shared = NetworkManager()
    private let session: URLSession
    
    init(session: URLSession = URLSession.shared) {
        self.session = session
    }
    
    func startLoad(request: URLRequest, mime: String, completionHandler: @escaping (Result<Data, NetworkError>) -> Void) {
        session.dataTask(with: request) { data, response, error in
            guard error == nil else {
                completionHandler(.failure(.responseError(error: error)))
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse else {
                completionHandler(.failure(.invalidResponse))
                return
            }
            
            guard (200...299).contains(httpResponse.statusCode) else {
                completionHandler(.failure(.responseCodeError))
                return
            }
            
            let mimeType = response?.mimeType
            
            guard ((mimeType?.lowercased().contains(mime)) != nil) else {
                completionHandler(.failure(.invalidMimeType))
                return
            }
            
            guard let validData = data else {
                completionHandler(.failure(.noData))
                return
            }
            
            completionHandler(.success(validData))
        }.resume()
    }
}

이 코드에서는 startLoad에 completionHandler라는 인자로 escaping 클로저가 전달 되었다. 이 escaping 클로저는 Result<Data, NetworkError>를 매개변수로 갖고, 반환값이 없는 클로저이다. startLoad에 Result값을 어떻게 처리해줄 지에 대한 도구를 넣어준 셈이다. 그리고 이 completionHandler라는 놈은 나중에 Result값에 상응하는 놈이 들어오면 실행이 될 것이다. 그것이 escaping 클로저니까.

 

 

 

 

참조

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Escaping-Closures

 

Documentation

 

docs.swift.org

https://jusung.github.io/Escaping-Closure/

 

[Swift] Escaping 클로저 (@escaping)

정의 Escaping 클로저는 클로저가 함수의 인자로 전달됐을 때, 함수의 실행이 종료된 후 실행되는 클로저 입니다. Non-Escaping 클로저는 이와 반대로 함수의 실행이 종료되기 전에 실행되는 클로저

jusung.github.io