🔥 Assertions과 Preconditions

682자
10분

프로그래밍을 하다 보면 런타임에 특정 조건이 만족되는지 확인해야 할 때가 있습니다. 이럴 때 우리는 AssertionPrecondition을 사용할 수 있죠.

Assertion과 Precondition은 어떤 필수 조건이 만족되었는지 검사한 후에 그 다음 코드를 실행합니다. 만약 해당 조건이 true로 평가되면 코드 실행은 평소처럼 계속되고, false로 평가되면 현재 프로그램의 상태는 유효하지 않은 것으로 간주되어 코드 실행이 중단되고 앱이 종료됩니다.

우리는 코딩하면서 가정하고 기대하는 바를 표현하기 위해 assertion과 precondition을 사용합니다. 그래서 이들을 코드의 일부로 포함시킬 수 있죠. Assertion은 개발 중에 실수나 잘못된 가정을 찾는 데 도움이 되고, precondition은 프로덕션에서 이슈를 탐지하는 데 유용합니다.

런타임에 우리의 기대를 검증하는 것 외에도, assertion과 precondition은 코드 내에서 유용한 문서화 형태가 됩니다. 앞서 Error Handling 에서 논의한 에러 조건과는 달리, assertion과 precondition은 복구 가능하거나 예상된 에러에는 사용되지 않습니다. Assertion이나 precondition이 실패했다는 것은 프로그램의 상태가 유효하지 않음을 나타내기 때문에, 실패한 assertion을 잡을 방법이 없습니다. 유효하지 않은 상태에서 복구하는 것은 불가능하죠.

Assertion이 실패하면, 프로그램 데이터의 적어도 한 부분은 유효하지 않습니다. 그런데 왜 유효하지 않은지, 또는 추가적인 상태도 유효하지 않은지는 알 수 없습니다.

Assertion과 precondition을 사용하는 것이 유효하지 않은 조건이 발생할 가능성이 낮도록 코드를 설계하는 것을 대신할 순 없습니다. 하지만 이들을 사용해 유효한 데이터와 상태를 강제하면, 유효하지 않은 상태가 발생했을 때 앱이 더 예측 가능하게 종료되고 문제를 더 쉽게 디버깅할 수 있습니다. 가정이 검사되지 않으면, 이런 문제를 훨씬 나중에야 알아챌 수 있습니다. 다른 곳의 코드가 눈에 띄게 실패하기 시작하고, 사용자 데이터가 조용히 손상된 후에야 말이죠. 유효하지 않은 상태가 감지되는 즉시 실행을 중지하면 해당 상태로 인한 피해를 제한하는 데에도 도움이 됩니다.

Assertion과 precondition의 차이점은 검사되는 시점입니다. Assertion은 디버그 빌드에서만 검사되지만, precondition은 디버그와 프로덕션 빌드 모두에서 검사됩니다. 프로덕션 빌드에서는 assertion 내부의 조건이 평가되지 않습니다. 이는 개발 과정에서 원하는 만큼 assertion을 사용할 수 있지만, 프로덕션에서는 성능에 영향을 미치지 않는다는 의미입니다.

Debugging with Assertions

Assertion은 Swift 표준 라이브러리의 assert(_:_:file:line:) 함수를 호출하여 작성합니다. 이 함수에는 true 또는 false로 평가되는 표현식과, 조건의 결과가 false일 때 표시할 메시지를 전달하죠. 예를 들면:

let age = -3
// age의 값이 0 이상인지 검사하는 assertion
// age가 음수이면 주어진 메시지를 출력하고 앱을 종료
assert(age >= 0, "A person's age can't be less than zero.")
swift

이 예제에서, age >= 0true로 평가되면, 즉 age의 값이 음수가 아니면 코드 실행이 계속됩니다. 위의 코드처럼 age의 값이 음수이면 age >= 0false로 평가되고, assertion은 실패하여 애플리케이션을 종료시키게 됩니다.

Assertion 메시지는 생략할 수도 있습니다. 예를 들어, 메시지가 조건을 그대로 서술하는 경우라면요.

assert(age >= 0)
swift

만약 코드에서 이미 조건을 체크했다면, assertionFailure(_:file:line:) 함수를 사용하여 assertion이 실패했음을 나타낼 수 있습니다. 예를 들면:

if age > 10 {
    print("You can ride the roller-coaster or the ferris wheel.")
} else if age >= 0 {
    print("You can ride the ferris wheel.")
} else {
    // age가 음수이면 바로 assertion failure를 호출하여 앱 종료
    assertionFailure("A person's age can't be less than zero.")
}
swift

Enforcing Preconditions

어떤 조건이 거짓(false)일 수도 있지만, 코드 실행을 계속하기 위해서는 반드시 참(true)이어야 하는 경우에 precondition을 사용하세요. 예를 들어, 서브스크립트가 범위를 벗어나지 않는지 확인하거나, 함수에 유효한 값이 전달되었는지 확인하는 데 precondition을 사용할 수 있죠.

Precondition은 precondition(_:_:file:line:) 함수를 호출하여 작성합니다. 이 함수에는 true 또는 false로 평가되는 표현식과, 조건의 결과가 false일 때 표시할 메시지를 전달합니다. 예를 들면:

// 서브스크립트 구현부에서...
precondition(index > 0, "Index must be greater than zero.")
swift

또한 preconditionFailure(_:file:line:) 함수를 호출하여 실패가 발생했음을 나타낼 수도 있습니다. 예를 들어, switch문의 default case가 실행되었는데, 모든 유효한 입력 데이터가 다른 case에서 처리되어야 하는 경우에 사용할 수 있죠.

주의할 점은, unchecked 모드(-Ounchecked)로 컴파일하면 precondition이 검사되지 않는다는 것입니다. 컴파일러는 precondition이 항상 참이라고 가정하고, 이에 따라 코드를 최적화합니다. 하지만 fatalError(_:file:line:) 함수는 최적화 설정에 관계없이 항상 실행을 중단시킵니다.

프로토타이핑이나 초기 개발 중에는 fatalError(_:file:line:) 함수를 사용하여 아직 구현되지 않은 기능에 대한 스텁을 만들 수 있습니다. fatalError("Unimplemented")를 스텁 구현으로 작성하면 되죠. Fatal error는 assertion이나 precondition과 달리 최적화로 제거되지 않기 때문에, 스텁 구현을 만나면 실행이 항상 중단된다는 것을 확신할 수 있습니다.

정리하자면, Assertion과 precondition은 프로그램의 정확성을 보장하기 위한 강력한 도구입니다. 이들은 개발 과정에서 버그를 조기에 발견하고, 프로덕션에서 잠재적인 문제를 방지하는 데 도움을 줍니다.

앱의 안정성과 신뢰성을 높이려면 적극적으로 활용해 보시기 바랍니다. 하지만 과도한 사용은 오히려 코드를 복잡하게 만들 수 있으니, 적절한 수준에서 사용하는 것이 중요하겠죠?

참고로 assertion과 precondition의 차이를 한 줄로 정리하면 다음과 같습니다.

Assertion은 디버깅용, Precondition은 프로덕션용!

이 말만 기억하셔도 두 개념을 구분하는 데 큰 도움이 될 거예요. 🌞