[Swift] @autoclosure
안녕하세요, 루피입니다.
오늘은 클로저를 공부하다 알게된 @autoclosure에 대해 정리 해보려합니다. @autoclosure에 대해 단순히 { } 를 생략하게 해주는 코드를 깔끔하게 만들어주는 Syntax Sugar로만 알고 계신분들도 많을텐데요. 이번시간에는 @autoclosure의 진짜 강력함인 실행 지연에 대해 좀 더 집중하도록 하겠습니다.
바로 시작합니다.
문제의 시작: "굳이 지금 계산해야 할까?"
@autoclosure를 이해하려면 먼저 이것이 해결하려는 문제를 알아야 합니다. 다음은 간단한 로그를 출력하는 함수입니다.
// 조건이 참일 때만 메시지를 출력하는 함수
func logIfTrue(_ condition: Bool, _ message: String) {
if condition {
print("Log: \(message)")
}
}
이 함수를 이렇게 사용해 볼까요?
let userIsActive = true
let expensiveDebugMessage = "User details: \(fetchUserDetails())" // fetchUserDetails()는 1초 걸리는 무거운 작업
logIfTrue(userIsActive, expensiveDebugMessage)
위 코드에는 심각한 비효율이 숨어있습니다.
logIfTrue 함수는 condition이 true일 때만 message를 사용하는데, expensiveDebugMessage 문자열은 logIfTrue 함수를 호출하기 전에 이미 생성됩니다.
즉, fetchUserDetails()는 userIsActive 값과 상관없이 항상 실행됩니다. 로그를 찍지도 않을 건데, 1초짜리 작업을 매번 실행하는 것은 엄청난 낭비입니다.
@autoclosure의 등장
@autoclosure는 이 문제를 해결합니다. 값 자체를 넘기는 대신, 그 값을 만들어내는 코드 조각을 넘기도록 만드는 것이죠.
// 이제 message 파라미터는 @autoclosure 속성을 가집니다.
func logIfTrueWithAutoclosure(_ condition: Bool, _ message: @autoclosure () -> String) {
// 조건이 참일 때만, message 클로저를 실행해서 값을 얻습니다.
if condition {
print("Log: \(message())") // 괄호()를 붙여 클로저를 실행!
}
}
// 호출하는 쪽의 코드는 그대로입니다. 하지만 동작 방식은 완전히 다릅니다.
logIfTrueWithAutoclosure(userIsActive, "User details: \(fetchUserDetails())")
무슨 일이 일어났을까요?
logIfTrueWithAutoclosure를 호출할 때, Swift 컴파일러는"User details: \(fetchUserDetails())"라는 표현식을{ return "User details: \(fetchUserDetails())" }라는 클로저로 자동으로 포장합니다.logIfTrueWithAutoclosure함수는 이 클로저를message파라미터로 전달받습니다. 아직fetchUserDetails()는 실행되지 않았습니다!- 함수 내부에서
if condition을 검사합니다. condition이true일 때, 비로소message()를 호출하여 클로저를 실행합니다. 바로 이 순간fetchUserDetails()가 실행되고 문자열이 생성됩니다.condition이false라면,message클로저는 영원히 실행되지 않아 불필요한 비용을 아낄 수 있습니다.
예시
한번 더 코드로 예시를 들어 보겠습니다.
func complexWork() -> Bool {
print("!!엄청나게 복잡하고 비싼 작업 실행!!")
return true
}
// 1. 일반 Bool 값을 받는 함수
func regularFunction(value: Bool) {
print("regularFunction: 시작")
if value {
print("regularFunction: 조건 만족")
}
}
// 2. @autoclosure 클로저를 받는 함수
func autoclosureFunction(_ logic: @autoclosure () -> Bool) {
print("autoclosureFunction: 시작")
// 여기서 logic()을 호출해야만 클로저 내부 코드가 실행됨
if logic() {
print("autoclosureFunction: 조건 만족")
}
}
print("--- 일반 함수 테스트 ---")
regularFunction(value: complexWork())
print("\n--- 오토클로저 함수 테스트 ---")
autoclosureFunction(complexWork())
--- 일반 함수 테스트 ---
!!엄청나게 복잡하고 비싼 작업 실행!!
regularFunction: 시작
regularFunction: 조건 만족
--- 오토클로저 함수 테스트 ---
autoclosureFunction: 시작
!!엄청나게 복잡하고 비싼 작업 실행!!
autoclosureFunction: 조건 만족
결과를 통해 차이점을 명확히 알 수 있습니다.
- 일반 함수: regularFunction의 파라미터로 complexWork()를 전달하기 위해, 함수 호출 전에 complexWork()가 먼저 실행됩니다.
- 오토클로저 함수: autoclosureFunction이 먼저 호출되고, 함수 내부에서 logic()이 호출되는 시점에 비로소 complexWork()가 실행됩니다.
Swift의 대표적인 활용 사례
이러한 @autoclosure의 장점은 Swift의 기본 라이브러리인 assert 함수에 완벽하게 녹아있습니다.
assert(condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = ...)
assert는 디버깅 중에 특정 조건이 참인지를 검사하는 함수입니다. 대부분의 경우 condition은 참이고, 아무 일도 일어나지 않습니다.
@autoclosure 덕분에 condition이 false라는 치명적인 오류가 발생했을 때만 message를 생성하여, 디버깅 빌드의 성능 저하를 최소화할 수 있습니다.
언제 사용해야 할까?
@autoclosure는 강력하지만, 남용하면 코드의 흐름을 파악하기 어렵게 만들 수 있습니다. 따라서 다음과 같은 경우에 사용하는 것이 좋습니다.
- 실행 지연으로 명백한 성능 이득이 있을 때:
assert처럼 조건부로 실행되는 코드 블록에 매우 유용합니다. - API의 가독성을 극적으로 향상시킬 때: 특정 도메인에 맞는 언어(DSL)를 설계할 때, 사용자가
{}없이 자연스러운 코드를 작성하게 할 수 있습니다.
오늘도 화이팅입니다!