[Swift] @escaping Closure / escape 클로저

2022. 8. 31. 22:10🍏/Swift

xCode: 13.4.1
Swift: 5.5

@escaping 속성은 인자값으로 전달된 클로저를 저장해 두었다가, 나중에 다른 곳에서도 실행 할 수 있도록 허용해주는 속성.

@escaping 클로저에 non-escaping parameter 'fn' 할당이라는 에러가 납니다.

디버깅 >

func outFunc() -> Void{
	print("outFunc")
}
var a: () -> Void = outFunc
func callback(fn: @escaping () -> Void) {
	let f = fn
	f()
	a = fn
	a()
}
a()				// outFunc
callback {
	print("closure")	// closure / closure
}
a()				// closure

 

스위프트에서 함수의 인자값으로 전달된 클로저는 기본적으로 탈출불가(non-escape)의 성격을 가집니다.
이는 해당 클로저를 1. 함수 내에서 2. 직접 실행을 위해서만 사용해야 하는 것을 의미하며, 이 때문에 함수 내부라 할지라도 변수나 상수에 대입할 수 없습니다. 변수나 상수에 대입하는 것을 허용한다면 내부 함수를 통한 캡처(Capture) 기능을 이용하여 클로저가 함수 바깥으로 탈출할 수 있기 때문입니다.
(해당 내용의 오류의 구글링이 Swift 3까지만 쓰여 있는 것으로 유추하여 Swift 3 까지는 허용하지 않았던 것으로 보입니다.)


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

func someFunctionWithNonescapingClosure(closure: () -> Void) {
	closure()    // 함수 안에서 끝나는 클로저
}

class SomeClass {
	var x = 10
	func doSomething() {
		someFunctionWithEscapingClosure { self.x = 100 }
// escaping될 때 값을 캡쳐 해야하므로 클로저 내부의 속성 'x'에 대한 참조는 'self'를 사용하여 캡쳐를 명시적으로 나타내야함
// self를 사용한 참조.
		someFunctionWithNonescapingClosure { x = 200 }
	}
}

let instance = SomeClass() 	// x == 10
instance.doSomething()		// x == 200
completionHandlers.first?()	// x == 100


클로저의 기본 속성을 탈출불가(non-escape)하게 관리하는 가장 큰 이유
> 가장 큰 이점은 컴파일러가 코드를 최적화하는 과정에서의 성능향상.
 - 해당 클로저가 탈출할 수 없다는 것은 컴파일러가 더 이상 메모리 관리를 해주지 않아도 된다는 뜻.
요약하자면 참조 타입의 경우 참조하는 횟수로 메모리에서 해제 혹은 유지를 판단하며 non-escaping 클로저의 경우
함수 안에서 무조건 실행이 되고 끝나는게 보장이 되기 때문에 클로저가 실행이 되고 종료되는 시점을 명확하게 판단 가능.

즉, 어떤 참조 타입을 클로저 안에서 참조를 하게 되어도 해당 함수가 종료되기 전에 이 클로저가 실행되고 끝나는걸 보장하기 때문에 참조 횟수를 딱히 신경 쓰지 않아도 된다.

한마디로 non-escaping 클로저는 메모리 관리에 있어서 추가 오버헤드가 발생하지 않음.

하지만 escaping 클로저의 경우 함수의 외부에서 호출되기 때문에 언제 실행되는지 모르며, 오히려 이 클로저를 언제든 실행할 수 있도록 컨텍스트의 캡쳐가 필요. 때문에 메모리 관리에 있어서 추가 오버헤드가 들어가고 성능적 코스트 이슈로 Swift에서는 이러한 성능의 퍼포먼스와 최적화를 위해 필요할 때만 escaping 클로저로 사용할 수 있도록 정의.