2016년 4월 20일 수요일

Swift - @noescape 너 정체가 뭐냐

예전에 Swift 1.2 변동사항에 대해 쓰면서 뭔지 잘 모르고 넘어갔었는데 갑자기 궁금해져서 그 정체를 찾아보기 시작했다. (참고로 현재 Swift 최신 안정 버전은 2.2 이다 -_-) 이번 글은 바로 이 @noescape 속성에 관한 내용이다.

애플 공식 레퍼런스 문서에서는 @noescape 속성(Attributes)에 대해 아래와 같이 정의하고 있다.
Apply this attribute to a function or method declaration to indicate that a parameter will not be stored for later execution, such that it is guaranteed not to outlive the lifetime of the call. Function type parameters with the noescape declaration attribute do not require explicit use of self. for properties or methods.
함수나 메소드의 파라미터에 이 속성이 적용되면 살아있는 동안에 호출된다는 보장(?)이 없어서 나중에 실행시킬 목적으로 저장할 수 없다. noescape 속성이 붙은 함수 타입 파라미터는 프로퍼티나 메소드의 사용을 위해 명시적인 self. 을 필요로 하지 않는다.
이게 도데체 뭔 말이여???

일단 테스트를 해 보자

아래와 같은 예제를 하나 만들어 봤다.
func someFunc(@noescape closure: () -> Int) -> Int {
  let result = closure()
  return result
}

someFunc {
  print("Running with @noescape")
  return 0
}
별 문제 없이 동작한다.

참고로 아랫쪽에서 someFunc를 괄호 없이 호출하고 있고 바로 뒤에 중괄호가 붙어서 모양이 좀 이상 할 수도 있는데, 함수의 인자가 클로져 하나뿐이고 인자로 넘겨주는 클로져를 꼬리클로져(trailing closure) 형태로 구현할 때 이런 식의 문법도 사용할 수 있다.

앞서 공식 문서의 설명 중 '나중에 실행시킬 목적으로 저장 할 수 없다' 라는 말을 한번 시험해보자.
func someFunc(@noescape closure: () -> Int) -> Int {
  let someClosure = closure
  // Error: @noescape parameter 'closure' may only be called

  let result = someClosure()
  return result
}
단순하게 다른 상수에 이 클로져를 넣는 코드에서 컴파일 에러가 발생한다. 에러 내용으로 봐서 @noescape 속성은 '저장은 불가능하고 호출만 가능하다' 라고 생각 할 수 있다.

그렇다면 저장이 아닌 다른 함수에 던져주는 것도 비슷할까? 시험해보자.
func anotherFunc(closure: () -> Int) -> Int {
  return closure()
}

func someFunc(@noescape closure: () -> Int) -> Int {
  let result = anotherFunc(closure)
  // Error: Invalid conversion from non-escaping function of type 
  //        '@noescape () -> Int' to 
  //        potentially escaping function type '() -> Int'

  return result
}
컴파일 에러가 나는 것 까지는 동일하다. 하지만 에러 내용이 다르다. 이번에는 타입이 다르다는 에러다. anotherFunc 의 closure 파라미터 역시 @noescape 속성을 붙여주면 에러가 안날까?
func anotherFunc(@noescape closure: () -> Int) -> Int {
  return closure()
}

func someFunc(@noescape closure: () -> Int) -> Int {
  let result = anotherFunc(closure)
  return result
}
이렇게 고치면 에러가 발생하지 않는다. @noescape 로 받은 클로져는 역시 동일한 @noescape 속성의 파라미터로 넘겨주는건 문제가 되지 않는다는 말이다.

비록 다른 함수 호출에 @noescape 속성의 파라미터를 던져주긴 했지만, 문법적인 면에서 아직까지는 '저장 불가' 라는 것을 위반하지는 않았다.

이제 다른 클로져에서의 호출의 경우는 어떻게 되는지 테스트 해 보자.
func someFunc(@noescape closure: () -> Int) -> Int {
  let anotherClosure: () -> Int = {
    return closure()
    // Error: Closure use of @noescape parameter 
              'closure' may allow it to escape
  }

  let result = anotherClosure()
  return result
}
다른 클로져에서 @noescape 속성의 클로져를 호출하는 것은 허용되지 않는다. 명쾌하다.

