1. 예외 처리(Exception Handling)

예외 처리(Exception Handling)에서 예외(Exception)이란 프로그램 실행 도중에 일어나는 비정상적인 상황을 의미합니다. 이런 상황이 벌어질때, 이를 처리하는 과정을 예외 처리라고 합니다. 예를 들어서, 나눗셈 프로그램에서 사용자로부터 두 개의 정수를 입력받는데, 나누는 수를 0으로 입력한것과 같이 말이죠. 직접 그런 프로그램을 만들어 보도록 합시다.

#include <iostream>

using namespace std;

int main()
{
	int a, b;

	cout << "두 개의 정수를 입력하세요: ";
	cin >> a >> b;
	cout << a << "를 " << b << "로 나눈 몫은 " << a/b << "입니다." << endl;
	return 0;
}

결과:

두 개의 정수를 입력하세요: 10 2

10를 2로 나눈 몫은 5입니다.

..

두 개의 정수를 입력하세요: 5 0

계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, 7행에 입력되는 두개의 정수를 담을 a와 b 변수가 선언되어 있습니다. 그리고 10행에서, 사용자로부터 a, b를 입력받고, 11행에서 a를 b로 나누고 난 뒤의 몫을 출력합니다. 결과를 보시면, 10을 2로 나눈다던가, 5를 3으로 나누면 잘 출력이 되지만, 나누는 수를 0으로 두었더니 프로그램이 갑자기 종료되어 버렸습니다. 기존에는, 이런 상황을 아래와 같이 예외를 처리했습니다.

#include <iostream>

using namespace std;

int main()
{
	int a, b;

	cout << "두 개의 정수를 입력하세요: ";
	cin >> a >> b;
	if (b == 0) // 나누는 수가 0이라면,
		cout << "나누는 수가 0이 될 수 없습니다." << endl;
	else
		cout << a << "를 " << b << "로 나눈 몫은 " << a/b << "입니다." << endl;
	return 0;
}

결과:

두 개의 정수를 입력하세요: 10 2

10를 2로 나눈 몫은 5입니다.

..

두 개의 정수를 입력하세요: 4 0

나누는 수가 0이 될 수 없습니다.

계속하려면 아무 키나 누르십시오 . . .


11행을 보시면, b가 0일 경우에 "나누는 수가 0이 될 수 없습니다."를 출력하고 프로그램이 종료됩니다. 반대로, 0이 아닐 경우에는 몫을 사용자에게 보여줍니다. 나누는 수가 0이 입력되어도, 프로그램은 종료되지 않습니다. 이런 방식으로 예외 처리를 하게 되면, 예외가 발생할때마다 이렇게 처리를 해주어야 하기 때문에 똑같은 코드가 늘어나 코드만 차지하거나, 예외 처리의 구분이 명확하지 않습니다. 그럼 어떻게 하여야만 할까요?


바로, 우리가 지금 배우게될 C++의 예외 처리를 이용하면 쉽게 해결할 수 있습니다.


2. try~catch(시도하다~잡다), throw(던지다)

C++에서 제공하는 예외 처리 메커니즘인 try~catch, throw에 대해 알아봅시다. try~catch, throw의 기본 형식은 아래와 같습니다.

try { // 예외가 발생하는 영역
   if (예외 조건) throw 예외 객체; // 예외가 발생하면 예외를 던지는 영역
} catch (예외 객체) { // 던져진 예외를 잡는 영역
   // 예외 처리 영역
}

살펴보자면, 예외가 발생할만한 영역을 try로 감싸주고, 그 뒤에 try 영역 내에서 예외 조건이 만족하면, throw로 그 예외를 던집니다. 그러면 catch가 그 예외를 잡아 처리해줍니다. 그리고, catch문이 굳이 하나가 아니라 2개 이상 등장해도 됩니다. 던져진 예외 객체와 한번, 위의 나눗셈 예제에서 if문이 아닌 try~catch, throw를 이용한 방법으로 예외 처리를 구현해보도록 하겠습니다.

#include <iostream>

using namespace std;

int main()
{
	int a, b;

	cout << "두 개의 정수를 입력하세요: ";
	cin >> a >> b;

	try {
		if (b == 0) throw b;
		cout << a << "를 " << b << "로 나눈 몫은 " << a/b << "입니다." << endl;
	} catch (int exception) {
		cout << "예외 발생, 나누는 수는 " << b << "가 될 수 없습니다." << endl;
	}
	return 0;
}

결과:

두 개의 정수를 입력하세요: 5 0

예외 발생, 나누는 수는 0가 될 수 없습니다.

계속하려면 아무 키나 누르십시오 . . .


