LearnCPP - 9

Justin·2026년 2월 17일

LearnCPP.com

목록 보기
9/22

코드 커버리지 (Code coverage)

  • 코드 커버리지라는 용어는 테스트를 진행하는 동안 프로그램의 소스 코드가 얼마나 실행되었는지를 나타낼 때 사용합니다.
  • 코드 커버리지를 측정하는 지표(Metric)는 매우 다양해요.

구문 커버리지 (Statement coverage)

  • 구문 커버리지는 작성하신 테스트 루틴에 의해 실행된 코드 내 구문의 비율을 의미합니다.
  • 다음 함수를 살펴볼까요?
int foo(int x, int y)
{
    int z{ y };
    if (x > y)
    {
        z = x;
    }
    return z;
}
  • 이 함수를 foo(1, 0)으로 호출하면 함수 내의 모든 구문이 빠짐없이 실행됩니다.
  • 함수에 대해 완벽한 구문 커버리지(100%)를 얻을 수 있습니다.
  • 다음으로 우리의 isLowerVowel() 함수를 살펴볼게요.
bool isLowerVowel(char c)
{
    switch (c) // 구문 1
    {
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
        return true; // 구문 2
    default:
        return false; // 구문 3
    }
}
  • 한 번의 함수 호출만으로는 구문 2와 구문 3에 동시에 도달할 수 없어요.
  • 따라서 모든 구문을 테스트하려면 이 함수를 두 번 호출해야 합니다.

분기 커버리지 (Branch coverage)

  • 분기 커버리지실행된 분기의 비율을 의미하며, 발생 가능한 각 분기를 개별적으로 계산합니다.
  • if 문에는 두 개의 분기가 있어요. 조건이 참일 때 실행되는 분기와, 거짓일 때 실행되는 분기입니다.
  • switch 문은 더 많은 분기를 가질 수 있습니다.

루프 커버리지 (Loop coverage)

  • 루프 커버리지는 비공식적으로 0 1 2 테스트라고도 불리는데요.
  • 코드에 루프(반복문)가 있다면 0번 1번 2번 반복될 때 제대로 작동하는지 확인해야 한다는 규칙입니다.
  • 만약 2번 반복되는 경우에 잘 작동한다면, 2보다 큰 모든 반복 횟수에 대해서도 올바르게 작동해야 하니까요.
  • 루프는 음수 번 실행될 수 없으므로, 이 세 가지 테스트면 모든 가능성을 충분히 다루게 됩니다.
#include <iostream>

void spam(int timesToPrint)
{
    for (int count{ 0 }; count < timesToPrint; ++count)
         std::cout << "Spam! ";
}
  • 이 함수 내의 루프를 제대로 테스트하려면 세 번 호출해 보아야 해요.
  • 0번 반복을 테스트하는 spam(0)
  • 1번 반복을 테스트하는 spam(1)
  • 2번 반복을 테스트하는 spam(2)
  • spam(2)가 제대로 작동한다면, n2보다 큰 spam(n)당연히 잘 작동할 거예요.

다양한 카테고리의 입력 테스트하기 (Testing different categories of input)

  • 매개변수를 받는 함수를 작성하거나 사용자의 입력을 받을 때는, 다양한 카테고리의 입력이 주어졌을 때 어떤 일이 일어날지 꼭 고려해야 합니다. 여기서 '카테고리'란 비슷한 특성을 가진 입력들의 집합을 의미해요.
  • 예를 들어, 제가 정수의 제곱근을 구하는 함수를 작성했다고 가정해 볼게요. 어떤 값으로 테스트하는 것이 타당할까요?
  • 아마 4와 같은 평범한 값으로 먼저 시작해 보겠죠. 하지만 여기서 그치지 않고 0이나 음수로도 테스트해 보는 것이 아주 좋은 생각입니다.
  • 카테고리 테스트를 위한 몇 가지 기본 가이드라인은 다음과 같아요.