이제 정의의 나머지 내용인 '명시적인 self. 을 붙이지 않아도 된다' 라는 말을 시험해보자. 명시적이라는 건 별로 생각할 필요 없고 그냥 self 를 인용하지 않아도 된다는 의미로 생각하고 아래와 같이 코드를 작성했다.
func someFunc(@noescape closure: () -> Int) -> Int {
  let result = closure()
  return result
}

struct SomeStruct {
  func generateValue() -> Int {
    return 0
  }
  
  func work() {
    print(someFunc {
      return generateValue()
    })
  }
}

let ss = SomeStruct()
ss.work()
아무 문제 없이 컴파일되고 의도대로 실행된다.

위 코드에서 일반적인 방식으로 generateValue() 라는 메소드를 클로져 안에서 호출하려면 self.generateValue() 라고 적어줘야 된다. 다르게 말해서, 만약 위 코드에서 someFunc에서 @noescpae 속성을 빼버리면 아래와 같은 오류가 발생한다.
Call to method 'generateValue' in closure registers explicit 'self.' to make capture semantics explicit
이 밖에도 클로저 머릿부분에 [weak self] 와 같은 메모리 관리 규칙도 필요하다면 알려줘야 했는데 그걸 생략해도 되게 해 주는데 초점이 맞춰져 있나보다.

그래서 도데체 넌 뭐냐

결론 부터 말하자면 noescape 라는 단어의 의미는 "탈출이 없다" 라는 것이 아니라 "이것을 가진 채로는 탈출 할 수 없다" 라고 해석하는게 맞는 것 같다.

이상의 테스트에서 확인 할 수 있는 사항은 '저장이 불가능하고 호출이 가능한 클로져 타입' 이라는 점이다. 물론 공식적인 내용에서 클로져라는 말은 없지만, 호출 가능하다는 점에서는 주로 클로져가 해당될테니 이 말이 완전히 틀린 말은 아닐 것이다. (물론 함수도 비슷하겠지만...)

저장 불가능 이라는 특징은 noescape 타입의 클로져를 어딘가 저장하는게 불가능하다는 의미이고, 이는 메모리 관리에서 어딘가에 종속될 여지를 불가능하게 만들어 버린다는 특징이 있다. 결국 메모리 관리에서 잇점이 될 수 있다.

또한 저장이 불가능하다는 의미에는 '@noescape 클로져를 사용하는 메소드는 그 메소드가 동작 중일때만 해당 클로져를 호출한다' 라고 생각 할 수 있다. 즉 나중에 다른 액션을 통해서 호출되는 경우는 절대로 발생하지 않는다.

그 외에 self. 을 붙이지 않아도 된다는 것은 명확한 의미는 모르겠다. 그냥 자동으로 weak self 가 된다거나 혹은 정말로 단순한 보너스 기능일지도 모른다. 어쨌든간에 편리함을 준다는 것은 맞다.

이 정도로 @noescape 의 정체에 대한 정리가 되는지는 잘 모르겠다. 하지만 이전 보다는 좀 더 명확하게 이해가 된 것 같다.

[관련글] Swift 1.2 에서 바뀌는 것들
[관련글] Swift - 클로져(Closures)
[관련글] 스위프트(Swift) 가이드

댓글 3개 :

Steve :

원문으로 이해가 안가서... 여기 글 보구 많은 도움을 받았습니다. 감사합니다.

duhyun kim :

많은 도움이 되었습니다. 그런데 such that it is guaranteed not to outlive the lifetime of the call. 번역이 조금 틀린것 같아요.
@noescape 속성으로 넘어온 함수나 메소드는 원래의 호출(the call)보다 오래 살지 않도록(not to outlive) 보장하므로(guaranteed), 나중에 실행할 목적으로 저장할 수 없다. 아닌가요?

Renn Seo :

@duhyun kim: 네 그런것 같네요. 지금 봐도 저 문구는 눈이 빙글빙글 돌아갈 정도로 해석이 어렵네요. 으으으으 ㅠㅠ