5. Errors

5.1. Introduction

에러에는 여러 종류가 있다.

  • 컴파일 에러 : 컴파일 타임에 잡히는 에러. 문법 에러와 타입 에러로 나뉜다.
  • 링크 에러 : 링크 타임의 에러.
  • 런타임 에러 : 프로그램 실행 시의 에러. 컴퓨터에 의한 에러, 라이브러리에 의한 에러, 유저 코드에 의한 에러로 나뉜다.
  • 로직 에러 : 프로그램 실행에는 문제가 없으나 의도하지 않은 결과가 나온 것.

에러를 모두 잡는 것이 이상적이지만 현실적으론 불가능하다. 프로그램의 이상적인 조건은 다음과 같을 것이다.

  • 모든 올바른 입력에 대해 의도한 결과를 냄
  • 모든 틀린 입력에 대해 적절한 에러 메시지를 냄
  • 잘못된 하드웨어에 대해 대처
  • 잘못된 시스템 소프트웨어에 대해 대처
  • 에러를 찾은 뒤 종료

3-5번에 대한 이야기는 이 책을 벗어난다. 1-2번은 지키도록 하자. 이것은 최소한의 조건이다. 이를 위해서는 3가지 접근법이 있다.

  • 에러를 최소화하도록 소프트웨어를 설계한다.
  • 디버깅과 테스팅을 통해 대부분의 에러를 제거한다.
  • 심각하지 않은 에러들만 남긴다.

5.2. Source of errors

에러의 원인은 주로 다음과 같다.

  • 부족한 명세화
  • 미완성된 프로그램
  • 예상치 못한 인자
  • 예상치 못한 입력
  • 예상치 못한 상태
  • 논리 에러

5.3. Compile-time errors

에러는 컴파일 타임에 잡을 수 있도록 하는 것이 가장 좋다. 이는 문법 에러와 타입 에러로 나뉜다.

5.3.1. Syntax errors

다음의 함수가 있을 때,

int area(int length, int width);

아래와 같은 에러는 문법 에러이다. 컴파일러가 짚어 주는 라인에 이상한 점이 없다면 그 전 라인들을 보라.

