01. SEH(Structed Exception Handling)
지금까지의 예제에서는 상당 부분의 예외처리를 생략하여 표현했다. 하지만, 실제로는 수많은 예외처리 코드를 필요로 한다. 보통의 개발자는 if문을 통해서 예외발생 유무를 확인하여, 이 결과에 따라 예외를 처리하게 된다. 만약, 예외를 처리하는 코드만을 따로 모아둘 수 있다면 코드 보기가 한결 수월해질 것이다.
SEH를 이야기하기 전에, 예외 처리에 대해 알아보자. 예외와 에러의 차이는 무엇일까?
문법적인 오류가 없지만, “test.txt” 파일이 존재하지 않아 프로그램이 실행되지 않는다고 가정해 보자. 이는 에러인가? 예외 발생인가? 이러한 상황을 에러로 보기는 어려울 것이다. 즉, 예외적 상황인 것이다.
반면에, 메모리의 동적 할당 상황에서 메모리 해제를 해주지 않아 문제가 발생했다고 가정해 보자. 이러한 상황에서는 사람마다 에러인지 예외인지 다르게 판단할 것이다. 이처럼 예외와 에러의 구분은 모호하기 때문에, 이 책에서는 예외처리를 다음과 같이 결론짓는다.
“프로그램 실행 시 발생하는 문제점 대부분을 예외라고 인식하자.”
예외를 위와 같이 정의한다면, 프로그램 실행 시 예측 가능한 대부분의 문제점을 예외로 간주하고, 처리 가능하도록 프로그램을 구현해야 한다.
하드웨어 예외와 소프트웨어 예외
구조적 예외처리에서 말하는 예외는 하드웨어 예외(Hardware Exception)와 소프트웨어 예외(Software Exception) 크게 두 가지로 나뉜다. 우선 하드웨어 예외란, 하드웨어에서 인식하고 알려주는 예외를 말하며, 정수를 0으로 나누는 연산의 경우 이에 해당한다. 이는 나누는 연산의 주체가 CPU이기 때문이다. 다음으로, 소프트웨어 예외는 소프트웨어에서 감지하는 예외를 말한다. 두 가지 예외의 차이점은 하드웨어 예외의 경우 개발자가 임의로 예외의 종류를 늘리거나 변경할 수 없다. 하지만 소프트웨어 예외는 가능하다.
02. 종료 핸들러(Termination Handler)
앞으로 설명할 SEH는 성능 저하에 영향을 미친다. 하지만, 이는 예외처리에서 중요한 부분이 아니기 때문에 책에서 자세한 언급은 하지 않는다.
먼저, SEH, 즉 구조적 예외처리 매커니즘은 크게 종료 핸들러(Termination Handler)와 예외 핸들러(Exception Handler) 두 가지로 나뉜다. 먼저, 종료 핸들러에 대해 이야기 해보자.
종료 핸들러의 기본 구성과 동작 원리
종료 핸들러에서 사용되는 키워드 두 가지는 다음과 같다.
__try, __finally
키워드 __try는 예외 핸들러에서도 사용되는데, 역할의 차이를 보인다. 아무튼 종료 핸들러에서는 두 가지 키워드가 사용되며, 다음과 같이 __try와 __finally는 개별적으로 사용할 수 없다. 즉, 컴파일러는 이 둘을 하나의 문장으로 인식한다.
__try
{
//코드 블록...
}
__finally
{
//종료 처리 블록...
}
위 코드는 “__try 블록을 한 줄이라도 실행하면, 반드시 __finally 블록을 실행해라” 라는 의미를 지닌다. 이는 컴파일러에 의해 보장된다. 또한, return, break 등에 의해 __try 블록을 빠져나오는 상황이 발생해도 __finally 블록은 실행된다.
그러나 한 가지 주의해야 할 점은 ExitProcess, ExitThread, exit 등의 함수에 의한 프로세스 또는 쓰레드의 강제 종료는 __finally 블록의 실행으로 이어지지 않는다.
종료 핸들러 사례 1
종료 핸들러의 특징은 프로그램이 정상/비정상으로 실행되는 것과 무관하게, 무조건 실행되는 __finally 블록이 존재한다는 것이다. 그렇다면, 문제 발생의 유무와 상관없이 무조건 실행되어야 하는 코드는 무엇이 있을까?
먼저, 가장 쉽게 생각할 수 있는 상황은 파일 open에 따른 종료 상황이다. 파일을 open 했다면, 문제 발생과 무관하게 무조건 close 해야 한다. 또한, 메모리의 동적 할당도 비슷한 맥락이다.
추가적으로, 앞서 멀티 쓰레드 동기화에서 공부했던 뮤텍스는 반드시 반환되어야 한다. 따라서, 이러한 상황에도 유용하게 사용 가능하다.
이러한 종료 핸들러를 사용한다면, 발생 가능한 예외상황이 여러 가지라서 if문을 여러 개 사용해야 하는 경우에 코드를 간결하고 안전성 높게 작성할 수 있다.
03. 예외 핸들러(Exception Handler)
종료 핸들러와는 다르게, 예외 핸들러는 “예외상황 발생 시 선별적 실행”이라는 특징을 지닌다. 일종의 약속이다.
예외 핸들러와 필터(Exeption Handler & Filters)
__try
{
}
__except(예외처리 방식)
{
}
예외 핸들러에서는 위와 같은 __try 블록과 __except 블록을 사용한다. __try 블록은 예외상황이 발생 가능한 영역을 묶는 데에 사용된다. 만약, __try 블록에서 예외 상황이 발생하면, 이어서 등장하는 __except 블록에서 이 상황을 처리하는 것이다.
__except문을 보면 괄호 안에 “예외처리 방식”이라는 부분이 있는데, 이 부분을 예외필터(Exception Filter)라 한다. 이는 인자 전달을 위해 있는 것은 아니며, 말 그대로 예외처리 매커니즘의 동작방식을 결정하는 부분이다. 이는 다음과 같은 세 가지 필터 표현식(Filter Expression)이 올 수 있다.
EXCEPTION_EXCUTE_HANDLER
EXCEPTION_CONTINUE_EXECUTION
EXCEPTION_CONTINUE_SEARCH
EXCEPTION_EXCUTE_HANDLER
이는 __try 블록 내에서 예외가 발생한다면, 나머지 코드를 실행시키지 않고 __except 블록으로 이동한다. 또한, 이러한 방식으로 예외를 처리하면 프로그램이 강제 종료 되지 않는다.
EXCEPTION_CONTINUE_EXECUTION
문제가 발생한 부분에 대해서만 정정하도록 __except 예외처리 블록을 구현하여, 다시 __try 블록을 이어서 실행시키도록 하는 예외필터 표현식이다. 예를 들어, 나눗셈 연산을 하는 프로그램의 예외 처리를 다음과 같이 구현할 수 있다.
//EXCEPTION_CONTINUE_EXECUTION 활용
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int n1;
int n2;
DWORD FilterFunc(DWORD exptType)
{
if (exptType == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
printf("n2가 0입니다. 다시 입력해주세요. \n");
scanf("%d", &n2);
printf("n1: %d, n2: %d\n", n1, n2);
}
return true;
}
int divide(int a, int b)
{
__try
{
int result = a / b;
return result;
}
__except (FilterFunc(GetExceptionCode()))
{
}
}
int main()
{
scanf("%d %d", &n1, &n2);
printf("n1: %d, n2: %d\n", n1, n2);
int result;
result = divide(n1, n2);
printf("result: %d \n", result);
return 0;
}
추가적으로, 위 예제에서 사용한 GetExceptionCode() 함수(매크로)는 예외가 발생했을 때 어떠한 종류의 예외가 발생했는지 확인할 수 있는 함수이다. 이는 __except 블록 내에서나, 예외필터 표현식을 지정하는 위치에서만 호출 가능하다는 특징이 있다.
또한, 예외 상수는 EXCEPTION_STACK_OVERFLOW와 같이 이름만으로 충분히 의미가 전달되기 때문에 MSDN을 참고하자.
처리되지 않은 예외의 이동
만약, main 함수가 a 함수를 호출하고, a함수 내의 try블록 내에서 b라는 함수를 호출하고 있고, b 함수 내에서 예외상황이 발생했을 경우 스택은 다음과 같이 구성될 것이다.
main() -> a() -> b()
이러한 예외 상황은 try블록 내에서 발생한 것으로 인식된다. 좀 더 구체적으로 설명하면, b 함수 내에서 예외가 발생하였지만, 해당 함수 내에는 try-except 블록이 존재하지 않기 때문에, 스택의 순서대로, b 함수를 호출하는 a 함수로 예외처리가 넘어가게 된다. 즉, 예외처리가 스택의 구조상 위에서 아래로 이동한다. 만약, a에도 try-except 블록이 없다면, main 함수로 예외처리가 넘어간다.
핸들러의 중복
예외 핸들러는 중복이 가능하다. 따라서, 다음과 같은 활용이 가능하다. 이는 finally 블록에 의해 영역 3의 코드를 실행시킬 수 있게 되는 것이다.
__try
{
__try
{
// 영역 1
}
__finally
{
// 영역 2
}
}
__except
{
//영역 3
}
EXCEPTION_CONTINUE_SEARCH
이는 다른 곳에 있는 예외 핸들러를 통해 예외를 처리하라는 것이다. 다른 예외를 찾기 위해 앞서 처리되지 않은 예외의 이동과 유사하게, 스택 내의 쌓여있는 순서를 바탕으로 예외 핸들러를 찾는다. 따라서, 이는 예외가 처리되어야 하는 위치를 별도로 지정하기 위해 사용된다.
04. 소프트웨어 기반의 개발자 정의 예외
소프트웨어 예외(Software Exceptions)의 발생
앞서 언급한 바와 같이, 소프트웨어 예외는 개발자가 임의로 종류를 수정 및 추가가 가능하다. 이때 사용하는 함수는 다음과 같다.
void RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
CONST ULONG_PTR* lpArguments
);
해당 함수는 예외발생을 알리기 위한 용도로 사용된다. 이 함수가 호출되면 SEH 매커니즘이 작동되면서 앞서 공부한 흐름으로 예외처리가 진행된다. 첫 번째 인자를 통해 예외의 정보를 추가할 수 있으며, GetExceptionInformain 함수를 통해 예외 상황이 발생했을 때, GetExceptionCode보다 더 많은 정보를 얻을 수 있는데, 이를 세 번째, 네 번째 인자를 통해 결정할 수 있다.
참고 자료:
윤성우. 『뇌를 자극하는 윈도우즈 시스템 프로그래밍』.한빛미디어, 2007.
'독서 > [ 뇌를 자극하는 윈도우즈 시스템 프로그래밍 ]' 카테고리의 다른 글
[ 뇌를 자극하는 윈도우즈 시스템 프로그래밍 ] Chapter 18. 파일 I/O와 디렉터리 컨트롤 (0) | 2023.12.19 |
---|---|
[ 뇌를 자극하는 윈도우즈 시스템 프로그래밍 ] Chapter 16. 컴퓨터 구조에 대한 네 번째 이야기 (2) | 2023.11.12 |
[ 뇌를 자극하는 윈도우즈 시스템 프로그래밍 ] Chapter 15. 쓰레드 풀링(Pooling) (1) | 2023.11.11 |
[ 뇌를 자극하는 윈도우즈 시스템 프로그래밍 ] Chapter 14. 쓰레드 동기화 기법 2 (2) | 2023.11.06 |
[ 뇌를 자극하는 윈도우즈 시스템 프로그래밍 ] Chapter 13. 쓰레드 동기화 기법 1 (2) | 2023.11.05 |