확인되지 않은 예외 - 논란의 여지가 있음
Java 프로그래밍 언어에는 Unchecked Exception(RuntimeException, Error 및 그 서브클래스)를 잡거나 지정하는 메서드가 필요하지 않으므로 프로그래머는 Exception만 던지는 코드를 작성하거나 모든 예외 서브클래스가 RuntimeException에서 상속되도록 하고 싶은 유혹을 받을 수 있습니다. 이 두 가지 단축키를 사용하면 프로그래머는 컴파일러 오류에 신경 쓰지 않고 예외를 지정하거나 잡는 데 신경 쓰지 않고 코드를 작성할 수 있습니다. 프로그래머에게는 편리해 보일 수 있지만 catch 또는 specify 요구 사항의 의도를 회피하고 클래스를 사용하는 다른 사용자에게 문제를 일으킬 수 있습니다.
설계자가 메서드에 그 범위 내에서 던질 수 있는 잡히지 않은 검사 예외를 모두 지정하도록 한 이유는 무엇일까요? 메서드가 던질 수 있는 모든 Exception은 메서드의 공용 프로그래밍 인터페이스의 일부입니다. 메서드를 호출하는 사람은 메서드가 던질 수 있는 예외에 대해 알고 있어야 예외에 대해 어떻게 처리할지 결정할 수 있습니다. 이러한 예외는 매개변수 및 반환값만큼이나 해당 메서드의 프로그래밍 인터페이스의 일부입니다.
다음 질문은 다음과 같을 수 있습니다: “메서드가 던질 수 있는 예외를 포함하여 메서드의 API를 문서화하는 것이 그렇게 좋다면 런타임 예외도 명시하지 않는 이유는 무엇인가요?”라는 질문입니다. 런타임 예외는 프로그래밍 문제로 인해 발생하는 문제를 나타내므로 API 클라이언트 코드가 이를 복구하거나 어떤 방식으로든 처리할 것으로 합리적으로 기대할 수 없습니다. 이러한 문제에는 0으로 나누기 등의 산술 예외, 널 참조를 통해 객체에 액세스하려고 하는 등의 포인터 예외, 너무 크거나 작은 인덱스를 통해 배열 요소에 액세스하려고 하는 등의 인덱싱 예외 등이 있습니다.
런타임 예외는 프로그램의 어느 곳에서나 발생할 수 있으며, 일반적인 프로그램에서는 그 수가 매우 많을 수 있습니다. 모든 메서드 선언에 런타임 예외를 추가해야 한다면 프로그램의 명확성이 떨어질 수 있습니다. 따라서 컴파일러는 런타임 예외를 캐치하거나 지정할 것을 요구하지 않습니다(할 수는 있지만).
RuntimeException가 발생하는 일반적인 경우 중 하나는 사용자가 메서드를 잘못 호출하는 경우입니다. 예를 들어, 메서드는 인자 중 하나가 잘못 null인지 확인할 수 있습니다. 인수가 널인 경우 메서드는 확인되지 않은 예외인 NullPointerException을 던질 수 있습니다.
일반적으로 메서드가 던질 수 있는 예외를 지정하는 것이 귀찮다고 해서 RuntimeException을 던지거나 RuntimeException의 서브클래스를 만들지 마세요.
결론적인 가이드라인은 다음과 같습니다: 클라이언트가 예외에서 복구할 수 있을 것으로 합리적으로 예상되는 경우 예외를 checked exception으로 설정하세요. 클라이언트가 예외에서 복구할 수 있는 방법이 없는 경우에는 unchecked exception로 설정하세요.
예외의 장점
이제 예외가 무엇이고 어떻게 사용하는지 알았으니 이제 프로그램에서 예외를 사용할 때의 이점에 대해 알아볼 차례입니다.
이점 1: 오류 처리 코드와 “일반” 코드 분리하기
예외는 프로그램의 기본 로직에서 비정상적인 일이 발생했을 때 수행해야 할 작업에 대한 세부 사항을 분리하는 수단을 제공합니다. 기존 프로그래밍에서 오류 감지, 보고 및 처리는 종종 혼란스러운 스파게티 코드로 이어집니다. 예를 들어 전체 파일을 메모리로 읽어들이는 의사 코드 메서드를 생각해 보세요.
readFile {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}이 기능은 언뜻 보기에는 간단해 보이지만 다음과 같은 잠재적인 오류를 모두 무시합니다.
- 파일을 열 수 없으면 어떻게 되나요?
- 파일 길이를 확인할 수 없으면 어떻게 되나요?
- 충분한 메모리를 할당할 수 없으면 어떻게 되나요?
- 읽기에 실패하면 어떻게 되나요?
- 파일을 닫을 수 없으면 어떻게 되나요?
이러한 경우를 처리하려면 readFile 함수에 오류 감지, 보고 및 처리를 위한 코드가 더 있어야 합니다. 다음은 함수가 어떻게 생겼는지 보여주는 예시입니다.
errorCodeType readFile {
initialize errorCode = 0;
open the file;
if (theFileIsOpen) {
determine the length of the file;
if (gotTheFileLength) {
allocate that much memory;
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
close the file;
if (theFileDidntClose && errorCode == 0) {
errorCode = -4;
} else {
errorCode = errorCode and -4;
}
} else {
errorCode = -5;
}
return errorCode;
}여기에는 오류 감지, 보고, 반환이 너무 많아서 원래 7줄의 코드가 혼란스러워집니다. 더 심각한 문제는 코드의 논리적 흐름도 손실되어 코드가 올바른 작업을 수행하고 있는지, 즉 함수가 충분한 메모리를 할당하지 못하면 파일이 정말 닫히는 것인지 알기 어렵다는 것입니다. 메서드를 작성한 후 3개월이 지나서 수정할 때 코드가 계속 올바른 작업을 수행하고 있는지 확인하는 것은 훨씬 더 어렵습니다. 많은 프로그래머는 이 문제를 단순히 무시하고 프로그램이 충돌할 때 오류가 보고되는 방식으로 해결합니다.
예외를 사용하면 코드의 주요 흐름을 작성하고 다른 곳에서 예외적인 경우를 처리할 수 있습니다. readFile 함수가 기존의 오류 관리 기법 대신 예외를 사용한다면 다음과 같이 보일 것입니다.
readFile {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}예외를 사용하면 오류를 감지, 보고 및 처리하는 작업을 수행하는 수고를 덜 수 없지만 작업을 보다 효과적으로 정리하는 데 도움이 됩니다.
이점 2: 오류를 호출 스택으로 전파하기
예외의 두 번째 장점은 메서드의 호출 스택에 오류 보고를 전파할 수 있다는 점입니다. readFile 메서드가 메인 프로그램에서 호출하는 일련의 중첩된 메서드 호출에서 네 번째 메서드라고 가정해 보겠습니다: method1은 method2를 호출하고, 이 메서드는 method3을 호출하며, 이 메서드는 finally readFile을 호출합니다.
method1 {
call method2;
}
method2 {
call method3;
}
method3 {
call readFile;
}또한 method1이 readFile 내에서 발생할 수 있는 오류에 관심이 있는 유일한 메서드라고 가정해 봅시다. 기존의 오류 알림 기법은 method2와 method3가 readFile이 반환한 오류 코드를 호출 스택 위로 전파하여 오류 코드가 마침내 관심 있는 유일한 메서드인 method1에 도달할 때까지 전파하도록 강제합니다.
method1 {
errorCodeType error;
error = call method2;
if (error)
doErrorProcessing;
else
proceed;
}
errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
proceed;
}
errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
proceed;
}Java 런타임 환경은 호출 스택을 거꾸로 검색하여 특정 예외를 처리하는 데 관심이 있는 메서드를 찾습니다. 메서드는 그 안에서 던져진 예외를 회피하여 호출 스택의 더 위쪽에 있는 메서드가 예외를 잡을 수 있도록 할 수 있습니다. 따라서 오류에 관심이 있는 메서드만 오류 감지에 대해 걱정하면 됩니다.
method1 {
try {
call method2;
} catch (exception e) {
doErrorProcessing;
}
}
method2 throws exception {
call method3;
}
method3 throws exception {
call readFile;
}그러나 의사 코드에서 볼 수 있듯이 예외를 회피하려면 미들맨 메서드에서 약간의 노력이 필요합니다. 메서드 내에서 던질 수 있는 체크된 예외는 모두 throws 절에 지정해야 합니다.
이점 3: 오류 유형 그룹화 및 구분하기
프로그램 내에서 던져지는 모든 예외는 객체이므로 예외를 그룹화하거나 분류하는 것은 클래스 계층 구조의 자연스러운 결과입니다. Java 플랫폼에서 관련 예외 클래스 그룹의 예로는 java.io - IOException 및 그 자손에 정의된 예외 클래스가 있습니다. IOException은 가장 일반적이며 I/O를 수행할 때 발생할 수 있는 모든 타입의 오류를 나타냅니다. 그 자손은 보다 구체적인 에러를 나타냅니다. 예를 들어, FileNotFoundException은 디스크에서 파일을 찾을 수 없음을 의미합니다.
메서드는 매우 특정한 예외를 처리할 수 있는 특정 핸들러를 작성할 수 있습니다. FileNotFoundException 클래스에는 자손이 없으므로 다음 핸들러는 한 가지 타입의 예외만 처리할 수 있습니다.
catch (FileNotFoundException e) {
...
}메서드는 catch 문에 예외의 수퍼클래스를 지정하여 그룹 또는 일반 타입에 따라 예외를 catch할 수 있습니다. 예를 들어, 특정 타입에 관계없이 모든 I/O 예외를 catch하려면 예외 처리기는 IOException 인수를 지정합니다.
catch (IOException e) {
...
}이 핸들러는 FileNotFoundException, EOFException 등을 포함한 모든 I/O 예외를 잡을 수 있습니다. 예외 처리기에 전달된 인수를 쿼리하면 어떤 예외가 발생했는지 자세히 확인할 수 있습니다. 예를 들어 다음을 사용하여 스택 추적을 인쇄합니다.
catch (IOException e) {
// Output goes to System.err.
e.printStackTrace();
// Send trace to stdout.
e.printStackTrace(System.out);
}여기에서 핸들러를 사용하여 Exception을 처리하는 예외 핸들러를 설정할 수도 있습니다.
// A (too) general exception handler
catch (Exception e) {
...
}Exception 클래스는 Throwable 클래스 계층 구조의 맨 위에 있습니다. 따라서 이 핸들러는 핸들러가 잡으려는 예외 외에도 다른 많은 예외를 잡을 수 있습니다. 예를 들어 프로그램에서 사용자에게 오류 메시지를 출력한 다음 종료하는 것이 전부인 경우 이러한 방식으로 예외를 처리할 수 있습니다.
그러나 대부분의 상황에서는 예외 처리기가 가능한 한 구체적이어야 합니다. 그 이유는 처리기가 최상의 복구 전략을 결정하기 전에 가장 먼저 해야 할 일은 어떤 유형의 예외가 발생했는지 파악하는 것이기 때문입니다. 사실상 특정 오류를 캐치하지 않음으로써 핸들러는 모든 가능성을 수용해야 합니다. 너무 일반적인 예외 처리기는 프로그래머가 예상하지 못했고 처리기가 의도하지 않은 예외를 잡아 처리함으로써 코드에 오류가 발생하기 쉬운 환경을 만들 수 있습니다.
앞서 설명한 대로 예외 그룹을 만들고 일반적인 방식으로 예외를 처리하거나 특정 예외 타입을 사용하여 예외를 구분하고 정확한 방식으로 예외를 처리할 수 있습니다.
요약
프로그램은 예외를 사용하여 오류가 발생했음을 나타낼 수 있습니다. 예외를 던지려면 throw 문을 사용하고 Throwable의 자손인 예외 객체를 제공하여 발생한 특정 오류에 대한 정보를 제공합니다. 잡히지 않고 확인된 예외를 던지는 메서드는 선언에 throws 절을 포함해야 합니다.
프로그램은 try, catch, finally 블록을 조합하여 예외를 잡을 수 있습니다.
try블록은 예외가 발생할 수 있는 코드 블록을 식별합니다.catch블록은 특정 타입의 예외를 처리할 수 있는 예외 핸들러라고 하는 코드 블록을 식별합니다.finally블록은 실행이 보장되는 코드 블록을 식별하며, try 블록에 포함된 코드가 실행된 후 파일을 닫고 리소스를 복구하거나 기타 정리를 수행하는 데 적합한 위치입니다.
try 문은 하나 이상의 catch 블록 또는 finally 블록을 포함해야 하며, 여러 개의 catch 블록을 포함할 수 있습니다.
예외 객체의 클래스는 발생한 예외의 타입을 나타냅니다. 예외 객체에는 오류 메시지를 포함하여 오류에 대한 추가 정보가 포함될 수 있습니다. 예외 연쇄를 사용하면 예외는 예외를 발생시킨 예외를 가리킬 수 있고, 예외는 다시 예외를 발생시킨 예외를 가리킬 수 있는 식으로 연결될 수 있습니다.