int s1 = area(7; // error: ) missing
int s2 = area(7) // error: ; missing
Int s3 = area(7); // error: Int is not a type
int s4 = area('7); // error: ' missing

5.3.2. Type errors

아래와 같은 에러는 타입 에러이다.

int x0 = arena(7); // error: undeclared function
int x1 = area(7); // error: wrong number of arguments
int x3 = area("seven", 2); // wrong type

5.3.3. Non-errors

에러는 아니지만 프로그래머의 부주의로 인해 의도치 않은 결과를 내는 경우가 있다.

int x4 = area(10, -7); // OK: but what's a rectangle with a width -7?
int x5 = area(10.7, 9.3); // OK: but calls area(10, 9)
char x6 = area(100, 9999); // OK: but truncates the result

5.4. Link-time errors

프로그램은 독립적으로 컴파일되는 번역 단위로 구성된다. 링크 에러는 함수 호출 시에 주로 나타난다. 모든 함수는 해당 함수가 사용되는 번역 단위 내에서 선언되어야 하며, 해당 프로그램 내에서 단 한 번만 정의되어야 한다. 또한 함수의 정의부는 리턴 타입과 인자 타입 모두 호출부와 일치해야 한다. 이를 위반하면 링크 에러가 난다.

5.5. Run-time errors

프로그램이 구동된 상태에서 발생하는 에러가 런타임 에러이다. 이는 원인을 알기 더 어렵기 때문에 문제가 된다.

int area(int length, int width) {
    return length * width;
}
int framed_area(int x, int y) {
    return area(x - 2, y - 2);
}
int main() {
    int x = -1;
    int y = 2;
    int z = 4;
    int area1 = area(x, y);
    int area2 = framed_area(1, z);
    int area3 = framed_area(y, z);
    double ratio = double(area1) / area3;
}

위 프로그램은 컴파일과 링크에는 문제가 없으나 area3이 0이 되기 때문에 ratio에서 0으로 나누게 되어 문제가 된다. 두 가지 방법이 있다:

  • area()를 호출하는 쪽에서 대처하게 한다.
  • area() 내에서 대처하게 한다.

5.5.1. The caller deals with errors

호출하는 쪽에서 대처하게 하는 것은 이상적이지 않다. area()를 호출하는 다른 함수가 또 생기면 그 함수에서도 대처를 추가해야 하며 area() 내의 내부 구현을 호출부에서 모두 알고 있어야 한다.

5.5.2. The callee deals with errors

함수 내에서 인자를 체크하게 하는 것이 훨씬 낫다. 그러나 항상 가능한 것은 아니다.

  • 함수의 정의부를 변경할 수 없을 경우
  • 함수에서 에러 발생 시 어떻게 해야 할 지 모를 경우
  • 함수에서 호출부를 모를 경우
  • 성능 문제

5.5.3. Error reporting

에러를 찾았으면 어떻게 해야 하나? 특수한 값을 리턴하는 것이 방법이다. 이는 단점이 있다.

  • 테스트를 호출부와 함수 내에서 2번 해야 한다.
  • 호출부는 테스트를 잊어버릴 수 있다.
  • 별도의 리턴 값을 두는 게 항상 가능하진 않다.

이에는 대안이 있다: 예외를 이용하는 것이다.

5.6. Exceptions

C++에서는 예외를 제공한다. 기본적인 아이디어는 에러를 감지하는 부분과 에러를 처리하는 부분을 분리하는 것이다. 이 때 에러가 발생한 함수에서는 일반적으로 return을 하는 것이 아니라 예외를 throw한다. 이후 에러를 처리하는 부분에서 try – catch 문으로 에러를 잡아 처리한다.

5.6.1. Bad arguments

잘못된 인자에 대한 체크는 다음과 같이 할 수 있다.

class Bad_area {};

int area(int length, int width) {
    if (length <= 0 || width <= 0) throw Bad_area();
    return length * width;
}

int main() {
    try {
        int x = -1;
        int y = 2;
        int z = 4;
        int area1 = area(x, y);
        int area2 = framed_area(1, z);
        int area3 = framed_area(y, z);
        double ratio = area1 / area3;
    } catch (Bad_area) {
        std::cout << "Oops! Bad arguments to area()\n";
    }
}

위의 코드는 발생하는 모든 Bad_area를 catch하고, 어디에서 발생했는지 main() 신경쓸 필요가 없다는 점을 눈여겨보자.

5.6.2. Range errors

다음과 같이 배열의 경계 범위 밖에 접근하면 에러가 난다. range-for loop을 쓰면 방지할 수 있지만 루프 안에서 인덱스 값이 필요할 수도 있기 때문에 문제가 된다. 이 에러가 날 경우 std::out_of_range 예외가 발생한다.

try {
    std::vector<int> v;
    for (int i; std::cin >> i; )
        v.push_back(i);
    for (int i = 0; i <= v.size(); i++) 
        std::cout << v[i] << '\n';
} catch (std::out_of_range& e) {
    std::cerr << "Oops! Range error\n";
    return 1;
} catch (...) {
    std::cerr << "Exception: something went wrong\n";
    return 2;
}

5.6.3. Bad input

입력이 잘못되었는지 체크하는 방법은 입력 스트림을 검사하는 것이다.

double d = 0.0;
std::cin >> d;
if (std::cin) {
} else {
}

C++에서는 std::out_of_range 에러 말고도 여러 예외를 제공하는데 가장 범용적으로 쓰기 편한 것은 std::runtime_error이다. 다음과 같이 유틸 함수를 만들어 쓸 수 있다.

void error(std::string& s) {
    throw std::runtime_error(s);
}

int main() {
    try {
        ...
        return 0;
    } catch (std::runtime_error& e) {
        std::cerr << "runtime error: " << e.what() << '\n';
        return 1;
    }
}

catch (std::runtime_error& e) 대신 catch (std::exception& e) 를 사용하면 std::runtime_error와 std::out_of_range를 모두 잡을 수 있다. 이 두 예외가 std::exception을 상속하기 때문이다. 상속에 대해서는 나중에 배운다.

5.6.4. Narrowing errors

다음과 같은 대입은 데이터 정보를 좁힌다.

int x = 2.9;
char c = 1066;

이를 이용해 다음과 같이 커스텀 narrow_cast를 만들어 쓸 수 있다. (디테일은 아직 이해하려고 하지 말라)

// run-time checked narrowing cast (type conversion). See ???.
template <class R, class A> R narrow_cast(const A& a)
{
	R r = R(a);
	if (A(r)!=a) throw std::runtime_error("info loss");
	return r;
}

int x1 = narrow_cast<int>(2.9); // throws
int x2 = narrow_cast<int>(2.0); // OK
char c1 = narrow_cast<char>(1066); // throws
char c2 = narrow_cast<char>(84); // OK

5.7. Logic errors

가장 찾기 힘든 버그는 프로그램이 정상 동작하는데도 프로그래머의 코딩상 논리적 오류 때문에 원하지 않는 출력 결과가 나오는 것이다. 다음을 보자.

int main() {
    std::vector<double> temps;
    for (double temp; std::cin >> temp; )
        temps.push_back(temp);
    double sum = 0.0;
    double high_temp = 0.0;
    double low_temp = 0.0;
    for (double x : temps) {
        if (x > high_temp) high_temp = x;
        if (x < low_temp) low_temp = x;
        sum += x;
    }
    std::cout << "High temperature: " << high_temp << '\n';
    std::cout << "Low temperature: " << low_temp << '\n';
    std::cout << "Average temperature: " << sum / temps.size() << '\n';
}

최대/최소/평균 온도를 출력할 것 같지만 그렇지 않다. 최저 온도의 초기값이 0.0도여서 모든 온도 데이터가 0.0보다 크더라도 최저 온도는 0.0으로 기록되기 때문이다. 최고 온도도 같은 문제가 있다. 이를 변경함으로써 해결할 수 있다.

5.8. Estimation

문제의 대략적인 답을 예측하는 것은 디버깅을 수월하게 한다.

5.9. Debugging

디버깅을 이렇게 하면 안 된다:

while (프로그램이 잘 동작하지 않는다) {
    프로그램이 이상해 보이는 부분을 아무렇게나 찾는다
    더 나아 보이는 방식으로 바꾼다
}

핵심은 이 프로그램이 올바르게 동작했는지 어떻게 알 수 있는가? 라고 질문하는 것이다.

5.9.1. Practical debug advice

코드를 한 줄 치기 전에 디버깅에 대해 생각하라. 코드를 읽기 쉽게 하라.

  • 주석을 잘 달아라. 주석을 남용하라는 말이 아니다. 코드로 표현될 수 없는 것들을 주석에 간결하게 표현하라. (프로그램의 목적 등)
  • 의미 있는 이름을 써라. 긴 이름을 쓰라는 말은 아니다.
  • 일관적인 코드 구조를 가져라.
  • 코드를 작은 함수들로 쪼개라.
  • 복잡한 코드 제어문을 피하라.
  • 라이브러리를 사용하라.

당신이 초고수가 아닌 한 컴파일러를 믿으라; 당신이 초고수라면 이 책을 읽을 필요가 없다.

불변성 (항상 만족시켜야 하는 조건)을 체크하기 위해 assert를 써라.

5.10. Pre- and post- conditions

사전 조건과 사후 조건에 대해서 첫째로는 주석으로 문서화하라.

5.10.1. Post-conditions

사후 조건도 체크해야 한다.

int area(int length, int width) {
// calculate area of a rectangle;
// expects: length and width are positive
// ensures: returns a positive value that is the area
    if (length <= 0 || width <= 0) error("area() pre-condition");
    int a = length * width;
    if (a <= 0) error("area() post-condition");
    return a;
}

5.11. Testing

테스팅은 입력에 대해 기대한 출력이 나오는지를 검사하는 것이다. 이를 위해서는 경계값에 대해 충분히 검사해야 한다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중