정수(Integer)

  • 정수의 경우, 함수가 음수 0 양수를 어떻게 처리하는지 반드시 확인하세요.
  • 또한 상황에 따라 오버플로우 문제도 꼭 체크해야 합니다.

    부동 소수점 숫자(Floating point number)
  • 부동 소수점 숫자의 경우, 정밀도 문제를 가진 값들을 함수가 어떻게 처리하는지 고려해 보세요.
  • 테스트하기에 좋은 double 자료형 값으로는 예상보다 약간 큰 숫자를 테스트하기 위한 0.1-0.1
  • 그리고 예상보다 약간 작은 숫자를 테스트하기 위한 0.7-0.7이 있습니다.

    문자열(String)
  • 문자열의 경우, 함수가 빈 문자열 영숫자 문자열 공백이 포함된 문자열(앞, 뒤, 중간 포함)
  • 그리고 오직 공백으로만 이루어진 문자열을 어떻게 처리하는지 확인하세요.

    포인터(Pointer)
  • 만약 함수가 포인터를 취한다면, nullptr도 잊지 말고 꼭 테스트해 보세요.

C++에서 흔히 발생하는 의미론적 오류 (Common semantic errors in C++)

  • C++ 언어의 문법에 맞지 않는 코드를 작성했을 때 발생하는 구문 오류(Syntax errors)에 대해 다루었어요.
  • 컴파일러가 이러한 오류를 친절하게 알려주기 때문에 발견하기 쉽고 고치기도 아주 간단하답니다.
  • 우리가 의도한 대로 코드가 작동하지 않을 때 발생하는 의미론적 오류(Semantic errors)에 대해서도 배웠죠.
  • 똑똑한 컴파일러가 경고를 보내는 일부 경우를 제외하면, 보통 이런 의미론적 오류를 스스로 잡아내지 못해요.
  • 프로그램을 작성하다 보면 의미론적 오류를 저지르는 것은 거의 피할 수 없는 일입니다.
  • 하지만 여기에 도움이 되는 또 한 가지 방법이 있어요.
  • 바로 어떤 종류의 의미론적 오류가 가장 흔하게 발생하는지 미리 알아두는 것입니다.

조건부 논리 오류 (Conditional logic errors)

  • 가장 흔한 의미론적 오류 중 하나는 바로 조건부 논리 오류입니다.
  • 조건부 논리 오류는 프로그래머가 조건문이나 루프 조건의 논리를 잘못 코딩했을 때 발생해요.
  • 간단한 예를 들어볼게요.
#include <iostream>

int main()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;

    if (x >= 5) // 앗, operator> 대신 operator>=를 사용했네요
        std::cout << x << " is greater than 5\n";

    return 0;
}
  • 조건부 논리 오류가 나타나는 프로그램의 실행 결과는 다음과 같습니다.
  • 사용자가 5를 입력하면 조건식 x >= 5으로 평가되므로, 연결된 명령문이 실행되어 버립니다. 55보다 크지 않은데 말이죠.
Enter an integer: 5
5 is greater than 5
  • for 루프를 사용한 또 다른 예를 살펴볼까요?
#include <iostream>

int main()
{
    std::cout << "Enter an integer: ";
    int x{};
    std::cin >> x;

    // 앗, operator< 대신 operator>를 사용했네요
    for (int count{ 1 }; count > x; ++count)
    {
        std::cout << count << ' ';
    }

    std::cout << '\n';

    return 0;
}
  • 이 프로그램은 원래 1부터 사용자가 입력한 숫자 사이의 모든 숫자를 출력해야 해요.
  • 하지만 실제로는 이렇게 작동합니다.
Enter an integer: 5
  • 이런 일이 발생하는 이유는 for 루프에 진입할 때 count > x거짓이 되기 때문에 루프가 단 한 번도 반복되지 않기 때문이랍니다.

하나 차이 오류 (Off-by-one errors)

  • 하나 차이 오류는 루프가 의도한 것보다 딱 한 번 더 실행되거나 한 번 덜 실행될 때 발생하는 오류예요.
  • 프로그래머는 아래 코드가 1 2 3 4 5를 출력하기를 원했어요.
  • 하지만 잘못된 관계 연산자를 사용했기 때문에(<= 대신 < 사용), 루프가 의도한 것보다 한 번 덜 실행되어 1 2 3 4만 출력하게 됩니다.
#include <iostream>

int main()
{
    for (int count{ 1 }; count < 5; ++count)
    {
        std::cout << count << ' ';
    }

    std::cout << '\n';

    return 0;
}

잘못된 연산자 우선순위 (Incorrect operator precedence)

  • 6챕터 '논리 연산자'에서 가져온 다음 프로그램은 연산자 우선순위와 관련된 실수를 보여줍니다.
#include <iostream>