코드를 살펴보시면, 12~17행에서 try~catch 구문이 사용되었습니다. 사용자가 10행에서 a와 b값을 입력하게 하고나서, try 영역 안으로 진입합니다. 13행에서, 만약 b가 0이면(나누는 수가 0이면), 이 b를 throw를 이용해 예외를 던져버립니다. 그럼 이렇게 던져진 예외는, 예외 데이터인 exception에 b의 값이 들어가게 되며, catch가 잡아 처리하게 됩니다.


만약, 예외가 발생하지 않으면 catch 영역은 실행되지 않습니다. 이번엔, 함수 내에서 예외가 발생하였을 경우 그 예외를 처리하도록 해봅시다.


3. 함수 예외 처리(Function Exception Handling)

이름이 func인, 두개의 정수를 인자로 받는 함수를 정의해봅시다. 그리고 그 함수 내에 b가 0이면(나누는 수가 0이면) 예외를 던지도록 하고, 그 아래 몫을 출력하도록 해봅시다. 아래처럼, 함수에서 예외가 발생했을 경우에 어떻게 예외가 처리되는지 살펴보도록 합시다.

#include <iostream>

using namespace std;

void func(int a, int b)
{
	if (b == 0) throw b;
	cout << a << "를 " << b << "로 나눈 몫은 " << a/b << "입니다." << endl;
}

int main()
{
	int a, b;

	cout << "두 개의 정수를 입력하세요: ";
	cin >> a >> b;

	try {
		func(a, b);
	} catch (int exception) {
		cout << "예외 발생, 나누는 수는 " << b << "가 될 수 없습니다." << endl;
	}
	return 0;
}

결과:

두 개의 정수를 입력하세요: 4 0

예외 발생, 나누는 수는 0가 될 수 없습니다.

계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, 5~9행에 func란 함수가 정의되었습니다. 함수 내부를 살펴보시면, 전달받은 b의 값이 0일 경우에 예외를 던지고 있습니다. 그런데, func 함수 내에는 예외를 처리하는 영역이 없기 때문에, func 함수가 호출된 영역으로 예외를 전달합니다.


main 함수 내부로 들어가면, 19행에서 try 내에 func 함수가 호출되었는데, func 함수에서 예외가 발생하면, 예외 데이터를 호출 영역으로 다시 전달합니다. 그럼 다시 전달된 예외 데이터를 catch 영역이 잡아 처리하게 되는 것입니다. (참고로, 기본 데이터형(int, char..)뿐만 아니라 객체(Object)도 예외로 던질 수 있습니다.)


4. 스택 풀기(Stack Unwinding)

위와 같이, 예외를 처리하는 영역이 없어 이 예외가 호출된 영역을 타고 계속 전달되는 현상을 가리켜 스택 풀기(Stack Unwinding)이라고 합니다. 아래 예제를 통해, 스택 풀기가 어떤 것인지 확인해보도록 합시다.

#include <iostream>

using namespace std;

void func1() { throw 0; }
void func2() { func1(); }
void func3() { func2(); }
void func4() { func3(); }

int main()
{
	try {
		func4();
	} catch (int exception) {
		cout << "예외 발생, " << exception << "!" << endl;
	}
	return 0;
}

결과:

예외 발생, 0!

계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, 5~8행에서 func1부터 func4까지 함수가 정의되었습니다. func1에서 예외 데이터를 던지며, func2 함수는 func1 함수 호출을, func3 함수는 func2 함수 호출을, func4 함수는 func3 함수를 호출합니다. main 함수 내의 13행을 보시게 되면, func4 함수가 호출되었습니다. 그러면 아래와 같이, func1 함수까지 호출하겠죠? 함수의 호출 순서는 아래와 같을 것입니다.


func4() -> func3() -> func2() -> func1()


그럼, 예외 데이터는 어떻게 넘어갈까요? 아래를 한번 보도록 합시다.


<스택 메모리(Stack Memory)>


func4() 호출 -> func3() 호출 -> func2() 호출 -> func1() 호출 -> func1 함수 내에서 예외 데이터 던짐 -> func2 함수가 예외 데이터를 받고 다시 func3 함수에 던짐 -> func3 함수가 예외 데이터를 받고 다시 func4 함수로 던짐 -> func4 함수가 예외 데이터를 받고 다시 main 함수 내에 있는 예외 처리 영역으로 던짐 -> 예외 처리


함수가 호출될 때는 저렇게 각 함수의 스택 프레임이 생성됩니다. 그리고, func1 함수에서 throw를 만나고, 자신과 자기를 호출한 함수의 스택을 모두 정리(해제)하고 돌아갑니다. 이것이 스택 풀기(Stack Unwinding)입니다.


이번 강좌는 여기서 마치도록 하겠습니다. 수고하셨고, C++ 강좌는 예외 처리를 마지막으로 끝을 내려 합니다. 나머지 올라오지 않은 강좌는 번외편으로 작성하도록 하겠습니다. 그리고 설명이 부족하다 싶으시면, 덧글로 달아시면 보충 설명을 달아드리도록 하겠습니다.