다음을 통해 공유


CLR Inside Out

손상된 상태 예외 처리

Andrew Pardoe

이 칼럼은 Visual Studio 2010 시험판 버전을 기준으로 합니다. 여기에 포함된 모든 정보는 변경될 수 있습니다.

목차

예외의 정확한 의미
Win32 SEH 예외와 System.Exception
관리 코드와 SEH
손상된 상태 예외
catch (Exception e) 사용은 여전히 잘못된 것
현명한 코딩

완벽하지는 않지만 그냥 적당한 수준의 코드를 작성한 적이 있습니까? 모든 부분이 원활하게 돌아갈 때는 문제 없이 동작하지만, 어떤 부분이 잘못될 경우 어떻게 될지 확신할 수는 없는 코드를 작성한 적이 있습니까? 여러분이 작성 또는 유지 관리해야 했던 코드에도 간단하면서 잘못된 문, 바로 catch (Exception e)가 있을지도 모릅니다. 이 문은 아무 문제 없고 단순해 보이지만 예상과 어긋나게 동작할 경우 많은 문제를 일으키는 원인이 됩니다.

다음 코드와 같은 방식으로 예외를 사용하는 코드를 본 적이 있다면 이 칼럼을 읽어야 합니다.

public void FileSave(String name)
{
    try
    {
      FileStream fs = new FileStream(name, FileMode.Create);
    }
    catch (Exception)
    {
      throw new System.IO.IOException("File Open Error!");
    }
}

이 코드의 오류는 흔한 오류입니다. try 블록에서 실행 중인 코드에 의해 발생할 수 있는 예외만 정확히 포착하는 것보다 모든 예외를 포착하는 코드를 작성하는 편이 더 간단합니다. 그러나 예외 계층 구조의 기반을 포착하면 결국 모든 유형의 예외를 무시하고 IOException으로 변환하게 되는 것입니다.

예외 처리는 대부분의 사람들이 근본적인 이해가 아닌 실용적인 지식만 가진 분야 중 하나입니다. 우선 네이티브 프로그래밍 또는 오래된 대학 교재를 통해 예외 처리를 익한 사람을 위해 배경 정보를 살펴보면서 CLR 관점에서의 예외 처리를 설명하겠습니다. 관리 예외 처리를 능숙하게 다루는 전문가라면 다른 종류의 예외 또는 관리 코드 및 SEH(구조적 예외 처리)에 대해 다루는 섹션으로 건너뛰어도 무관합니다. 어쨌든 뒷부분의 섹션들은 꼭 읽도록 하십시오.

예외의 정확한 의미

예외는 정상적인 프로그램 스레드 실행에서 예상되지 않은 상황이 탐지된 경우 발생하는 신호입니다. 많은 에이전트가 잘못된 상황을 탐지하고 예외를 일으킬 수 있습니다. 프로그램 코드(또는 프로그램 코드가 사용하는 라이브러리 코드)는 System.Exception에서 파생된 유형을 발생시킬 수 있고, CLR 실행 엔진은 예외를 발생시킬 수 있으며, 관리 코드도 예외를 발생시킬 수 있습니다. 실행 스레드에서 발생하는 예외는 AppDomain을 거쳐 네이티브 코드 및 관리되는 코드를 통해 스레드를 따라가며, 프로그램에서 처리되지 않은 경우 운영 체제에 의해 처리되지 않은 예외로 취급됩니다.

예외는 잘못된 무엇인가가 발생했음을 나타냅니다. 모든 관리되는 예외는 유형을 갖지만(예: System.ArgumentException 또는 System.ArithmeticException) 이 유형은 예외가 발생한 컨텍스트에서만 의미를 가집니다. 프로그램은 예외 발생을 일으킨 상황을 알 경우 예외를 처리합니다. 프로그램이 예외를 처리하지 않는다면 그 원인이 되는 문제는 여러 개일 수 있습니다. 그러나 일단 프로그램을 떠난 예외는 매우 포괄적인 의미, 즉 '무언가 잘못되었다'는 의미만을 지닙니다.

