2016년 5월 11일 수요일

Swift - Assertion과 컴파일 최적화

assert() 라는 함수는 C언어 시절 부터 예기치 못한 에러를 잡기 위한 무기(?)로써 활용되어 왔다. 문제가 생길 것 같으면 일부러 자살(?)하기 위한 무기가 바로 assert()이다.

개인적으로는 이 assert() 문이 쓸 모 없는 evaluation 을 유발시키지 않나 항상 궁금해하고 걱정해왔다. Swift 에서는 과연 이 assert() 가 실제 실행에 어떤 영향을 끼치는지 자료를 찾아본 결과를 테스트로 정리해 볼까 한다.

Assertion의 간단한 소개

Assertion 은 '코드가 실행될 때 반드시 만족해야 하는 조건을 코드 상에 명시해 놓는 것' 이라고 생각한다. 그래서 코드가 정상적으로 실행되면 문제가 없는 것이고 만약 Assertion 을 통해 문제가 발생한다면 어딘가 문제가 있다는 의미가 되어서 디버깅에 좋은 자료가 된다.

Swift 에서도 Assertion 용도로 여러 함수가 지원된다.
  • assert()
  • assertionFailure()
  • precondition()
  • preconditionFailure()
  • fatalError()
다른 언어에서 쓰이던 거랑 거의 동일하기 때문에 함수의 상세한 스펙은 생략한다. 궁금하다면 직접 찾아보자.

일반적으로 가장 많이 쓰이는 함수는 assert() 와 precondition() 이다. 이 둘은 첫 인자(Arguments)로 조건식, 즉 Boolean 값을 유발시키는 특정 식을 넣는다. 아래는 assert() 호출 예제이다.
func someFunc(value: Int) {
  assert(value > 0, "value must bigger than 0")
  ...
}
위 코드에서 assert() 의 역활은 'value 가 반드시 0 보다 커야 한다' 라는 규칙을 알려주고 있다고 생각하면 편하다. 코드 상에서 의도가 잘 드러나지는 않지만, 적어도 실수로 값을 0 이하로 주고 호출하는 경우를 미연에 방지 할 수 있다.

precondition() 함수도 assert() 와 동일한 역활을 하고 문법도 동일하다. 왜 assert() 와 같은게 하나 더 있는지는 계속 글을 읽어보면 알 수 있다.

assertionFailure() 나 preconditionFailure() 등의 함수는 assert() 나 precondition() 의 조건이 실패한 것 처럼 동작시키는 코드다. 따라서 첫 인자의 조건식이 존재하지 않는다. 이름 그대로 Failure가 되는 상황, 즉 실행되면 안되는 코드를 타고 있을 때 이를 Assertion 화 시키는 용도로 제공된다.
switch (someCases) {
case .A:
  blah()
case .B:
  blahBlah()
default:
  assertionFailure("someCases must be A or B")
}
이런 식으로 쓰인다.

만약 디버깅 모드에서 assert() 등을 통해 앱이 죽으면 죽은 위치가 확실하게 잡힌다. 이 점을 이용하면 버그를 미연에 방지하거나 디버그에 유용한 정보로 쓸 수 있다.

마지막으로 남은 fatalError() 함수는 이름 그대로 그냥 에러를 발생시키고 앱을 죽여야 할 상황에서 쓰이는터라 앞의 Assertion 이라는 것과는 좀 용도가 다르다. 아마도 자주 쓸 일은 없을거라 생각된다.

컴파일 최적화와의 관계

Swift 도 컴파일(Compile) 과정을 통해 바이너리로 바꾸어야 하는 언어다. 그래서 최적화라는 단계가 존재한다. 디버그 때에는 디버깅에 도움이 되는 정보를 넣어서 컴파일 하고 릴리즈 때는 연산 최적화와 더불어 이런 디버깅에 필요한 정보를 몽땅 빼버리는 식으로 컴파일 과정 중 바이너리를 최적화 시킨다.

Target Settings - Optimization Level 항목

최적화 플래그는 크게 디버그용(None, -Onone)과 릴리즈용(Fast, -O) 두 가지로 나눌 수 있다. 예전에는 -Ounchecked 같은 것이 있었지만 지금은 아예 선택 목록에서 제외되었고, 최근에는 -O whole-module-optimization 이 추가되었는데 일단 -O 와 비슷하다고 생각된다.

