2016년 4월 27일 수요일

Swift - @autoclosure 이야기

@noescape 속성을 정리하다 보니 @autoclosure 도 함께 따라다니는 것 같아서 이 속성도 정리해 본다.

@autoclosure 속성은 이름만 보면 명확한 의미 이해는 안되는데 뭔가 자동화 된다고는 생각 할 수 있다. 일단 공식 레퍼런스의 정의는 아래와 같다.
This attribute is used to delay the evaluation of an expression by automatically wrapping that expression in a closure with no arguments. Apply this attribute to a parameter declaration for a function or method type that takes no arguments and that returns the type of the expression. Declarations with the autoclosure attribute imply noescape as well, except when passed the optional attribute argument escaping. For an example of how to use the autoclosure attribute, see Autoclosures and Function Type.
핵심을 꼽자면 역시 제일 첫 문장이다. wrapping 즉 감싸준다는 말이 쓰이고 있다.

지연된 실행

'지연된 실행' 이라는 단어는 클로져의 특징과 관계가 있다. 클로져는 선언 단계에서 코딩된 내용이 바로 실행되지 않는다. 클로져가 호출되어야만 비로소 실행이 된다. 아래 예제를 보자.
var names = ["Kim", "Park", "Lee"]

let closure = {
  names.removeFirst()
}

names
// ["Kim", "Park", "Lee"]

closure()

names
// ["Park", "Lee"]
names 의 내용을 두 차례에 걸쳐서 확인해 봤는데, closure 자체가 호출되기 전에는 names.removeFirst() 라는 기능이 실행되지 않았음을 확인 할 수 있다. 바로 이것이 '지연된 실행(Delayed Evaluation)'이라는 말의 의미이다.

뭐 당연하다면 당연한 이야기다.

좀 더 짧게 쓰고 싶어!

이제 다른 예제를 보자. 평범하게 클로져를 활용하는 예제이다.
var names = ["Kim", "Park", "Lee"]

func updateNames(closure: () -> String) {
  print("Updated names array with \(closure())")
}

names
// ["Kim", "Park", "Lee"]

updateNames() {
  return names.removeFirst()
}

names
// ["Park", "Lee"]
updateNames() 라는 함수는 클로져를 받아서 이 클로져를 실행시키고 콘솔에 결과를 표시하는 매우 단순한 녀석이다. 앞서 본 지연된 실행이라는 이야기도 같이 들어있다.

여기서 updateNames() 를 통해 names의 첫 번째 아이템을 삭제하는 코드를 클로져를 구현하고 있다.

그런데, 만약 이 클로져 코딩 내용을 좀 더 짧게 표시하고자 하는 생떼(?)를 부릴 사람이 있을지도 모르겠다. 아래 처럼 말이다.
updateNames(names.removeFirst())
물론 현재 상태에서 위 코드는 에러가 발생한다. 여기서 쓰인 names.removeFirst()  코드는 지연된 실행이 아니라 즉시 실행되는 코드이다. 즉 함수 호출 전에 이미 names.removeFirst() 가 실행되어 버리고 이 결과가 updateNames() 함수에 인자로 전달되려 하고 결국 타입이 맞지 않다는 오류를 일으킨다.

이런 경우에 활용이 가능한 것이 바로 @autoclosure 속성이다.
var names = ["Kim", "Park", "Lee"]

func updateNames(@autoclosure closure: () -> String) {
  print("Updated names array with \(closure())")
}

names
// ["Kim", "Park", "Lee"]

updateNames(names.removeFirst())

names
// ["Park", "Lee"]
앞서 봤던 생떼가 실제로 이루어지는 기적을 볼 수 있다.

기적 같지만... 여기서 생긴 변화가 실제로는 앞서 본 클로져를 이용하는 것과 동일하게 변화한다고 생각해야 한다. @autoclosure 속성은 wrapping 이라는 걸 상기하자. 즉, @autoclosure 가 표기되어 있는 곳에 들어가는 코드는 자동으로 클로져 형식으로 한번 더 감싸져서 넘어가게 된다.

결론

지연된 실행이니 뭐니 이상한 말만 많이 쓴 것 같은데, 간략히 정리하면 @autoclosure 는 '구문을 클로져로 알아서 감싸달라' 라는 속성이다. 그래서 위의 예제가 동작이 가능해 지는 셈이지, 실제로 그냥 함수 호출 형태가 바로 넘어가는게 아니다.

개인적으론 @autoclosure 같은 '코드에 모호성을 주는 문법'은 가급적 쓰지 말자는 주의이다. 물론 잘 쓰면 편한데, 만약 함수나 메소드의 원형에 @autoclosure 가 명시되어 있는지 미리 확인하지 않고 코드를 작성하면 문제가 발생할 여지가 있다. 심지어 @autoclosure 파라미터에는 일반 클로져를 넘길 수가 없다. 함수나 구문이 자동으로 클로져로 감싸지니 말이다.

... 잊지 말자. 프로그래머에게 귀차니즘이란 필수불가결한 속성(?)임을 ...

참고로 @autoclosure 속성은 기본적으로 @noescape 속성을 부여한다. 이걸 없애고 싶다면 아래와 같은 식으로 구현하면 된다.
func updateNames(@autoclosure(escaping) closure: () -> String) {
  print("Updated names array with \(closure())")
}
한번 더 말하지만, 적재적소에 잘 쓰면 좋은 기능이다.

[관련글] Swift - @noescape 너 도데체 뭐냐
[관련글] Swift - 클로져(Closures)
[관련글] 스위프트(Swift) 가이드

댓글 3개 :

jusung :

좋은 정보 공유 감사합니다. :)

Unknown :

시작한지 얼마 안됐지만
스위프트는 왜 저렇게 만들었는지 이해가 안가네요.
징징대는 안드로이드 개발자가...

Seorenn :

@The Dongster:
이게 어떻게 쓰느냐에 따라 달라지긴 하지만, 함수형 프로그래밍에선 문법적 잇점을 주기 때문에 나쁜건 아닌 것 같습니다. 다만 escaping 문제는 ARC 와 연관이 있기 때문에 해당 부분에 대한 지식이 없다면 이해하기 어려울 수도 있습니다.