Windows는 프로그램이 예외를 처리하지 않음을 확인하면 해당 프로세스를 종료하여 프로그램의 존속 데이터(디스크의 파일, 레지스트리 설정 등)를 보호하려 시도합니다. 예외가 원래는 심각하지 않은, 단순히 예기치 못한 프로그램 상태(예: 빈 스택에서 꺼내기 실패)를 의미했더라도 Windows 입장에서는 이 예외를 적절히 해석할 컨텍스트가 없으므로 심각한 문제로 받아들일 수 있습니다. 한 AppDomain에 있는 하나의 스레드가 하나의 예외를 처리하지 못할 경우 전체 CLR 인스턴스가 중단될 수 있습니다(그림 1 참조).

fig01.gif

그림 1 한 스레드의 처리되지 않은 예외가 전체 프로세스 종료를 유발

예외가 그렇게 위험하다면 대체 왜 그렇게 많이 사용될까요? 모터사이클이나 전기톱과 마찬가지로, 예외의 강력함이 매우 유용하기 때문입니다. 프로그램에서 정상적인 데이터 흐름은 호출과 반환을 통해 함수에서 함수로 이동합니다. 함수에 대한 모든 호출은 스택에 하나의 실행 프레임을 만들고, 모든 반환은 이 프레임을 제거합니다. 전역 상태를 변경하는 것 외에 프로그램의 데이터 흐름은 오로지 연속된 프레임 간에 함수 매개 변수 또는 반환 값으로 데이터를 전달함으로써 이루어집니다. 예외 처리가 없다면 모든 호출자는 호출한 함수의 성공을 확인해야 합니다(아니면 모든 부분이 항상 아무런 문제도 없다고 가정해야 함).

Windows는 예외 처리를 사용하지 않으므로 대부분의 Win32 API는 0이 아닌 값을 반환하여 실패를 나타냅니다. 프로그래머는 호출된 함수의 반환 값을 확인하는 코드로 모든 함수 호출을 래핑해야 합니다. 예를 들어 디렉터리의 파일을 나열하는 방법에 대한 MSDN 설명서의 이 코드는 명시적으로 각 호출의 성공을 확인합니다. FindNextFile(...)에 대한 호출은 반환이 0이 아닌지 확인하는 과정에서 래핑됩니다. 호출이 실패하면 별도의 함수 호출 GetLastError()가 예외 상황의 세부 정보를 제공합니다. 반환 값은 반드시 로컬 함수 범위로 제한되어야 하므로 모든 호출은 다음 프레임에서 성공 여부를 검사해야 합니다.

// FindNextFile requires checking for success of each call 
while (FindNextFile(hFind, &ffd) != 0); 
dwError = GetLastError(); 
if (dwError != ERROR_NO_MORE_FILES) 
{ 
  ErrorHandler(TEXT("FindFirstFile")); 
} 
FindClose(hFind); 
return dwError; 

오류 상황은 예기치 않은 상황이 포함된 함수에서만 해당 함수의 호출자로 전달될 수 있습니다. 예외는 현재 함수 범위에서 벗어나 스택을 올라가며 예기치 않은 상황을 처리할 방법을 아는 프레임에 이를 때까지 함수 실행의 결과를 전달할 수 있습니다. CLR의 실행 시스템(2단계 예외 시스템이라고 함)은 호출자부터 시작하여 예외 처리가 가능한 함수에 이를 때까지 스레드 호출 스택의 모든 선행 항목으로 예외를 전달합니다(여기까지가 1단계).

그런 다음 예외 시스템은 예외가 발생한 위치와 처리되는 위치 사이의 각 호출 스택 프레임 상태를 해제합니다(2단계). 스택이 해제되면서 CLR은 각 프레임이 해제될 때 두 finally 절과 fault 절을 실행합니다. 그런 다음 처리 프레임의 catch 절이 실행됩니다.

CLR은 호출 스택의 모든 선행 항목을 확인하므로 호출자가 catch 블록을 가질 필요는 없습니다. 예외는 스택 상위의 어디에서든 포착이 가능합니다. 프로그래머는 모든 함수 호출의 결과를 즉각 확인하는 대신 예외 발생지로부터 멀리 떨어진 곳에서 오류를 처리하도록 코드를 작성할 수 있습니다. 오류 코드를 사용하려면 프로그래머는 잘못된 상황을 처리할 수 있는 위치에 이를 때까지 모든 스택 프레임에서 오류 코드를 조사해야 합니다. 예외 처리를 사용하면 프로그래머는 스택의 모든 프레임에서 예외를 조사할 필요가 없어집니다.

