🔥 Autoclosures

483자
6분

Swift에서 autoclosure는 함수의 인자로 전달되는 표현식을 자동으로 감싸는 클로저를 말해요. autoclosure는 인자를 받지 않고, 호출되면 내부에 감싸진 표현식의 값을 반환하지요. 이러한 문법적 편의성 덕분에 명시적인 클로저 대신 일반 표현식을 작성하여 함수의 매개변수 주위의 중괄호를 생략할 수 있답니다.

autoclosure를 사용하는 함수를 호출하는 것은 일반적이지만, 그런 종류의 함수를 직접 구현하는 것은 흔하지 않아요. 예를 들어, assert(condition:message:file:line:) 함수는 conditionmessage 매개변수에 대해 autoclosure를 사용하는데요. condition 매개변수는 디버그 빌드에서만 평가되고, message 매개변수는 conditionfalse인 경우에만 평가된답니다.

autoclosure를 사용하면 클로저 내부의 코드가 호출될 때까지 실행되지 않기 때문에 평가를 지연시킬 수 있어요. 부작용이 있거나 계산적으로 비용이 많이 드는 코드의 경우 평가 시점을 제어할 수 있어서 유용하지요. 아래 코드는 클로저가 어떻게 평가를 지연시키는지 보여줍니다.

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// "5" 출력
 
 
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// "5" 출력
 
 
print("Now serving \(customerProvider())!")
// "Now serving Chris!" 출력
print(customersInLine.count)
// "4" 
swift

클로저 내부의 코드에 의해 customersInLine 배열의 첫 번째 요소가 제거되더라도, 클로저가 실제로 호출될 때까지 배열 요소는 제거되지 않아요. 만약 클로저가 호출되지 않으면, 클로저 내부의 표현식은 절대 평가되지 않고, 배열 요소도 제거되지 않습니다. customerProvider의 타입이 String이 아니라 () -> String, 즉 매개변수가 없고 문자열을 반환하는 함수라는 점에 주목하세요.

클로저를 함수의 인자로 전달할 때도 동일한 지연 평가 동작을 얻을 수 있어요.

// customersInLine은 ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// "Now serving Alex!" 출력
 
swift

위 예제의 serve(customer:) 함수는 고객의 이름을 반환하는 명시적 클로저를 인자로 받아요. 아래 버전의 serve(customer:)는 동일한 작업을 수행하지만, 명시적 클로저 대신 매개변수 타입에 @autoclosure 속성을 표시하여 autoclosure를 사용합니다. 이제 customerProvider 매개변수의 타입이 @autoclosure 속성으로 표시되었기 때문에, 클로저 대신 String 인자를 사용하는 것처럼 함수를 호출할 수 있어요. 인자는 자동으로 클로저로 변환됩니다.

// customersInLine은 ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// "Now serving Ewa!" 출력
 
swift

escape가 허용되는 autoclosure를 원한다면, @autoclosure@escaping 속성을 모두 사용하세요. @escaping 속성은 위에서 Escaping Closures에 설명되어 있답니다.

// customersInLine은 ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
 
 
print("Collected \(customerProviders.count) closures.")
// "Collected 2 closures." 출력
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// "Now serving Barry!" 출력
// "Now serving Daniella!" 출력
swift

위 코드에서 collectCustomerProviders(_:) 함수는 customerProvider 인자로 전달된 클로저를 호출하는 대신 customerProviders 배열에 추가해요. 배열은 함수의 범위 밖에서 선언되므로, 함수가 반환된 후에도 배열의 클로저를 실행할 수 있습니다. 그 결과, customerProvider 인자의 값은 함수의 범위를 벗어날 수 있어야 해요.

이렇게 autoclosure를 사용하면 코드의 가독성을 높이면서도 성능을 향상시킬 수 있답니다. 평가를 지연시켜 불필요한 연산을 피할 수 있고, 문법적 편의성으로 코드를 간결하게 작성할 수 있지요.

하지만 autoclosure를 남용하면 코드의 의도가 불명확해질 수 있으니 주의해야 해요. 꼭 필요한 경우에만 사용하고, 함수의 동작을 명확하게 문서화하는 것이 좋답니다.

Swift의 autoclosure는 강력한 기능이면서도 편리한 문법을 제공하는 훌륭한 예시라고 할 수 있어요. 적절히 사용한다면 코드의 가독성과 성능을 모두 높일 수 있습니다.