참고로 None과 Fast의 퍼포먼스 차이는 엄청나다. Swift 로 짠 코드가 느리다고 생각되면 릴리즈 모드로 빌드해서 시험해보자.

여기서는 Xcode 상에서 Swift Compiler Optimization 플래그를 바꿔가며 assert() 등의 함수류가 어떻게 반응하는지 시험해 보자. 간단히 OS X 용 앱 프로젝트를 생성해서 특정 뷰컨트롤러 상에서 구현했다. 코드는 아래와 같다.
import Cocoa

func someFunc(value: Bool) -> Bool {
  print("someFunc with \(value)")
  return value
}

class ViewController: NSViewController {

  override func viewDidLoad() {
    super.viewDidLoad()

    assert(someFunc(false))
  }
}
someFunc() 함수가 Assertion 체크의 대상인데, 이 함수 안에서 로그를 찍는(print) 이유는 이 함수가 제대로 실행되는가를 파악하기 위함이다.

이제 플래그를 세팅해서 시험해보자.

None 최적화(-Onone)일 경우 assert() 는 아래와 같이 동작했다.
assert() 에 전달된 인자가 실행되었음을 확인 할 수 있고, someFunc가 false를 리턴했으며, 그래서 조건에 부적합한 상태가 되어 앱이 비정상 종료되었으며 문제가 생긴 위치를 확실하게 알 수 있다.

None 최적화(-Onone)일 경우 precondition() 은 아래와 같이 동작했다.
precondition() 에 전달된 인자가 실행되었음을 확인 할 수 있고 그래서 앱이 비정상 종료되었으며 문제가 생긴 위치 역시 동일하게 알 수 있다.

결론적으로 None 최적화, 즉 디버그 모드일 경우 assert() 와 precondition() 은 동일한 동작을 한다.

Fast 최적화(-O)일 경우 assert() 는 아래와 같이 동작했다.
'이거 실행시킨거 맞냐' 라는 생각이 들 정도로 그냥 깨끗한 화면이다. 로그 창에서 someFunc() 안에서 찍는 로그가 보이지 않는다. 결과적으로 릴리즈용으로 빌드하게 되면 assert() 자체와 전달되는 조건식이 아예 생략되는 것이다.

Fast 최적화(-O)일 경우 precondition() 은 아래와 같이 동작했다.
precondition() 의 경우 None 최적화 때와 동일하게 비정상 종료시켜 버린다. assert() 와는 다르게 디버그든 릴리즈든 무조건 실행해서 결과에 따라 앱을 비정상 종료시켜 버린다.

정리

assert() 와 assertionFailure() 함수는 디버그 모드(-Onone)에서만 동작(Evaluation)한다. 즉 assert() 와 assertionFailure() 는 릴리즈 모드(-Ofast)에서는 아무런 역활을 하지 않는다.

precondition() 과 preconditionFailure() 함수는 디버그(-Onone)나 릴리즈(-Ofast) 모드를 가리지 않고 항상 체크한다.

따라서 assert() 의 경우 개발 과정에서 마구 쓰더라도 실제 프로덕션 단계의 앱의 성능에는 영향을 끼치지 않으니 걱정할 필요가 없다라고 결론 낼 수 있다. 제일 처음에 적었던 내 걱정은 기우였던 것이다.

사소한 것이지만, 앱을 실제로 릴리즈 하는 과정이 순탄하도록 이 둘을 적절히 잘 써야겠다는 생각이 든다.

[관련글] 스위프트(Swift) 가이드
[참고글] Andy Bargh - Swift Assertions

댓글 3개 :

익명 :

잘읽었습니다. precondition 은 실제 어떤 상황에 적용해야 하나요?
예외처리 안된 상태에서 죽는거나 아짜피 같은 상황이라 명확히 어디인지 알 수 있기 때문 인가요?

jusung :

좋은 정보 감사합니다.!

Seorenn :

Unknown: 네. 언급하신대로 릴리즈로 빌드된 상황에서는 예외상황 발생으로 죽는 경우는 위치를 정확히 알 수 없는 경우가 많습니다만 precondition의 경우는 위치와 함께 메시지도 확인 가능하다는 차이가 있겠네요. precondition의 원형을 보시면 아시겠지만 파일과 라인 정보가 함께 전달되기 때문에 릴리즈로 빌드되어도 이런 정보 확인이 가능합니다.

jusung: 감사합니다.