사용자 지정 예외 유형 발생에 대한 자세한 내용은 "오류 처리: 관리되는 COM+ 서버 응용 프로그램에서 사용자 지정 예외 유형 발생시키기"를 참조하십시오.

Win32 SEH 예외와 System.Exception

예외가 발생한 곳으로부터 멀리 떨어진 지점에서 예외를 포착하는 기능에는 한 가지 흥미로운 부수적 효과가 따릅니다. 프로그램 스레드는 예외가 발생한 지점을 모른 채 호출 스택의 활성 프레임으로부터 프로그램 예외를 수신할 수 있습니다. 그러나 예외가 프로그램이 탐지하는 오류 상황을 나타내지 않는 경우도 일부 있습니다. 프로그램 스레드는 프로그램 외부에서 예외를 일으킬 수도 있기 때문입니다.

스레드 실행이 프로세서 오류를 일으키는 경우 제어는 운영 체제 커널로 넘어가며, 이로써 오류는 SEH 예외로 스레드에 전달됩니다. catch 블록에서 스레드의 스택 어디에서 예외가 발생했는지 알지 못하는 것과 마찬가지로 OS 커널이 SEH 예외를 일으키는 정확한 지점을 알 필요는 없습니다.

Windows는 SEH를 사용하여 OS 예외에 대해 프로그램 스레드에 알립니다. CLR은 일반적으로 SEH 예외에 의해 나타나는 종류의 오류를 차단하기 때문에 관리 코드 프로그래머가 이를 볼 일은 거의 없습니다. 그러나 Windows가 SEH 예외를 일으키면 CLR은 이를 관리 코드로 전달합니다. 관리 코드의 SEH 예외는 드물지만 안전하지 않은 관리 코드는 프로그램이 잘못된 메모리 액세스를 시도했음을 나타내는 STATUS_ACCESS_VIOLATION을 생성할 수 있습니다.

SEH에 대한 자세한 내용은 Microsoft Systems Journal 1997년 1월호에 실린 Matt Pietrek의 기사 "Win32 구조적 예외 처리에 대한 심층 분석"을 참조하십시오.

SEH 예외는 프로그램에서 일으키는 예외와는 종류가 다릅니다. 프로그램이 예외를 일으키는 이유는 빈 스택에서 항목을 꺼내려고 시도하거나 존재하지 않는 파일을 열려고 시도하기 때문입니다. 이러한 모든 예외는 프로그램 실행 컨텍스트에서 의미를 가집니다. SEH 예외는 프로그램 외부의 컨텍스트를 참조합니다. 예를 들어 AV(액세스 위반)는 잘못된 메모리에 쓰기가 시도되었음을 나타냅니다. 프로그램 오류와 달리 SEH 예외는 런타임 프로세스의 무결성이 손상되었을 수 있음을 나타냅니다. 그러나 SEH 예외가 System.Exception에서 파생되는 예외와 다르다 해도 CLR이 관리 스레드로 SEH 예외를 전달하는 경우 catch (Exception e) 문으로 포착이 가능합니다.

일부 시스템은 이러한 두 종류의 예외를 구분하려 시도합니다. /EH 스위치를 사용하여 프로그램을 컴파일할 경우 Microsoft Visual C++ 컴파일러는 C++ throw 문에 의해 발생한 예외와 Win32 SEH 예외를 구분합니다. 일반적인 프로그램은 자신이 일으키지 않은 오류를 어떻게 처리해야 하는지 모르므로 이러한 구분은 유용합니다. std::vector에 요소를 추가하려 시도하는 C++ 프로그램은 메모리 부족으로 이 작업이 실패할 수 있음을 예상해야 합니다. 그러나 잘 작성된 라이브러리를 사용하는 올바른 프로그램이 액세스 위반을 처리할 것이라는 예상은 금물입니다.

