ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [예외 처리] 너라고 예외는 아니야
    개발/C·C++ 2021. 4. 28. 02:54

    완벽한 프로그램은 없다고 생각합니다. 인간이 불완전하기 때문에 인간이 만드는 결과물은 무엇이 됐든 불완전할 수밖에 없습니다. 예외 처리는 불완전한 프로그램이 그 속성을 유지하면서 지속적으로 기능할 수 있게 도와줍니다. 사람들이 사용하고 있는 서비스가 시도 때도 없이 종료가 되어 다시 시작해야 한다면 많이 불편할 겁니다.

     

    예외 처리에 try-catch 구문을 사용합니다. 예외가 발생했다고 알리는 키워드는 throw입니다. throw는 단어 그대로 던진다는 의미를 가집니다. re-throw가 아닌 이상 예외를 알릴 무언가를 던지면 됩니다. 문자열이어도 되고 숫자여도 되며 특정 클래스도 가능하고, std::exception 클래스를 상속한 클래스여도 됩니다. 무엇은 던지든 catch만 잘하면 됩니다. try 구문 안에서 throw가 실행되면 던지는 객체의 타입에 따라 catch가 되는데, catch의 파라미터가 적당하지 않으면 프로그램은 강제 종료됩니다.

    #include <iostream>
    using std::cout;
    using std::endl;
    
    class MyException : public std::exception
    {
    public:
        virtual const char* what() const override
        {
            return "my exception";
        }
    }
    
    double divide(double dividend, double divisor)
    {
        if (divisor == 0)    
        {
            throw MyException();
        }    
        return dividend / divisor;
    }
    
    void func1()
    {
        divide(10, 0);
        // your code
        cout << "func1()" << endl;
    }
    
    void func0()
    {
        func1();
        // your code
        cout << "func0()" << endl;
    }
    
    int main()
    {
        try
        {
            func0();
            // your code
            cout << "main" << endl;
        }
        catch(std::exception& e)
        {
            cout << e.what() << endl;
        }
        catch(...)
        {
            cout << "exception" << endl;
        }
    }

    어떤 문자열도 출력되지 않고 MyException 클래스에서 재정의한 함수가 리턴하는 문자열만 출력됐습니다. 그 의미는 예외를 발생시키는 순간(throw), 실행 제어는 더 이상 다음 코드로 넘어가지 않고 적당한 catch 구문으로 이동한다는 것입니다. 그렇다고 해서 예외를 만나기 전까지 스택 프레임에 존재하고 있던 메모리들이 찝찝하게 남아있지는 않습니다. catch 문으로 가면서 메모리는 정리됩니다. Test 클래스를 정의해서 소멸자(파괴자)가 호출됐을 때 문자열을 출력하도록 해보면 확인할 수 있습니다. 함수 다음의 어느 문자열도 출력되지 않고 소멸자에서 출력하는 문자열만 콘솔창에 나타날 것입니다. catch(...)는 모든 예외를 잡을 수 있습니다만, 어떤 종류의 예외인지 알 수는 없습니다. 

     

    try-catch를 더 안쪽에 사용할 수도 있습니다. 예외 처리는 범위를 넓게 잡기 보다는 예외가 발생하는 특정 구간에 작성하는 것이 명료합니다. 특별한 사유가 없는 한 그다음 상위 지점으로 예외를 throw하지 않습니다.

    #include <iostream>
    using std::cout;
    using std::endl;
    
    double divide(double dividend, double divisor)
    {
        if (divisor == 0)    
        {
            throw "divisor is zero";
        }    
        return dividend / divisor;
    }
    
    void func1()
    {
        // 예외가 발생한 곳에서 예외처리
        try
        {
            divide(10, 0);
        }
        // 여기에서 catch했으므로 main의 catch로 안 간다
        catch(const char* e) 
        {
            cout << e << endl;
            // 던지면 main에서 예외처리를 하겠다는 것. re-throw는 e를 안 넣어도 됨
            //throw; 
        }
        
        // your code
        cout << "func1()" << endl;
    }
    
    void func0()
    {
        func1();
        // your code
        cout << "func0()" << endl;
    }
    
    int main()
    {
        try
        {
            func0();
            // your code
            cout << "main" << endl;
        }
        catch(std::exception& e)
        {
            cout << e.what() << endl;
        }
        catch(const char* e)
        {
            cout << e << endl;
        }
        catch(...)
        {
            cout << "exception" << endl;
        }
    }

    여기에선 쉽게 결과를 보여주기 위해서 main에서 catch할 때 std::exception을 가장 위에 뒀는데, catch 구문이 한 가지가 아닐 때는 구체적인 예외를 상위에 두어야 합니다. catch는 if-else if처럼 하나의 구문만 선택되기 때문입니다. 즉 std::exception이나 ...는 가장 마지막 catch 구문에 들어가는 게 좋습니다. <stdexcept>를 include하면 이미 정의된 에러를 볼 수 있습니다. 표준 에러를 사용하느냐, 사용자 정의 에러 클래스를 새로 정의하느냐는 팀의 룰을 따릅니다.

    댓글

Designed by Tistory.