int main()
{
    int x{ 5 };
    int y{ 7 };

    if (!x > y) // 앗: 연산자 우선순위 문제가 발생했어요
        std::cout << x << " is not greater than " << y << '\n';
    else
        std::cout << x << " is greater than " << y << '\n';

    return 0;
}
  • 논리 부정 연산자(Logical NOT !)비교 연산자(>)보다 우선순위가 더 높기 때문에,
    이 조건문은 프로그래머의 의도와 다르게 (!x) > y처럼 평가된답니다.
  • 결과적으로 이 프로그램은 다음과 같이 출력합니다.
5 is greater than 7
  • 이런 문제는 동일한 표현식 안에서 논리 OR논리 AND를 섞어 쓸 때도 발생할 수 있어요.
    (논리 AND논리 OR보다 우선순위가 높습니다.)
  • 이런 종류의 오류를 피하려면 명시적으로 괄호를 사용하는 습관을 들이는 것이 아주 좋습니다.

부동 소수점 자료형의 정밀도 문제 (Precision issues with floating point types)

  • 다음 부동 소수점 변수는 전체 숫자를 저장할 만큼 충분한 정밀도를 가지고 있지 않아요.
#include <iostream>

int main()
{
    float f{ 0.123456789f };
    std::cout << f << '\n';

    return 0;
}
  • 이러한 정밀도 부족으로 인해 숫자가 다음과 같이 약간 반올림되어 버립니다.
0.123457
  • 6챕터에서, 부동 소수점 숫자에 == 연산자와 != 연산자를 사용하는 것이 미세한 반올림 오차로 인해 얼마나 문제의 소지가 있는지(그리고 어떻게 대처해야 하는지) 이야기했었죠. 다음이 그 예입니다.
#include <iostream>