이 구분은 프로그래머에게 유용합니다. AV는 심각한 문제입니다. 중요한 시스템 메모리에 대한 예기치 못한 쓰기는 프로세스에 예측할 수 없는 영향을 미칠 수 있기 때문입니다. 그러나 일부 SEH 오류(예: 잘못된 사용자 입력 및 이에 대한 확인 누락으로 발생하는 0으로 나누기 오류)는 그만큼 심각하지는 않습니다. 0으로 나누기가 포함된 프로그램은 잘못된 것이기는 하지만 시스템의 다른 부분에 영향을 미칠 가능성은 낮기 때문입니다. 사실 C++ 프로그램은 시스템의 나머지 부분을 불안정화하지 않고도 0으로 나누기를 처리할 수 있을 가능성이 높습니다. 따라서 이 구분은 유용하긴 해도 관리 프로그래머에게 필요한 의미 체계를 나타내지는 않습니다.

관리 코드와 SEH

CLR은 항상 프로그램 자체에서 일으킨 예외와 동일한 메커니즘을 사용하여 관리 코드에 SEH 예외를 전달했습니다. 코드가 적절히 처리할 수 없는 예외적 상황을 처리하려고 시도하지만 않는다면 이는 문제가 되지 않습니다. 대부분의 프로그램은 액세스 위반 이후 안전하게 실행을 계속할 수 없습니다. 아쉽게도 CLR의 예외 처리 모델은 프로그램이 System.Exception 계층 구조의 최상위에서 모든 예외를 포착하도록 허용함으로써 사용자가 이러한 심각한 오류를 포착하도록 종용해왔습니다. 그러나 대부분의 경우 이는 올바른 방법이 아닙니다.

catch (Exception e)를 작성하는것은 일반적인 프로그래밍 오류입니다. 처리되지 않은 예외에는 심각한 결과가 뒤따르기 때문입니다. 그러나 함수에서 어떤 오류를 일으킬지 알지 못한다면 프로그램에서 해당 함수를 호출할 때 가능한 모든 오류로부터 보호해야 한다고 주장할 수 있습니다. 언뜻 타당한 조치처럼 보이겠지만, 프로세스 상태가 손상되었을 가능성이 있는 상황에서 실행을 계속할 경우를 생각해 보십시오. 중단한 후 다시 시도하는 것이 최선의 방법인 경우도 있습니다. Watson 대화 상자를 반길 사람은 없지만 데이터가 손상되는 것보다는 프로그램을 다시 시작하는 것이 낫습니다.

이해하지 못하는 컨텍스트에서 발생한 예외를 포착하는 프로그램은 심각한 문제가 됩니다. 그러나 예외 사양 또는 다른 계약 메커니즘으로는 문제를 해결할 수 없습니다. 또한 CLR은 많은 종류의 응용 프로그램과 호스트를 위한 플랫폼이므로 관리 프로그램이 SEH 예외 알림을 받을 수 있어야 한다는 것이 중요합니다. SQL Server와 같은 일부 호스트의 경우 응용 프로그램의 프로세스에 대한 완전한 제어가 가능해야 합니다. 네이티브 코드와 상호 운용되는 관리 코드는 종종 네이티브 C++ 예외 또는 SEH 예외를 처리해야 합니다.

그러나 catch (Exception e)를 작성하는 대부분의 프로그래머는 액세스 위반을 포착하려 하지 않습니다. 이들은 심각한 오류가 발생할 경우 프로그램이 알 수 없는 상태로 불안정하게 실행되도록 두느니 프로그램 실행을 중지하는 편을 선호합니다. 관리되는 추가 기능을 호스팅하는 Visual Studio 또는 Microsoft Office와 같은 프로그램의 경우 특히 더 그렇습니다. 추가 기능이 액세스 위반을 일으킨 후 예외를 무시할 경우 호스트는 무엇이 잘못되었는지 알지도 못한 채 자체 상태나 사용자 파일에 손상을 가할 수 있습니다.

