본문으로 바로가기

런타임 어설션에 대한 생각(feat. 언리얼 check 매크로)

배경

언리얼 엔진코드에는 check매크로 등 런타임 어설션이 많다. check매크로를 발견하면 check매크로의 내용은 참이라는 안도감을 가지고 코드를 편하게 읽을 수 있었다. 하지만 개발을 진행하던 중 다른 사람이 무분별하게 걸어둔 check 매크로에 걸려서 클라이언트가 종료되는 경험도 종종 하다 보면 굳이 여기에 check를 둬야하나? 라는 의문이 들 때도 있었다. 그래서 한 동안 런타임 어설션에 대한 고민을 많이하고 검색도 하면서 "런타임 어설션은 왜 쓸까?" 그리고 "if문과 check 매크로는 어떤 차이점이 있는가?", "언제 쓰는게 바람직할까?" 라는 내용들을 정리해 보았다. 

런타임 어설션의 의미와 목적

런타임 어설션에 대한 사용 목적을 찾아보다가 가장 인상 깊었던 표현이 "런타임 어설션은 Document 이다." 였다. Assert를 직역하면 "주장"이다. 코드를 작성한 이가 읽는이 에게 책임지고 상황을 주장하는 코드가 된다. 주석도 비슷한 기능을 하지만 주석의 책임성은 낮다. 주석은 코드가 수정될 때 함께 항상 수정된다는 보장을 할 수 없다. 그래서 주석을 읽는 사람은 주석에 오류가 존재할 수 있음을 전재하고 봐야 한다. 하지만 어설션은 오류가 발생하면 프로그램이 중단되기 때문에 상대적으로 더 신뢰할 수 있다.

예외 처리와 런타임 어설션의 차이

어설션은 반드시 발생하지 않을 상황에 사용해야 한다. 반면 if문은 발생할 수 있는 예외상황에 사용해야 한다.

예외가 발생하지 않을 상황 예시

예를 들어 구조상 부모 노드가 존재할 경우에만 자식 노드가 존재하도록 설계되어 있는 상황이 있을 수 있다. 이제 코드를 처음 보는 사람(혹은 먼 미래의 자신)의 입장에서 자식노드에서 부모노드를 접근할 때 부모 노드가 null인지 아닌지 신경을 써야할 수도 있다. 하지만 이 구조를 설계한 사람이 자식 노드에서 부모 노드를 접근할때 런타임 어설션을 달아두면 코드를 읽는 사람은 불필요하게 예외상황을 신경 쓸 필요가 없게 된다. 만약 어설션이 없었다면 코드를 읽는 사람이 부모 노드와 자식노드의 구조를 다 파악해야만 안전한 코드라는 것을 인지할 수 있었을 것이다.

void ChildNode::Func()
{
    assert(Parent) // Parent 포인터는 항상 null이 아닙니다!
    Parent->GetSlot();
    // ....
}

예외가 발생할 수 있는 상황 예시

예외처리가 꼭 필요한 상황으로 대표적인 경우가 데이터를 외부에서 읽어 올 때이다. 외부에서 유효한 데이터가 들어오리라 기대는 하지만, 항상 유효하다는 보장을 할 수는 없다. 외부 데이터의 오류가 프로그램을 진행시키기 어려운 수준이 아니라면 오류가 있을 때마다 프로그램을 정지시켜버리는 것보다는 예외처리를 통해서 에러 로그를 보여주거나, 필요한 처리를 진행하는 것이 더 바람직할 수 있다. (간단한 프로그램일 경우 차이를 못 느낄 수 있지만, 필자의 경우 클라이언트를 재실행하는데만 약 20분가량이 걸린다. 결코 적은 비용은 아니다.)

void ReadData(const Data& InData)
{
    if(IsValid(InData))
    {
        // Do Someting...
    }
    else
    {
        // Logging...
    }
}

런타임 어설션의 디버깅 기능

런타임 어설션은 문제가 발생한 시점에 프로그램을 정지시켜 디버깅을 더 빠르게 진행시켜준다. 심각한 문제라면 if문으로 적당히 예외처리하고 그냥 지나갔다가 엉뚱한 곳에서 크래시가 발생하게 되는 것 보다 문제가 발생된 시점에서 프로그램을 중단시키고 콜스택을 분석하는게 디버깅이 더 수월할 수 있다.

정리

  • 런타임 어설션은 문서와 같은 기능을 한다. 작성한 사람이 읽는이에게 책임지고 정보를 제공하는 기능이다.
  • 런타임 어설션은 반드시 참인 경우에만 사용한다. 참이 아닐 수도 있다면 예외처리를 고려하자.
  • 심각한 문제라면 발생한 지점에서 어설션을 내는 것이 디버깅에 도움이 될 수 있다.