int main()
{
    double d{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // 1.0이 되어야 정상이죠

    if (d == 1.0)
        std::cout << "equal\n";
    else
        std::cout << "not equal\n";

    return 0;
}
  • 이 프로그램은 다음과 같이 출력합니다.
not equal
  • 부동 소수점 숫자로 산술 연산을 많이 하면 할수록, 이런 작은 반올림 오차들이 점점 더 눈덩이처럼 누적되게 됩니다.

오류 탐지와 처리 (Detecting and handling errors)

  • 위에서 초보 C++ 프로그래머들이 언어를 다루면서 흔히 겪는 여러 유형의 의미론적 오류들을 살펴보았습니다.
  • 만약 오류가 언어의 기능을 잘못 사용했거나 논리적인 실수로 인해 발생한 것이라면,
    그 오류는 단순히 코드를 수정함으로써 바로잡을 수 있습니다.
  • 하지만 프로그램에서 발생하는 대부분의 오류는 언어의 기능을 무심코 잘못 사용해서 일어나는 것이 아닙니다.
  • 오히려 대부분의 오류는 프로그래머의 잘못된 가정이나 적절한 오류 탐지 및 처리의 부재 때문에 발생한답니다.
  • 이렇게 잘못된 가정이 흔히 발생하는 세 가지 주요 지점이 있습니다.
  • 함수가 반환될 때 호출된 함수가 실제로는 실패했는데도, 프로그래머는 성공했다고 가정할 수 있습니다.
  • 프로그램이 입력(사용자 또는 파일로부터)을 받을 때 입력값이 실제로는 잘못되었음에도,
    프로그래머는 입력 형식이 올바르고 의미상으로 유효하다고 가정할 수 있습니다.
  • 함수가 호출되었을 때 전달된 인수(Arguments)가 실제로는 유효하지 않은데도, 프로그래머는 의미상 유효할 것이라고 가정할 수 있습니다.
  • 많은 초보 프로그래머들은 코드를 작성한 후, 오류가 전혀 없는 정상적인 경로, 즉 해피 패스(Happy path)만 테스트하곤 합니다.
  • 하지만 여러분은 상황이 꼬이고 잘못될 수 있는 새드 패스(Sad paths)에 대해서도 반드시 계획하고 테스트해야 합니다.

함수에서 오류 처리하기 (Handling errors in functions)

  • 함수는 정말 다양한 이유로 실패할 수 있습니다.
  • 호출자(Caller)가 유효하지 않은 값을 인수로 전달했을 수도 있고, 함수 본문 내부에서 무언가 실패했을 수도 있죠.
  • 예를 들어, 읽기 모드로 파일을 여는 함수는 해당 파일을 찾을 수 없을 때 실패하게 됩니다.
  • 이런 일이 발생했을 때, 여러분이 마음대로 사용할 수 있는 꽤 많은 선택지가 있습니다.
  • 무조건 '이게 최고의 방법이다!' 하는 정답은 없어요.
  • 문제의 본질이 무엇인지, 그리고 그 문제를 고칠 수 있는지 없는지에 따라 크게 좌우되거든요.
  • 일반적으로 사용할 수 있는 4가지 전략은 다음과 같습니다.
  1. 함수 내부에서 오류 처리하기
  2. 호출자(Caller)에게 오류를 넘겨서 처리하게 하기
  3. 프로그램 중단하기
  4. 예외(Exception) 던지기

함수 내부에서 오류 처리하기 (Handling the error within the function)

  • 가능하다면 오류가 발생한 바로 그 함수 안에서 오류를 복구하는 것이 가장 좋은 전략입니다.
  • 그래야 함수 외부의 다른 코드에 영향을 주지 않고 오류를 격리하여 바로잡을 수 있기 때문이죠.
  • 여기에는 '성공할 때까지 재시도하기''실행 중인 작업 취소하기'라는 두 가지 선택지가 있습니다.
  • 만약 프로그램이 통제할 수 없는 외부 요인 때문에 오류가 발생했다면, 프로그램은 성공할 때까지 재시도를 할 수 있습니다.
  • 예를 들어, 프로그램에 인터넷 연결이 필요한데 사용자의 연결이 끊어졌다면, 프로그램은 경고 메시지를 띄운 뒤 반복문을 사용해 주 기적으로 인터넷 연결 상태를 다시 확인할 수 있습니다.
  • 또 다른 예로, 사용자가 잘못된 값을 입력했다면, 프로그램은 사용자에게 다시 시도해 달라고 요청하고 유효한 값을 입력할 때까지 - - 반복할 수 있습니다. 잘못된 입력을 처리하고 반복문을 사용해 재시도하는 예제는 후술에서 더 자세히 보여드리도록 하겠습니다.
  • 또 다른 전략은 오류를 그냥 무시하거나 작업을 취소하는 것입니다. 다음 코드를 볼까요?
// y가 0일 경우, 아무런 경고 없이 조용히 실패(종료)합니다.
void printIntDivision(int x, int y)
{
    if (y != 0)
        std::cout << x / y;
}
  • 위의 예제에서 사용자가 y에 유효하지 않은 값 0을 전달하면, 우리는 나눗셈 결과를 출력해 달라는 요청을 그냥 무시해 버립니다.
  • 하지만 이 방식의 가장 큰 문제점은 호출자나 사용자가 무언가 잘못되었다는 사실을 알아챌 방법이 없다는 거예요.
  • 이런 경우에는 오류 메시지를 출력해 주는 것이 큰 도움이 될 수 있습니다.
void printIntDivision(int x, int y)
{
    if (y != 0)
        std::cout << x / y;
    else
        std::cout << "Error: Could not divide by zero\n"; // 오류: 0으로 나눌 수 없습니다.
}
  • 하지만, 호출하는 함수 측에서 호출된 함수가 어떤 반환값을 주거나 유용한 부수 효과를 일으킬 것으로 기대하고 있다면, 단순히 오류를 무시하는 것은 올바른 선택지가 될 수 없습니다.

호출자에게 오류 넘기기 (Passing errors back to the caller)

  • 오류를 발견한 함수 안에서 오류를 적절히 처리할 수 없는 경우도 아주 많습니다.
  • 예를 들어, 다음 함수를 한 번 살펴볼까요?
int doIntDivision(int x, int y)
{
    return x / y;
}
  • 만약 y0이라면 우리는 어떻게 해야 할까요? 이 함수는 반드시 어떤 값이든 반환해야 하므로, 프로그램 로직을 그냥 건너뛸 수는 없습니다. 그렇다고 사용자에게 y의 새 값을 입력하라고 요청해서도 안 됩니다. 이 함수는 순수한 '계산용 함수'이기 때문에, 여기에 입력 루틴을 집어넣는 것은 이 함수를 호출하는 프로그램의 성격에 맞을 수도 있고 안 맞을 수도 있기 때문이죠.
  • 이런 상황에서 가장 좋은 선택은, 호출자가 이 상황을 알아서 처리해 주기를 바라며 오류를 호출자에게 다시 넘겨주는 것(Pass back)입니다.
  • 어떻게 할 수 있을까요? 함수의 반환 타입이 void라면, 이를 성공이나 실패를 나타내는 bool 타입으로 바꿀 수 있습니다.
  • 예를 들어, 아래와 같이 작성하는 대신 이렇게 바꿀 수 있습니다.
bool printIntDivision(int x, int y)
{
    if (y == 0)
    {
        std::cout << "Error: could not divide by zero\n";
        return false; // 실패했음을 알립니다.
    }

    std::cout << x / y;

    return true; // 성공했음을 알립니다.
}
  • 이렇게 하면 호출자는 반환값을 확인해서 함수가 어떤 이유로든 실패했는지를 알아낼 수 있습니다.

치명적 오류 (Fatal errors)

  • 오류가 너무 심각해서 프로그램이 정상적으로 작동을 계속할 수 없다면, 이를 복구 불가능한 오류 또는 치명적 오류라고 부릅니다.
  • 이럴 때는 프로그램을 종료하는 것이 최선입니다.
  • 여러분의 코드가 main() 함수 안에 있거나 main()에서 직접 호출된 함수 안에 있다면,
    main()0이 아닌 상태 코드를 반환하게 하는 것이 가장 좋습니다.
  • 하지만, 깊게 중첩된 하위 함수 내부라면 오류를 main()까지 끝까지 전달하는 것이 불편하거나 불가능할 수도 있습니다.
  • 이런 경우에는 std::exit()와 같은 중단 문을 사용할 수 있습니다.
double doIntDivision(int x, int y)
{
    if (y == 0)
    {
        std::cout << "Error: Could not divide by zero\n";
        std::exit(1); // 프로그램을 즉시 종료하고 상태 코드 1을 반환합니다.
    }
    return x / y;
}

예외 (Exceptions)

  • 함수에서 발생한 오류를 호출자에게 다시 넘겨주는 과정이 복잡하기 때문에 C++은 오류를 호출자에게 넘겨주는 완전히 독립된 방법을 제공합니다. 바로 예외(Exceptions)입니다.
  • 기본적인 아이디어는 이렇습니다. 오류가 발생하면 예외가 "던져집니다(Thrown)". 현재 함수가 그 오류를 "잡지(Catch)" 않으면, 이 함수를 호출한 호출자가 오류를 잡을 기회를 얻게 됩니다.
  • 만약 그 호출자도 오류를 잡지 않으면, 호출자의 호출자가 다시 기회를 얻습니다. 이렇게 오류는 누군가에게 잡혀서 처리될 때까지(처리되면 프로그램은 다시 정상적으로 실행됩니다), 또는 main() 함수마저 오류를 처리하지 못해 예외 오류와 함께 프로그램이 강제 종료될 때까지 콜 스택(Call stack)을 타고 위로 점진적으로 이동합니다.
  • 예외 처리에 대한 자세한 내용은 27 챕터에서 다룰 예정입니다.

std::cin과 잘못된 입력 처리하기

  • 여러분이 지금까지 작성해 온 프로그램에서는 std::cin을 사용하여 사용자에게 텍스트 입력을 요청해 왔습니다.
  • 텍스트 입력은 형태가 매우 자유롭기 때문에 프로그램이 예상하지 못한 잘못된 입력을 받기가 아주 쉽답니다.
  • 프로그램을 작성할 때는 사용자가 여러분의 프로그램을 어떻게 사용할지 항상 고민해야 해요.
  • 잘 만들어진 프로그램은 사용자의 오용을 미리 예상하고 그런 상황을 부드럽게 처리하거나 가능하다면 아예 발생하지 않도록 막아냅니다.
  • 이렇게 오류 상황을 잘 처리하는 프로그램을 가리켜 견고한(Robust) 프로그램이라고 불러요.
  • 이번 강의에서는 사용자가 std::cin을 통해 유효하지 않은 텍스트를 입력하는 구체적인 사례들을 살펴보고,
    이런 상황들을 처리하는 여러 가지 방법을 알려드릴 거예요.
  • 다음은 입력 과정에서 operator>>가 작동하는 방식을 간단히 정리한 것입니다.
  1. 가장 먼저, 입력 버퍼 의 맨 앞에 있는 선행 공백(스페이스, 탭, 줄바꿈 문자 등)이 제거됩니다.
    이 과정을 통해 이전 입력에서 버퍼에 남아있던 처리되지 않은 줄바꿈 문자들이 지워지게 돼요.
  2. 만약 입력 버퍼가 비어 있다면 operator>>는 사용자가 새로운 데이터를 입력할 때까지 기다립니다.
    사용자가 입력을 마치면 다시 선행 공백을 제거해요.
  3. 그런 다음 operator>>는 줄바꿈 문자를 만나거나,
    혹은 저장하려는 변수 타입에 맞지 않는 유효하지 않은 문자를 만날 때까지 연속된 문자들을 최대한 많이 추출(Extract)합니다.
  4. 추출 결과는 다음과 같이 나뉩니다.
  • 위 3번 단계에서 문자가 하나라도 추출되었다면, 추출 성공입니다. 추출된 문자는 적절한 값으로 변환되어 변수에 할당(대입)됩니다.
  • 만약 3번 단계에서 아무 문자도 추출하지 못했다면, 추출 실패입니다. (C++11 기준) 입력을 받으려던 객체에는 0이라는 값이 할당되며, std::cin의 상태가 초기화될 때까지 이후의 모든 추출 시도는 즉시 실패하게 됩니다.

입력 유효성 검사 (Validating input)

  • 사용자의 입력이 프로그램이 기대하는 형태와 일치하는지 확인하는 과정을 입력 유효성 검사(Input validation)라고 해요.
  • 입력 유효성을 검사하는 기본 방식에는 크게 세 가지가 있습니다.

입력 중 검사 (Inline: 사용자가 타이핑할 때)

  • 애초에 사용자가 잘못된 입력을 타이핑하지 못하도록 막는 방식이에요.

입력 후 검사 (Post-entry: 사용자가 타이핑을 마친 후)

  • 사용자가 문자열(String) 형태로 원하는 것을 마음껏 입력하게 한 다음, 그 문자열이 올바른지 검증해요.
  • 만약 올바르다면 문자열을 최종 변수 형식으로 변환합니다.
  • 사용자가 무엇이든 입력하게 두고, std::cinoperator>>가 입력을 추출하도록 시도한 뒤에 오류 상황을 처리합니다.
  • 문자열(String)의 경우 어떤 문자를 입력하든 제한이 없기 때문에 추출은 항상 성공해요.
  • 다만 std::cin은 첫 번째 공백 문자를 만나면 추출을 멈춘다는 점을 기억해 주세요.
  • 일단 문자열이 입력되면 프로그램은 이 문자열을 분석(Parse)하여 유효한지 확인할 수 있습니다.
  • 하지만 문자열을 분석하고 다른 타입(예: 숫자)으로 변환하는 작업은 꽤 까다로울 수 있어서 아주 드물게만 사용된답니다.
  • 그래서 우리는 가장 자주, std::cin과 추출 연산자에게 이 어려운 작업을 맡기는 방식을 씁니다.
  • 사용자가 마음대로 입력하게 둔 뒤에 std::cinoperator>>가 처리를 시도하고,
    만약 실패하면 그 후유증을 수습하는 거죠. 이 방법이 가장 쉬우며, 아래에서 더 자세히 이야기할 내용입니다.

단언문(Assertions)

  • 단언문(Assertion)은 프로그램에 버그가 없는 한 항상 참이 될 표현식입니다.
  • 표현식이 참(true)으로 평가되면 단언문은 아무 일도 하지 않고 넘어갑니다.
  • 만약 표현식이 거짓(false)으로 평가되면, 오류 메시지가 표시되고 프로그램이 종료됩니다( std::abort를 통해).
  • 이 오류 메시지에는 일반적으로 실패한 표현식의 텍스트, 코드 파일의 이름, 그리고 단언문이 있는 줄 번호가 포함돼요.
  • 덕분에 무엇이 문제인지뿐만 아니라, 코드의 어디에서 문제가 발생했는지 아주 쉽게 알 수 있죠.
  • 이는 디버깅 작업에 엄청난 도움이 됩니다.
  • C++에서 런타임 단언문은 <cassert> 헤더에 있는 assert 전처리기 매크로를 통해 구현됩니다.
#include <cassert> // assert()를 사용하기 위해 포함
#include <cmath> // std::sqrt를 사용하기 위해 포함
#include <iostream>

double calculateTimeUntilObjectHitsGround(double initialHeight, double gravity)
{
  assert(gravity > 0.0); // 양의 중력이 없으면 물체는 땅에 닿지 않습니다.

  if (initialHeight <= 0.0)
  {
    // 물체는 이미 땅에 있거나 묻혀 있습니다.
    return 0.0;
  }

  return std::sqrt((2.0 * initialHeight) / gravity);
}

int main()
{
  std::cout << "Took " << calculateTimeUntilObjectHitsGround(100.0, -9.8) << " second(s)\n";

  return 0;
}

NDEBUG 매크로

  • assert 매크로는 조건을 검사할 때마다 아주 약간의 성능 비용이 발생해요.
  • 게다가, 상용 서비스용으로 배포되는 실제 프로덕션 코드에서는 오류로 멈추는 일이 결코 발생해서는 안 됩니다.
  • 따라서 대부분의 개발자는 디버그 빌드에서만 단언문이 활성화되는 것을 선호합니다.
  • C++에는 프로덕션 코드에서 단언문을 끌 수 있는 내장된 방법이 있어요.
  • 바로 NDEBUG라는 전처리기 매크로가 정의되어 있으면, assert 매크로비활성화되는 것입니다.
  • 대부분의 IDE는 릴리스 구성에 대한 프로젝트 설정의 일부로 NDEBUG를 기본적으로 설정해 둡니다.
  • 테스트 목적으로 특정 번역 단위 내에서 단언문을 활성화하거나 비활성화할 수 있습니다.
  • 그렇게 하려면 모든 #include 문구 이전에 별도의 줄에 #define NDEBUG 또는 #undef NDEBUG 중 하나를 배치하세요.
#define NDEBUG // 단언문을 비활성화합니다 (반드시 모든 #include 이전에 위치해야 함)
#include <cassert>
#include <iostream>

int main()
{
assert(false); // 이 번역 단위에서는 단언문이 비활성화되었으므로 작동하지 않습니다.
std::cout << "Hello, world!\n";

return 0;
}

static_assert (정적 단언문)

  • C++에는 static_assert라고 불리는 또 다른 유형의 단언문도 존재합니다.
  • 일반 assert가 실행 시간에 검사하는 것과 달리, static_assert는 컴파일 타임에 검사되는 단언문이에요.
  • 그래서 static_assert가 실패하면 컴파일 오류가 발생하여 아예 프로그램이 만들어지지 않게 됩니다.
  • 또한, <cassert> 헤더에 선언되어 있는 assert 매크로와는 다르게, static_assert는 C++의 키워드입니다.
  • 따라서 이를 사용하기 위해 별도의 헤더 파일을 포함할 필요가 없답니다.
  • static_assert는 다음과 같은 형태를 가집니다.
static_assert(조건, 진단_메시지)
  • 조건이 참이 아니면 진단 메시지가 출력됩니다.
  • 타입들이 특정한 크기를 가지는지 확인하기 위해 static_assert를 사용하는 예제를 살펴볼까요?
static_assert(sizeof(long) == 8, "long must be 8 bytes");
static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");

int main()
{
	return 0;
}

단언문(Asserts)과 오류 처리(Error handling)의 차이점

  • 언문과 오류 처리는 목적이 꽤 비슷해 보여서 헷갈리기 쉽습니다. 차이점을 명확히 정리해 드릴게요.

단언문(Assertions)은 절대 일어나서는 안 되는 일에 대한 가정을 문서화하여, 개발 과정 중에 발생하는 프로그래밍 오류를 감지하는 데 사용됩니다. 만약 단언문에서 설정한 조건이 깨졌다면, 그건 100% 프로그래머의 잘못입니다. 또한 단언문은 오류로부터의 복구를 허용하지 않아요. (애초에 일어나선 안 될 일이 일어났으니 복구할 필요도 없는 셈이죠.)
단언문은 주로 릴리스 빌드에서 컴파일 시 제거되므로 성능 걱정 없이 아주 넉넉하게 많이 넣으셔도 좋습니다.

오류 처리(Error handling)는 릴리스 빌드에서 (아무리 드물더라도) 실제로 발생할 수 있는 상황들을 우아하게 처리해야 할 때 사용됩니다. 이러한 문제들은 복구가 가능한 문제일 수도 있고(프로그램이 계속 실행될 수 있음), 복구가 불가능한 문제일 수도 있습니다(프로그램을 종료해야 하지만, 적어도 친절한 오류 메시지를 보여주고 자원들이 제대로 정리되도록 보장할 수 있음). 오류 감지와 처리는 런타임 성능 비용과 개발 시간 비용이 모두 발생합니다.

profile
안녕하세요.

0개의 댓글