CLR 버전 4에서 제품 팀은 손상된 프로세스 상태를 나타내는 예외를 다른 모든 예외와 구별되도록 하고 있습니다. 저희 팀은 현재 손상된 프로세스 상태를 나타내는 십여 개의 SEH 예외를 지정 중입니다. 이 지정은 예외 유형 자체가 아닌 예외가 발생한 컨텍스트와 관련됩니다. 이는 Windows에서 수신한 액세스 위반은 CSE(손상된 상태 예외)로 표시되지만 throw new System.AccessViolationException 작성으로 사용자 코드에서 발생한 액세스 위반은 CSE로 표시되지 않음을 의미합니다. PDC 2008 참석자에게는 이러한 변경 내용이 포함된 Visual Studio 2010 CTP(Community Technology Preview)가 배포되었습니다.

예외가 프로세스를 손상시키지 않는다는 점이 중요합니다. 예외는 프로세스 상태에서 손상이 발견된 후에 발생합니다. 예를 들어 안전하지 않은 코드의 포인터를 통한 쓰기가 프로그램에 속하지 않은 메모리를 참조할 경우 액세스 위반이 발생합니다. 운영 체제가 메모리의 소유권을 확인하고 동작을 차단하므로 잘못된 쓰기가 실제 발생하지는 않습니다. 액세스 위반은 스레드 실행에서 앞선 시점에 포인터 자체가 손상되었음을 나타냅니다.

손상된 상태 예외

버전 4부터 CLR 예외 시스템은 코드가 손상된 상태 예외를 처리할 수 있음을 명시적으로 나타내지 않았다면 CSE를 관리 코드로 전달하지 않습니다. 이는 관리 코드의 catch (Exception e) 인스턴스에 CSE가 제공되지 않음을 의미합니다. CLR 예외 시스템 내부에서 변경을 수행하면 예외 계층 구조를 변경하거나 관리 언어의 예외 처리 구문을 바꿀 필요가 없습니다.

호환성을 위해 CLR 팀은 다음과 같이 기존 동작으로 기존 코드를 실행할 수 있는 방법을 제공했습니다.

  • Microsoft .NET Framework 3.5에서 작성된 코드를 소스 업데이트 없이 다시 컴파일하여 .NET Framework 4.0에서 실행하려면 응용 프로그램 구성 파일에 legacyCorruptedStateExceptionsPolicy=true 항목을 추가하면 됩니다.
  • .NET Framework 3.5 또는 이전 버전의 런타임을 대상으로 컴파일된 어셈블리는 .NET Framework 4.0에서 실행될 때 손상된 상태 예외를 처리할 수 있습니다(즉, 기존 동작을 유지할 수 있음).

코드에서 CSE를 처리하도록 하려면 새 특성, 즉 System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions가 있는 예외 절(catch, finally 또는 fault)이 포함된 함수를 만들어 이러한 의도를 나타내야 합니다. CSE가 발생하면 CLR은 일치하는 catch 절 검색을 수행하지만 검색 대상은 HandleProcessCorruptedStateExceptions 특성으로 표시된 함수로 제한됩니다(그림 2 참조).

그림 2 HandleProcessCorruptedStateExceptions 사용

// This program runs as part of an automated test system so you need
// to prevent the normal Unhandled Exception behavior (Watson dialog).
// Instead, print out any exceptions and exit with an error code.
[HandledProcessCorruptedStateExceptions]
public static int Main()
{
    try
    {
        // Catch any exceptions leaking out of the program
        CallMainProgramLoop();
    }
    catch (Exception e) // We could be catching anything here
    {
        // The exception we caught could have been a program error
        // or something much more serious. Regardless, we know that
        // something is not right. We'll just output the exception
        // and exit with an error. We won't try to do any work when 
        // the program or process is in an unknown state!
        System.Console.WriteLine(e.Message);
        return 1;
    }
    return 0;
  } 

적절한 catch 절이 발견되면 CLR은 평소와 같이 스택을 해제하지만 이 특성으로 표시된 함수에서만 finally 및 fault 블록을 실행합니다. 부분적으로 신뢰할 수 있는 코드 또는 투명 코드에 나타나는 HandleProcessCorruptedStateExceptions 특성은 무시됩니다. 신뢰할 수 있는 호스트는 신뢰할 수 없는 추가 기능이 이러한 심각한 예외를 포착 및 무시하기를 원하지 않을 것이기 때문입니다.

catch (Exception e) 사용은 여전히 잘못된 것

