C 및 C++ 프로그래밍에서 assert
는 개발 중에 예상치 못했거나 유효하지 않은 런타임 조건을 탐지하고 진단하는 데 도움이 됩니다. 이러한 조건은 포인터가 null이 아닌지, 제수가 0이 아닌지, 함수가 반복적으로 실행되지는 않는지 등 코드가 필요로 하는 중요한 가정을 확인하지만, 매번 확인하는 것은 비효율적일 수 있습니다. 일부 사례에서 assert
는 향후 틱에서 필요하게 될 오브젝트를 삭제하는 등 실제 크래시가 발생하기 전에 딜레이된 크래시를 유발하는 버그를 발견하고 최종적인 크래시의 근본 원인을 파악하여 개발자를 돕기도 합니다. assert
의 주요 특징은 출시 코드에는 존재하지 않는다는 것입니다. 즉 출시된 제품에는 퍼포먼스상의 영향을 미치지 않지만, 추가 이펙트도 없습니다. assert
가 무엇인지 가장 쉽게 이해하는 방법은, '어서트'되는 것은 무엇이든 true여야 하며, 그렇지 않으면 프로그램이 멈춘다고 생각하는 것입니다.
언리얼 엔진은 assert
와 동일한 3가지 계열 check
, verify
, ensure
를 제공합니다. 이러한 기능의 코드를 검사하고 싶다면 Engine/Source/Runtime/Core/Public/Misc/AssertionMacros.h
에서 관련 매크로를 찾을 수 있습니다. 각 계열은 약간 다르게 작동하지만, 개발 과정에서 진단 툴이라는 일반적인 역할을 한다는 것은 동일합니다.
Check
Check 계열은 첫 번째 파라미터가 false 값으로 평가되었을 때 이 계열의 멤버가 실행을 정지하며, 기본적으로 출시 빌드에서 실행되지 않는다는 점에서 베이스 assert
와 가장 비슷합니다. 다음과 같은 Check 매크로를 사용할 수 있습니다.
매크로 | 파라미터 | 동작 |
---|---|---|
check 또는 checkSlow |
Expression |
Expression 이 false인 경우 실행 정지됩니다. |
checkf 또는 checkfSlow |
Expression , FormattedText , ... |
Expression 이 false인 경우 실행 정지되고 로그에 FormattedText 를 출력합니다. |
checkCode |
Code |
한 번 실행되는 do-while 루프 구조 내에서 Code 를 실행합니다. 주로 다른 Check에 필요한 정보를 준비할 때 유용합니다. |
checkNoEntry |
(없음) | check(false) 와 비슷하게 라인에 히트하면 실행을 정지하지만, 도달할 수 없는 코드 경로에 사용합니다. |
checkNoReentry |
(없음) | 라인에 두 번 이상 히트하면 실행을 정지합니다. |
checkNoRecursion |
(없음) | 범위 내에서 라인에 두 번 이상 히트하면 실행을 정지합니다. |
unimplemented |
(없음) | check(false) 와 비슷하게 라인에 히트하면 실행을 정지하지만, 오버라이드되어야 하며 호출되지 않는 가상 함수에 사용합니다. |
Check 매크로는 디버그, 개발, 테스트, 출시 에디터 빌드에서 작동하지만, 'Slow'로 끝나는 매크로는 예외적으로 디버그 빌드에서만 작동합니다. USE_CHECKS_IN_SHIPPING
이 true 값(일반적으로 1
)을 가지도록 정의하면 Check 매크로가 모든 빌드에서 작동합니다. Check 매크로 내 코드가 값을 변경하는 것이 의심되거나, 추적이 어려운 출시 전용 버그가 있으며 기존 Check 매크로에 확인될 것으로 의심되는 경우 유용합니다. 프로젝트는 USE_CHECKS_IN_SHIPPING
을 디폴트값인 0
으로 설정하여 출시되어야 합니다.
Verify
Verify 계열은 대부분의 빌드에서 Check 계열과 똑같이 작동합니다. 하지만 Verify 매크로는 Check 매크로가 비활성화된 빌드에서도 표현식을 평가합니다. 이는 표현식이 진단 확인과 관계없이 실행되어야 하는 경우에만 Verify 매크로를 사용해야 한다는 것을 뜻합니다. 예를 들어, 액션을 수행한 후 해당 액션의 성공 또는 실패 여부를 표시하는 bool
을 반환하는 함수가 있다면, Check보다는 Verify를 사용하여 해당 액션이 성공적이었는지 확인해야 합니다. 출시 빌드에서 Verify는 반환 값을 무시하고 액션을 계속 수행하기 때문입니다. 하지만 Check는 단순히 모든 출시 빌드에서 함수를 아예 호출하지 않으므로 결과적으로 다른 동작이 일어납니다.
매크로 | 파라미터 | 동작 |
---|---|---|
verify 또는 verifySlow |
Expression |
Expression` 이 false인 경우 실행을 정지합니다. |
verifyf or verifyfSlow |
Expression , FormattedText , ... |
Expression 이 false인 경우 실행을 정지하고 로그에 FormattedText 를 출력합니다. |
Verify 매크로는 디버그, 개발, 테스트, 출시 에디터 빌드에서 모두 작동하지만, 'Slow'로 끝나는 매크로는 예외적으로 디버그 빌드에서만 작동합니다. USE_CHECKS_IN_SHIPPING
이 true 값을 갖도록 정의하면(일반적으로 1
) 이 동작을 오버라이드합니다. 그 외의 모든 경우 Verify 매크로는 표현식을 평가하지만, 실행을 정지하거나 로그에 텍스트를 출력하지 않습니다.
Ensure
Ensure 계열은 Verify 계열과 비슷하지만, 치명적이지 않은 오류에 사용합니다. 즉, Ensure 매크로의 표현식이 false로 평가되는 경우 엔진이 크래시 리포터에 알리지만 실행은 계속됩니다. Ensure 매크로는 크래시 리포터 플러드를 피하기 위해 엔진 또는 에디터 세션당 한 번만 보고합니다. 표현식이 false로 평가될 때마다 Ensure 매크로의 리포트가 필요한 경우 매크로의 'Always' 버전을 사용합니다.
매크로 | 파라미터 | 동작 |
---|---|---|
ensure |
Expression |
Expression 이 처음으로 false인 경우 크래시 리포터에 알립니다. |
ensureMsgf |
Expression , FormattedText , ... |
Expression 이 처음으로 false인 경우 크래시 리포터에 알리고 로그에 FormattedText 를 출력합니다. |
ensureAlways |
Expression |
Expression 이 false인 경우 크래시 리포터에 알립니다. |
ensureAlwaysMsgf |
Expression , FormattedText , ... |
Expression 이 false인 경우 크래시 리포터에 알리고 로그에 FormattedText 를 출력합니다. |
Ensure 매크로는 모든 빌드에서 표현식을 평가하지만, 디버그, 개발, 테스트, 출시 에디터 빌드에서만 크래시 리포터에 알립니다.
사용 예시
다음 가설은 Check, Verify, Ensure을 통해 코드를 명확히 하거나 디버깅에 도움을 받을 수 있는 사용 사례를 보여줍니다.
// 이 함수는 null JumpTarget으로 호출되어서는 안 됩니다. 그러면 프로그램이 정지됩니다.
void AMyActor::CalculateJumpVelocity(AActor* JumpTarget, FVector& JumpVelocity)
{
check(JumpTarget != nullptr);
// (JumpTarget에 착지 시 필요한 속도를 계산합니다. 이제 JumpTarget이 null이 아닌 것을 확신할 수 있습니다.)
}
// 메시 값을 설정하고 그 값이 null이 아닐 것으로 예상합니다. 그 후 메시가 null이면 프로그램을 정지합니다.
// 표현식에 추가 이펙트(메시 세팅)가 있으므로 Check 대신 Verify를 사용합니다.
verify((Mesh = GetRenderMesh()) != nullptr);
// 이 코드 줄은 제품의 출시 버전에서 발생할 수 있는 사소한 오류를 파악합니다.
// 오류가 실행을 정지하지 않아도 처리할 수 있을 정도로 충분히 사소합니다.
// 버그를 수정했다고 생각할 수 있지만, 그래도 버그가 발생하는지 확인하고자 합니다.
void AMyActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// 진행하기 전 bWasInitialized가 true인지 확인합니다. false인 경우 버그가 아직 수정되지 않았다고 로그에 기록합니다.
if (ensureMsgf(bWasInitialized, TEXT("%s ran Tick() with bWasInitialized == false"), *GetActorLabel()))
{
// (제대로 초기화된 AMyActor가 필요한 어떤 작업을 수행합니다.)
}
}
// 이 코드는 새 셰이프 타입을 추가했지만 이 스위치 블록에서 처리하는 것을 잊은 경우 정지됩니다.
switch (MyShape)
{
case EShapes::S_Circle:
// (원을 처리합니다.)
break;
case EShapes::S_Square:
// (정사각형을 처리합니다.)
break;
default:
// 모든 셰이프 타입에 대한 case여야 하므로, 일어나서는 안 됩니다.
checkNoEntry();
break;
}
// 이 UObject는 추가 이펙트가 없는 테스트 함수인 IsEverythingOK를 가지고 있지만, 문제가 있는 경우 false를 반환합니다.
// 이러한 경우가 발생하면 치명적인 오류로 중단됩니다.
// 코드에 추가 이펙트가 없으며 진단 목적만 수행하기 때문에, 출시 빌드에서 실행할 필요가 없습니다.
checkCode(
if (!IsEverythingOK())
{
UE_LOG(LogUObjectGlobals, Fatal, TEXT("Something is wrong with %s! Terminating."), *GetFullName());
}
);
// 이 목록에는 순환이 없으며, 순환이 있는 경우 프로그램이 멈춥니다. 하지만 순환 확인으로 인해 느려질 수 있으므로 디버그 빌드에서만 순환을 확인하고자 합니다.
checkfSlow(!MyLinkedList.HasCycle(), TEXT("Found a cycle in the list!"));
// (목록을 살펴보며 각 요소의 일부 코드를 실행합니다.)