예외 (Exception) - 1부 -
2006년 3월 27일 강현신
전통적인 프로그래밍 언어가 가지는 오류 처리의 한계점
컴퓨터를 구성하는 대부분의 시스템의 경우 많은 종류의 작업을 처리하는 과정에서 어떤 특정 명령 혹은 그 명령의 결과가 제어할 수 없는 상황을 발생시키는 경우, 예를 들면 연산의 결과가 변수의 형이 가질 수 있는 최대 값을 벗어나 버렸다든가 현재 어플리케이션에 할당되지도 않은 메모리 주소를 가져오려 했다든가 하는 상황 등이 발생하였을 경우 사용자에게 이러한 상황이 발생하였음을 알린 후 시스템은 작업을 중지하게 된다.
따라서 프로그램이 계속 정상적으로 실행되기 위해서는 작업을 수행하는 도중 이러한 상황을 항상 확인하여 문제가 발생하였을 경우 이를 적절히 해결할 수 있는 코드를 삽입해 두어야 하고, 더불어 이러한 상황을 사용자에게 알려주도록 프로그램 코드를 작성해 두어야만 한다.
문제는 정상적으로 문제점을 해결할 수 있는 코드를 작성했다고 하더라도, 많은 경우 데이터 자체는 정상적이지 않은 경우가 많다는 점이고, 특히 이 점이 더욱 문제가 되는 것은 이제까지 많은 프로그래밍 언어들이 추구하던 컨셉인 imperative language 는 특성상 순차적으로 명령을 처리하기 때문에, 후속 처리를 요하는 코드들이 먼저 발생한 오류에 모두 영향을 받게 된다는 점 때문이다.
따라서 이런 경우 사용자는 최초로 접근 데이터의 무결성에 영향을 줄 수 있는 코드부터 시작하여, 해당 데이터와 관련된 모든 코드에 데이터의 오류 발생 시 처리할 수 있는 방안을 삽입해야 했고, 이러한 방식은 같은 코드의 반복과 잦은 분기문을 유발시켜 전체적인 구조를 난잡하게 만들게 되었다.
더불어 코드들이 재사용성을 위하여 점차 모듈화되면서 외부 모듈이 작업 처리 도중 발생한 오류의 다양한 처리 방식을 내부 코드에서 인식해야 하는 문제가 발생했고, 이는 사용자들의 코드 인식 능력을 더욱 불편하게 만들었다. 게다가 더욱 큰 문제는 오류를 사용자에게 알리기 위한 통로를 단일화하기 어렵다는 문제가 있었다. 예를 들어 A 라는 모듈은 Standard Error 를 통해 오류를 출력하고, B 라는 모듈은 파일 출력을 통하여 오류 메시지를 출력시킨다면 이 두 모듈을 쓰는 C 라는 모듈은 오류 메시지를 보기 위해서 두 통로를 모두 확인해야만 한다. 만약 C 라는 모듈이 구동해야 하는 환경이 Standard Error 와 파일 출력을 지원하지 않는 환경이라면? 그것은 정말로 끔찍한 상황이 아닐 수 없다.
이러한 문제가 발생하는 근본적인 원인은, 오류를 처리하는 코드와 일반적인 상황을 처리하는 코드가 하나로 묶여 있기 때문이다. 그러나 고전적인 프로그래밍 언어에서는 오류를 처리하는 코드와 일반적인 처리를 하는 코드를 나눌 만한 명시적인 방법을 제시하고 있지 않다. 그렇기 때문에 사용자는 알아서 오류 처리 코드를 최대한 복잡하지 않은 형식이 되도록 프로그래밍해야만 했다.
입출력 장치 등의 외부 시스템 처리에서 발생하는 문제
입출력 시스템과 같은 메인 시스템에 종속적으로 구동되지 않는 외부 시스템의 경우에는, 메인 시스템 측에서 호출하는 명령의 처리 방식에 대해 조금 다른 관점으로 접근한다. 일반적으로 이러한 외부 시스템에 명령을 호출하면 예측했던 데이터를 주거나 받는 것이 일반적이지만, 실제로는 외부 시스템의 상태에 따라서 이러한 작업이 불가능하거나 지속되지 못할 수 있으므로 이러한 상황을 알리기 위하여, 제어 신호라 불리는 일반적인 데이터와는 다른 신호를 전달하게 된다.
따라서 프로그램 코드는 일반적인 데이터를 입력받는 상황과 제어 신호를 전달받는 상황을 다르게 처리해야 할 필요성이 있다. 많은 경우 외부 시스템에 대한 데이터 교환은 구조상 조건 반복문을 통하여 이루어지므로 이 두 가지 종류의 신호를 코드상으로 구분하는 데에 있어서 큰 어려움이 없다. 예를 들면 파일에서 데이터를 읽어오는 경우, 반복문 안에서는 데이터를 읽는 코드를 삽입하고, 조건문에서는 읽어들인 데이터가 실제로는 파일 읽기 오류나 파일 종료와 같은 제어코드가 아닌지만을 판별하면 되는 식이다. 하지만 종종 몇몇 환경에서는 이러한 처리로 다 소화할 수 없는 상황이 발생할 수 있는데, 이러한 경우는 전술했던 시스템 오류 처리 방식과 동일한 문제에 처하게 된다.
예외 처리의 기본 컨셉
예외는, 이러한 두 가지 문제. 즉 메인 시스템의 오류와 외부 시스템의 제어 신호 발생을 하나로 묶어, 원래 일반적인 처리가 되어야 하는 상황과는 다른 상황이 발생하였다는 것으로 추상화시킨 개념을 말한다.
예외는 말 그대로 예외적인 상황에서 발생하므로 예외적인 상황이 발생하지 않았을 때 수행되는 기본적인 명령어 흐름으로는 이를 처리할 수 없다. 따라서 이러한 예외를 묘사하기 위해서는 프로그램을 처리하는 주 흐름 (Main Flow) 을 벗어나는 다른 흐름을 만들어야 한다. 이러한 흐름을 우리는 예외 흐름 (Exception Flow) 이라고 부를 수 있으며, 이러한 예외 흐름을 통하여 위에서 언급했던, 오류 및 제어 신호 처리 코드와 일반 데이터 처리 코드의 혼재를 피할 수 있게 된다.
예외 처리 이후의 세 가지 분기
예외 처리 코드는 해당 예외를 처리하고 나서 다음 흐름을 결정하게 되는데 그 흐름은 사용자가 어떻게 코드를 구성하였느냐에 따라서 다음의 3가지로 구분된다.
1. Main Flow 로 복귀
프로그램의 주 처리 흐름으로 복귀한다. 따라서 이후 작업은 일반적인 방식으로 처리된다. 이 경우는 모든 데이터 및 상태가 주 처리 흐름에 문제가 없도록 완전히 보정되었을 때 주로 이루어진다.
2. 다음 Exception 처리 구문으로 작업 인계
다음 단계의 예외 처리 구문으로 작업을 인계한다. 이 경우는 적절한 복구 작업을 수행할 수 없으므로 해당 처리 작업이 정상적으로 복귀될 수 있는 상태가 될 때까지 주 처리 흐름을 건너뛰고 모두 다음 예외 처리 구문에 맡기겠다는 것이다.
3. 프로그램 종료
최근 일반적으로 사용되는 무한 반복 구조의 프로그램에서는 (DAEMON 등) 대부분 어떠한 오류가 발생한다고 하더라도 최종적으로는 주 처리 흐름의 최종단에 이르는 메인 루프로 복귀하게 하는 것이 원칙이지만, 아주 극단적인 상황의 경우 (예를 들면 중대한 역할을 하는 서브 시스템이 더 이상 작동하지 않는다든가 등) 에는 어쩔 수 없이 현재 프로그램을 완전히 종료시켜야만 하는 경우가 있다. 이 경우는 이에 해당한다.
Imperative Language 에서 사용하는 예외 처리의 기본 방식
시간에 따라 순서대로 명령을 처리하는 Imperative Language 에서는 명령어 처리 자체가 1차원적인 구조를 가지며, 따라서 개념적으로 2차원적인 처리를 요구하는 예외 처리를 구현하기 위해서 특정한 코드 블록을 예외 처리 블록으로 지정하게 된다. 이는 조건 분기문이 각각의 조건 처리를 위해 별도의 블록을 지정하는 것과 동일한 이치이다.
이러한 개념을 이미지상으로 그리면 다음과 같다.
다음 편에서는 C++, Java, C# 의 경우를 들어 각각의 언어들이 어떠한 방식으로 예외 처리를 진보시켜왔는지를 다룰 것입니다.
반응형