CLR 예외 시스템이 가장 심각한 예외를 CSE로 표시한다 해도 코드에서 catch (Exception e)를 사용하는 것은 여전히 좋은 방법이 아닙니다. 예외는 다양한 종류의 예기치 못한 상황을 나타냅니다. CLR은 가장 심각한 예외(손상되었을 가능성이 있는 프로세스 상태를 나타내는 SEH 예외)를 탐지할 수 있습니다. 그러나 무시되거나 포괄적으로 처리되는 다른 예기치 못한 상황이 여전히 해를 끼칠 수 있습니다.

프로세스 손상이 없으면 CLR은 프로그램 정확성과 메모리 안전성에 대해 상당히 강력한 보장을 제공합니다. 안전한 MSIL(Microsoft Intermediate Language) 코드로 작성된 프로그램을 실행할 경우 프로그램의 모든 명령이 정확하게 실행됨을 확신할 수 있습니다. 그러나 프로그램 명령이 지시하는 동작을 수행하는 것과 프로그래머가 원하는 것을 수행하는 것은 서로 다른 경우가 많습니다. CLR 관점에서 완벽하게 올바른 프로그램도 지속 상태(예: 디스크에 쓰인 프로그램 파일)를 손상시킬 수 있습니다.

고등학교 시험 점수 데이터베이스를 관리하는 프로그램을 간단한 예로 들어 보겠습니다. 프로그램은 개체 지향 디자인 원칙을 사용하여 데이터를 캡슐화하고 관리되는 예외를 일으켜 예기치 못한 이벤트를 나타냅니다. 어느날 교직원이 성적 파일을 생성하면서 Enter 키를 너무 많이 누릅니다. 프로그램은 빈 큐에서 값을 꺼내려 시도하고, 처리되지 않은 채 호출 스택의 프레임을 통과하는 QueueEmptyException을 발생시킵니다.

스택의 최상위 근처에 GenerateGrades()라는 함수가 있고 이 함수에는 예외를 포착하는 try/catch 절이 있습니다. 불행히도 GenerateGrades()는 학생이 큐에 저장되어 있음을 알지 못하며 QueueEmptyException을 어떻게 처리해야 할지도 모릅니다. 그러나 GenerateGrades()를 작성한 프로그래머는 프로그램이 지금까지 계산된 데이터를 저장하지 않은 채 충돌을 일으키길 원하지 않습니다. 따라서 모든 내용이 안전하게 디스크에 쓰이고 프로그램이 종료됩니다.

이 프로그램의 문제는 부정확할 수 있는 여러 가정을 전제한다는 점입니다. 마지막에 학생 큐에 누락된 항목이 있다면 어떻게 될까요? 첫 번째, 또는 열 번째 학생 레코드를 건너뛸 수 있습니다. 예외는 프로그래머에게 프로그램이 잘못되었다는 점만 알릴 뿐입니다. 여기서는 어떤 조치든(데이터를 디스크에 저장하거나 "복구"하고 실행을 계속함) 올바르지 않습니다. 예외가 발생한 컨텍스트를 알지 못하면 아무런 수정 작업도 수행할 수 없습니다.

프로그램이 예외가 발생한 지점과 가까운 곳에서 예외를 포착했다면 적절한 조치가 가능했을 수도 있습니다. 프로그램은 큐에서 학생을 빼는 함수에서 QueueEmptyException의 의미를 압니다. 함수가 유형별로 해당 예외를 포착한다면(전체 예외 유형을 포착하지 않고) 프로그램 상태를 올바르게 하기가 훨씬 더 유리하게 됩니다.

일반적으로 특정 예외 포착은 예외 처리기에 최대한의 컨텍스트를 제공하므로 바람직한 방법입니다. 두 개의 예외를 포착할 수 있는 코드는 두 개의 예외를 모두 처리할 수 있어야 합니다. catch (Exception e)가 포함된 코드는 말 그대로 모든 예외적인 상황을 처리할 수 있어야 합니다. 그러나 실제로 그렇게 하기는 매우 어렵습니다.

