Assertions & Preconditions
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 Assertions과 Preconditions이라는 개념과 실제 사용되는 메서드들을 살펴볼까 합니다 🙋🏻
우선 Assertions와 Preconditions가 어떤 개념인지부터 알아보면 좋을것 같네요!
Assertions & Preconditions
두가지 모두 런타임에서 사용할 수 있는 조건 검사입니다.
추가 코드를 실행하기 전에 해당 필수 조건이 충족되는지 확인하는데 사용하죠.
조건은 Bool 타입으로 결국 true이면 다음 코드를 이상없이 수행하고 만약 false이면 더 이상 코드를 실행하지 않고 앱을 죽여버립니다!
런타임 에러가 발생하죠!
즉, 강제로 앱을 꺼버릴 수 있다는 특징이자 단점이 될 수 있는 부분이 핵심적이라고 보입니다 🤔
그럼으로 사실 치명적이지 않은 에러들 (에러 핸들링을 할 수 있는 부분들)에서는 거의 사용되지 않는것이 좋을 수 있어보입니다.
앱을 강제로 터트려버리니까요 🥲
다만, 이것들을 사용하면 반대로 생각해보면 확실히 앱이 더 예측 가능하게 종료되기에 문제 상황을 더 쉽게 디버깅 할 수 있습니다.
그럼으로 런타임 에러가 나지 않아 에러인지 뒤늦게 알거나 모르는 상황이 애초에 없기도 하기에, 실행을 그 즉시 중지 시켜버리는 이러한 기능들이 어떻게 보면 더 큰 피해를 막을 수도 있다고 생각합니다.
물론 정답은 없기에, 정말 프로덕트마다 상황마다 사용의 필요성은 다르겠지만요 🙋🏻
그럼, 이 Assertions과 Preconditions는 어떤 차이가 있을까요?
바로 검사를 하는 시점 차이로 볼 수 있습니다.
Assertions은 디버그 빌드에서만 검사하지만 Preconditions은 디버그와 프로덕션 빌드 모두에서 검사합니다.
즉, 프로덕션 빌드라면 Assertions 내부 조건 자체를 검사하지 않습니다.
결국 Assertions을 사용하면 배포될 프로덕션 빌드 성능에는 영향을 주지 않고 디버그 개발 빌드 단에서 편하게 사용하면서 에러들을 더 쉽게 파악할 수 있겠죠.
사실 그렇다면 Preconditions을 사용하지 않는것이 제일 좋은거 아니냐고 생각이 들 수도 있습니다.
디버그 개발 단계에서 모든것을 다 체크하고 빠짐없이 확인한다는 가정에서는 좋을 수 있겠지만, 만약 그렇지 않고 배포되어 실 사용중인데 특정한 상황에서 꼭 예기치 않더라도 앱을 죽여야된다 하면 사용될 수 도 있겠죠.
상황 상 다르겠지만, 보완을 위해서 불법적인 접근이 일어나거나 한다면 프로덕션 빌드더라도 검사해서 앱을 죽이는게 더 안전할 수 있을것 같아요!
그럼, 이렇게 Assertions과 Preconditions에 대해 알아봤으니 실제 이 테스팅을 위해 어떤 메서드들이 있는지 확인해보겠습니다!
먼저, Assertions에 관련된 메서드들부터 보시죠.
assert(_:_:file:line:)
해당 메서드는 아래와 같이 정의되어 있어요.
func assert(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String = String(),
file: StaticString = #file,
line: UInt = #line
)
선택적 메시지를 사용하여 전통적인 C 스타일의 Assertions을 수행해주는 메서드입니다.
실제로 이렇게 사용을 해볼 수 있습니다.
let age = -3
assert(age >= 0, "A person's age can't be less than zero.")
// This assertion fails because -3 isn't >= 0.
예시를 보면 assert 메서드를 통해 조건을 검사하고 있습니다.
age가 0보다 크거나 같다면 true를 반환하고 작으면 false를 반환하겠죠.
즉, false라면 더 이상의 코드 실행은 없고 담아준 메시지와 현재 실패한 코드가 있는 파일 정보 그리고 호출된 코드 라인 수의 정보들을 제공해줘서 알 수 있게 합니다.
앞서 말한적이 있는데, assert 메서드는 다음과 같은 다양한 빌드 설정에서 다르게 동작합니다.
1️⃣ playgrounds와 -Onone 빌드 (Xcode 디버그 설정의 기본 값) - false로 판별나면 메시지를 출력한 후 프로그램 실행을 디버거에서 중지
2️⃣ -O 빌드 (Xcode 릴리즈 설정의 기본 값) - 조건 검사를 하지 않습니다.
3️⃣ -Ounchecked 빌드 - 조건 검사를 하지는 않지만, 항상 true로 검사 된다고 가정합니다.
이 가정 자체를 만족하지 못하면 심각한 프로그래밍 오류가 발생할 수 있다고 하네요!
결국 해당 assert 메서드는 코드 안정성을 검증하는데 매우 중요하며, 특히 테스트 단계에서 버그를 발견해 예상치 못한 문제를 조기에 감지하는데 도움을 줄 수 있겠죠? 😀
다음으로 assertFailure라는 메서드를 볼까요?
assertionFailure(_:file:line:)
해당 메서드는 assert 메서드와 하는 역할은 갖지만 사용 방법이 다릅니다.
정의는 이렇습니다.
func assertionFailure(
_ message: @autoclosure () -> String = String(),
file: StaticString = #file,
line: UInt = #line
)
보시면 condition 파라미터가 없습니다.
즉 코드가 이미 조건을 확인하여 false인 상황에서 바로 호출하여 사용할 수 있는것이죠!
아래와 같은 예시 코드를 보겠습니다.
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 {
assertionFailure("A person's age can't be less than zero.")
}
else로 떨어지면 항상 false로 들어올테니 여기서는 assert 메서드로 다시 조건을 주는것보다 assertionFailure를 사용하면 더 간편합니다.
이와 같이 이미 내부 로직 상 조건 검사를 assert에서 하지 않는 상황에서 사용할 수 있습니다!
마찬가지로 assert와 동일하게 다양한 빌드 설정에서 다르게 동작합니다. (위 내용과 똑같음!)
그럼 다음으로, Preconditions에 관한 메서드를 살펴볼까요?
먼저, precondition 메서드입니다.
precondition(_:_:file:line:)
해당 메서드는 앞으로의 코드 진행을 위해 필요한 조건을 확인합니다.
정의를 볼까요?
func precondition(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String = String(),
file: StaticString = #file,
line: UInt = #line
)
assert 메서드와 동일하죠 사실?
동작도 동일합니다.
다만 차이나는건 딱 빌드 설정 시 동작 부분일텐데요.
다른건 같지만, -O 빌드 즉, Xcode 릴리즈 설정의 기본값일때 assert와 달리 false면 실행을 중지하죠.
그게 다입니다ㅎ..
그럼 이어서 비슷하게 뭐가 나올지 이제 유추가 되지 않나요!?
assertionFailure가 있다면 쌍으로 당연히 preconditionFailure도 있어야겠죠!?
preconditionFailure(_:file:line:)
정의부터 봐볼께요.
func preconditionFailure(
_ message: @autoclosure () -> String = String(),
file: StaticString = #file,
line: UInt = #line
) -> Never
이제 익숙하시죠!?
마찬가지로 conditions 파라미터가 없기에 어떤 로직상 이미 조건 검사를 마치고 false인 상황일때 사용합니다.
주로 옵셔널 바인딩과 같은 상황에서도 else 구문에 넣어서 사용할 수도 있겠네요!
그런데 특이하게 Never를 반환값으로 던져주고 있습니다.
아마 Swift에서 컴파일러가 더 정확한 최적화를 수행할 수 있게 함이 목적이지 않을까 싶습니다.
반환 타입이 Never인 메서드 후에 있는 코드는 실행되지 않기에 말이죠!
assertionFailure는 프로덕션 빌드에서는 다음 코드를 실행하거나 해야하니 Never를 사용하지 않나 싶긴 합니다.
자 마지막으로 하나 생각해볼게 있어요!
만약 -Ounchecked 모드로 컴파일하면 항상 true로 가정하고 코드를 그에 맞게 최적화한다고 했잖아요?
근데 사실 false였다면 치명적인 결함이 생길수도 있고..!
그렇다면 fatalError를 사용해봐도 방법이라곤 보입니다.
해당 fatalError는 최적화 설정에 관계없이 항상 실행을 중단해버리니까요.
해당 메서드는 프로토타이핑 및 초기 개발 단계에서 아직 미구현된 기능에 대해 써볼 수 있을것 같아요.
마무리
결국 여러 검사와 에러를 처리하는 방법들이 있겠지만, 적재적소에 맞게 사용하는것이 가장 고민일거라 생각드는 하루였습니다 🧐
레퍼런스