일부 언어에서는 프로그래머가 광범위한 종류의 예외를 포착하지 못하도록 합니다. 예를 들어 C++에는 예외 사양이 있는데, 이는 프로그래머가 해당 함수에서 발생 가능한 예외를 지정할 수 있도록 하는 메커니즘입니다. Java는 더 나아가 특정 종류의 예외를 지정해야 하는 컴파일러 강제 요구 사항인 확인 예외를 사용합니다. 두 언어 모두 프로그래머는 함수 선언에 이 함수에서 나올 수 있는 예외를 나열하고, 호출자는 이러한 예외를 처리해야 합니다. 예외 사양은 괜찮은 개념이지만 실제 사용 시의 결과는 반반입니다.

모든 관리 코드가 CSE를 처리할 수 있어야 하는지에 대해서는 활발한 논의가 진행 중입니다. 이러한 예외는 일반적으로 시스템 수준 오류를 나타내며 시스템 수준 컨텍스트를 아는 코드에 의해서만 처리되어야 합니다. 대부분의 사람들에게는 CSE를 처리하는 기능이 필요 없지만 두 가지 시나리오에서는 이 기능이 필요합니다.

하나는 예외가 발생한 위치에 매우 근접해 있는 경우입니다. 예를 들어 프로그램이 네이티브 코드를 호출하는데 이 코드에 버그가 있는 것으로 알려진 경우를 가정해 보겠습니다. 프로그래머는 코드를 디버깅하면서 이 코드가 가끔 포인터에 액세스하기 전에 해당 포인터를 제거하고, 이로 인해 액세스 위반이 발생한다는 사실을 발견합니다. 프로그래머는 포인터 손상의 원인을 알고 프로세스 무결성이 유지된다는 사실에 안심하기 때문에 P/Invoke를 사용하여 네이티브 코드를 호출하는 함수에 HandleProcessCorruptedStateExceptions 특성을 사용하고자 할 수 있습니다.

이 특성을 사용해야 하는 두 번째 시나리오는 오류에서 최대한 멀리 떨어진 경우입니다. 사실 프로세스를 종료하기 위한 준비가 거의 된 상태입니다. 오류가 발생할 경우 사용자 지정 로깅을 수행하는 호스트 또는 프레임워크를 작성했다고 가정해 보겠습니다. try/catch/finally 블록으로 main 함수를 래핑하고 HandleProcessCorruptedStateExceptions로 표시할 수 있습니다. 오류가 예기치 못하게 프로그램의 main 함수에까지 영향을 미칠 경우 로그에 일부 데이터를 작성하고 최소한의 할 일만 수행한 후 프로세스를 종료합니다. 프로세스의 무결성을 확신할 수 없다면 어떤 작업이든 위험할 수 있지만 사용자 지정 로깅이 실패하는 경우에는 허용되기도 합니다.

그림 3의 다이어그램을 보십시오. 함수 1(fn1())에는 [HandleProcessCorruptedStateExceptions] 특성이 있으므로 이 함수의 catch 절은 액세스 위반을 포착합니다. 함수 3의 finally 블록은 함수 1에서 예외가 포착되더라도 실행되지 않습니다. 스택 가장 아래의 함수 4는 액세스 위반을 일으킵니다.

fig03.gif

그림 3 예외와 액세스 위반

이 두 시나리오에서는 수행하는 작업이 완전히 안전하다는 보장이 없지만 프로세스 종료를 허용할 수 없는 경우도 있습니다. 한편 CSE를 처리하려는 경우 프로그래머는 이를 올바르게 처리해야 한다는 막중한 부담을 떠안게 됩니다. CLR 예외 시스템은 1단계(일치하는 catch 절을 검색할 때) 또는 2단계(각 프레임의 상태를 해제하고 finally 및 fault 블록을 실행할 때) 중에 새 특성으로 표시되지 않은 함수에는 CSE를 전달조차 하지 않는다는 점에 유의하십시오.

finally 블록은 예외가 있는지 여부에 관계없이 항상 코드가 실행되도록 보장하기 위해 존재합니다. fault 블록은 예외가 발생하는 경우에만 실행되지만 항상 실행된다는 보장은 비슷합니다. 이러한 구문은 파일 핸들 해제 또는 가장 컨텍스트 되돌리기와 같은 중요 리소스 정리에 사용됩니다.

CER(제약이 있는 실행 영역) 사용을 통해 안정적으로 작성된 코드라도 HandleProcessCorruptedStateExceptions 특성으로 표시된 함수 내에 있지 않다면 CSE가 발생한 경우 실행되지 않습니다. CSE를 처리하고 프로세스를 안전하게 계속 실행하는 올바른 코드를 작성하기란 매우 어렵습니다.

그림 4의 코드에서 어떤 부분이 잘못될 수 있는지 살펴보십시오. 이 코드가 CSE를 처리할 수 있는 함수에 없다면 액세스 위반이 발생할 때 finally 블록은 실행되지 않습니다. 프로세스가 종료된다면 문제 없습니다. 열린 파일 핸들은 해제됩니다. 그러나 다른 어떤 코드가 액세스 위반을 포착하고 상태를 복원하려 시도하는 경우 이 코드는 이 파일을 닫아야 한다는 것, 그리고 이 프로그램이 변경한 다른 외부 상태를 복원해야 한다는 것을 알아야 합니다.

그림 4 finally 블록은 실행되지 않을 수 있음

void ReadFile(int index)
    {
      System.IO.StreamReader file = 
        new System.IO.StreamReader(filepath);
          try
          {
            file.ReadBlock(buffer, index, buffer.Length);
          }
          catch (System.IO.IOException e)
          {
            Console.WriteLine("File Read Error!");
          }
          finally
          {
            if (file != null)
                {
                    file.Close()
                }
          }
    }

CSE를 처리하기로 결정한다면 여러분의 코드는 해제되지 않은 중요한 상태가 엄청나게 많다는 것을 예상해야 합니다. finally 및 fault 블록은 실행되지 않았습니다. 제한된 실행 영역은 실행되지 않았습니다. 프로그램과 프로세스는 알 수 없는 상태에 있습니다.

코드가 올바르게 동작할 것임을 안다면 여기서 무엇을 해야 하는지는 여러분이 잘 알 것입니다. 그러나 프로그램의 실행 상태를 확신할 수 없다면 그냥 프로세스가 종료되도록 하는 편이 낫습니다. 또는 응용 프로그램이 호스팅되는 경우에는 호스트가 지정한 에스컬레이션 정책을 호출합니다. 안정적인 코드 및 CER 작성에 대한 자세한 내용은 Alessandro Catorcini와 Brian Grunkemeyer의 2007년 12월호 CLR Inside Out 칼럼을 참조하십시오.

현명한 코딩

CLR은 CSE를 마구잡이로 포착하지 못하도록 하지만 어쨌든 지나치게 많은 종류의 예외를 포착하는 것은 좋은 생각이 아닙니다. 그러나 catch (Exception e)는 많은 코드에 사용되고 있고 앞으로 이러한 현상이 바뀔 가능성도 높지 않습니다. 모든 예외를 마구잡이로 포착하는 코드에 손상된 프로세스 상태를 나타내는 예외를 전달하지 않으면 이 코드로 인해 심각한 상황이 더 악화되는 것은 막을 수 있습니다..

다음 번에 예외를 포착하는 코드를 작성하거나 유지 관리할 때는 예외의 의미에 대해 생각해 보십시오. 여러분이 포착한 유형이 프로그램(및 이 프로그램이 사용하는 라이브러리)에서 발생시키도록 정해진 유형과 일치합니까? 여러분은 프로그램이 올바르게, 그리고 안전하게 실행을 계속할 수 있도록 예외를 처리하는 방법을 알고 있습니까?

예외 처리는 신중하게 사용해야 하는 강력한 도구입니다. 이 기능을 꼭 사용해야 한다면, 즉 손상된 프로세스를 나타낼지도 모르는 예외를 꼭 처리해야 한다면 CLR에서는 여러분을 믿고 이 기능을 사용하도록 허용합니다. 신중을 기해 정확하게만 하십시오.

질문이나 의견이 있으면 clrinout@microsoft.com으로 보내시기 바랍니다.

Andrew Pardoe는 Microsoft CLR 담당 프로그램 관리자이며, 데스크톱 및 Silverlight 런타임을 위한 실행 엔진의 다양한 측면에 관여하고 있습니다. 문의 사항이 있으면 Andrew.Pardoe@microsoft.com으로 연락하시기 바랍니다.