예외 던지기 (Exception Throwing)
키워드 throw을 사용하여 프로그래머가 고의로 예외를 발생 시킨 후에 던질수 있다.
public class Throw {
public static void main(String[] args) {
try {
Exception e = new Exception("고의로 발생시킨 예외"); // 문자열을 Exception 인스턴스의 message에 저장
throw e;
} catch (Exception e) {
System.out.println("Error message: " + e.getMessage());
e.printStackTrace();
}
}
}
throw란 키워드는 Error나 Exception 던져서 catch 한다는 의미이다. 그리고 Error와 Exception 클래스의 최상위 계층은 Throwable 클래스이다. 즉, Throwable 클래스는 error나 exception를 던질수 있는 클래스란 의미이다.
Exception 생성자에 할당한 문자열은 생성된 예외 인스턴스에 message로 저장된다. 이 메세지는 getMessage()를 호출하여 얻을수 있는 값이다.
다음 코드는 위의 코드에서 예외를 생성하여 던지는 코드를 한줄로 줄인 코드이다. Exception 객체 생성후 바로 throw한 것이다.
public class Throw {
public static void main(String[] args) {
try {
throw new Exception("고의로 발생시킨 예외");
} catch {
System.out.println("Error message: " + e.getMessage());
e.printStackTrace();
}
}
}
아래의 코드는 Exception 객체를 던지는 코드를 명시하였기 때문에, 예외가 처리되어야 할 부분에 예외를 처리해주지 않으면 컴파일 에러가 발생한다.
public class Throw {
public static void main(String[] args) {
throw new Exception("고의로 발생시킨 예외");
}
}
런타임에 발생하는 RuntimeException 이외의 사용자들의 특정 작업에 의해서 발생되는 예외 클래스가 Exception 클래스의 하위 클래스이다. 이러한 checked exception는 컴파일 시점에서 예외를 확인하기 때문에 컴파일에러가 발생한다.
그러나 RuntimeException 객체를 던지는 코드는 런타임에 발생하는 unchecked exception이므로, 예외를 명시하여도 컴파일 에러는 발생하지 않는다.
public class Throw {
public static void main(String[] args) {
throw new RuntimeException("고의로 발생시킨 예외");
}
}
만일 RuntimeException 클래스에 속하는 예외가 발생 가능성이 있는 코드에도 예외처리를 필수적으로 해야 했다면, 다음과 같은 배열만 생성 및 선언하여 배열 요소를 읽는 간단한 코드에서도 참조 변수와 배열이 사용되는 모든 곳에 예외처리를 해야할 것이다. 그리하여 RuntimeException은 예외처리를 선택적으로 할 수 있도록 되었다.
public class Unchecked {
public static void main(String[] args) {
try {
int[] arr = new int[10];
System.out.println(arr[11]);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
}
}
메서드 예외 선언 (Method throws Declaration)
메서드 예외 선언의 역할과 적용
메서드 선언부에 throws 키워드로 발생할 예외를 선언할 수 있다.
public class ThrowToMethod {
public static void method() throws Exception {
throw new Exception();
}
public static void main(String[] args) {
try {
method();
} catch (Exception e) {
e.printStackTrace();
}
}
}
메서드에 throws키워드로 예외를 명시하는 것은 해당 메서드에서 발생한 예외를 처리하는 것이 아니라, 이 예외가 발생할 수 있는 메서드에서 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다. 이렇게 호출한 메서드로 예외 객체를 던져 호출한 메서드에게 예외처리를 위임하기 위해서, 메서드에 예외를 선언하는 방식이 고안되었다.
메서드 선언부에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드를 보았을때, 이 메서드를 사용하기 위하여 어떤 예외들을 처리되야하는지 파악하기 쉽다. Java API 문서에서 사용하고자 하는 메서드의 선언부를 보고 어떤 예외가 발생하고 처리해야하는지를 파악도 필요하다.
메서드의 예외 선언에 하나 덧붙이자면, 메서드에 예외를 선언할때 일반적으로 반드시 처리해주어야하는 예외들만 선언한다. 그래서 잘못된 프로그램 작성에 의해서 런타임에 발생되는 예외 클래스인, RuntimeException 클래스와 그 하위 클래스들은 어떠한 코드로 인한 에러가 발생할지 모르기 때문에 선언하지 않는다.
예외를 전달 받은 메서드가 또 다시 자신을 호출한 메서드에게 전달할 수 있으며, 이런식으로 호출 스택에 있는 메서들을 따라 전달되다가 제일 마지막에 있는 main 메서드에서도 처리되지 않으면 비정상적으로 종료되고, 처리하지 못한 예외는 JVM의 예외처리기가 받아서 예외의 원인을 화면에 출력한다.
public class ThrowToMethod {
public static void method1() throws Exception {
method2();
}
public static void method2() throws Exception {
throw new Exception();
}
public static void main(String[] args) throws Exception {
method1();
System.out.println("after method1"); // 출력 안함
}
}
/*
JVM의 예외처리기가 받아서 예외의 원인을 화면에 출력 (어떤 스레드에서 예외가 발생했는지도 출력)
Exception in thread "main" java.lang.Exception
at ExceptionHandle.ThrowToMethod2.method2(ThrowToMethod2.java:9)
at ExceptionHandle.ThrowToMethod2.method1(ThrowToMethod2.java:5)
at ExceptionHandle.ThrowToMethod2.main(ThrowToMethod2.java:13)
*/
예외가 발생한 호출 스택(call stack)에 있었던 메서드의 정보와 예외 메세지를 한번에 출력하며, 수행을 완료하여 스택에서 제거된 메서드부터 차례로 출력된다. 그리고 어떤 스레드에서 예외가 발생했는지를 알 수 있다.
위의 예제에서는 method2() 메서드에서 예외 객체가 생성되었고 호출한 method1()메서드로 예외 객체를 throw하여 던졌다. 그리고 method1()메서드에서도 예외를 처리해주지 않고 main()메서드로 던졌다. method1() 메서드에 throw 키워드가 없어도 Exception예외를 선언해주었으므로 예외 객체가 던져질수 있는 것이다. 이후, main()메서드에서 전달 받은 예외 객체를 catch하지 않아서 비정상 종료된다. 그러므로 여러 메서드에서 던져지던 예외는 결국 메서드 한곳에서 try-catch문으로 예외를 처리해주어야한다.
다음 코드는 마지막으로 실행되는 main 메서드에서 try-catch문으로 예외를 처리해주었다.
public class ThrowToMethod {
public static void method1() throws Exception{
method2();
}
public static void method2() throws Exception{
throw new Exception();
}
public static void main(String[] args) throws Exception {
try {
method1();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
예외 클래스의 인스턴스의 printStackTrace() 메서드 출력문
java.lang.Exception
at ExceptionHandle.ThrowToMethod1.method2(ThrowToMethod1.java:16)
at ExceptionHandle.ThrowToMethod1.method1(ThrowToMethod1.java:12)
at ExceptionHandle.ThrowToMethod1.main(ThrowToMethod1.java:21)
*/
위의 코드에서 Throwable 클래스의 printStackTrace() 메서드는 JVM 예외처리기(UncaughtExceptionHandler)와는 다르게, 어떤 스레드에서 예외가 발생했지는 안나오지만 호출 스택(Call Stack)에 있었던 메서드의 정보와 예외 메세지를 한번에 출력한다.
public class ThrowToMethod3 {
public static void method1() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace(); // 예외가 발생한 호출 스택(call stack)에 있었던 메서드의 정보와 예외 메세지를 한번에 출력
}
}
public static void main(String[] args) {
method1();
}
}
메서드간 예외 전파 과정
예외가 발생하면 현재 메서드의 실행이 즉시 중단되고, 예외 클래스의 객체는 호출 스택을 통해 전파되면서 예외 처리할 코드를 찾는다. 이 과정은 일반적인 메서드 반환과는 다르며, 예외 처리 메커니즘을 통해 이루어진다.
- 예외 발생: 예외가 발생되거나 throw 키워드를 사용하여 예외를 발생시킨다. 참고로 예외가 throw되면 에외 객체를 호출한 메서드로 return을 하는 것은 아니다.
- 현재 메서드 종료: 현재 메서드의 실행이 즉시 중단된다.
- 호출 스택 전파: 예외는 호출 스택을 따라 상위 메서드로 전파된다. (메서드에 선언한 예외의 타입과 일치하거나 하위 클래스이면, 자동으로 예외 객체를 호출한 메서드로 전파된다.)
- 예외 처리: 상위 메서드에서 적절한 catch 블록을 찾으면 해당 블록에서 예외가 처리됩니다. 적절한 catch 블록이 없으면 예외는 계속 전파되며, 최종적으로 JVM에 의해 처리되어 프로그램이 비정상적으로 종료된다.
try 블럭에서 Exception 예외 객체가 throw하여 즉시 예외 객체를 참조가능한 catch 블럭의 참조변수 타입이이 있는지 호출 스택(call stack)을 따라 method()2 -> method1() -> main()을 따라서 확인한다.
public class ThrowToMethod1 {
public static void method1() throws Exception {
method2();
System.out.println("after method2");
}
public static void method2() throws Exception{
throw new Exception();
}
public static void main(String[] args) throws Exception {
try {
method1();
System.out.println("Out try-catch");
} catch (Exception e) {
e.printStackTrace();
}
}
}
method2()메서드 실행 후 예외가 발생하여 즉각적으로 예외 처리하는 코드를 찾기 때문에, main()메서드의 System.out.println("Out try-catch"); 코드와 method1()메서드의 System.out.println("after method2")는 실행되지 못한다.
만일, 발생한 예외와 일치하는 예외를 처리하지 못하여도 비정상적으로 프로그램이 종료되기 때문에 실행되진 않는다. 위의 코드에서는 main() 메서드에서 발생한 예외와 일치하는 catch 블럭을 찾고서 catch 블럭 내 코드를 실행하고 try-catch문을 빠져나간다.
메서드 예외 선언의 필요성
어떠한 메서드에서 a작업이 성공적으로 완료된 후에 b작업을 수행해야한다. 헌데, a작업은 예외가 발생할 수 있는 작업이라면 try 블럭에서 a작업을 수행 후에 b작업을 수행해야하고, catch 블럭에서 예외 발생에 대한 후속 조치에 대한 작업을 수행해야한다.
a작업과 b작업이 각각 메서드A와 메서드B를 호출하여 수행하는 작업이라면, 메서드A는 예외를 호출한 메서드로 던져야한다. 호출한 메서드에서 a작업의 정상 수행 여부에 따라 b작업을 수행할지 말지 결정할 수 있기 때문이다.
아래의 예시에서 메서드A가 readFile 메서드이고 메서드B가 transformContentToUppercase메서드이다.
class ExceptionHandlingMethod {
public static void main(String[] args) {
try {
String fileContent = readFile(args[0]);
String transformedContent = transformContentToUppercase(fileContent);
System.out.println(transformedContent);
} catch (IOException e) {
e.printStackTrace();
}
}
public static String readFile(String fileName) throws IOException {
StringBuilder content = new StringBuilder();
FileReader reader = null;
try {
reader = new FileReader(fileName); // FileNotFoundException이 발생할 수 있음
int character;
while ((character = reader.read()) != -1) { // IOException이 발생할 수 있음
content.append((char) character);
}
} finally {
try {
if (reader != null) {
reader.close(); // IOException이 발생할 수 있음
}
} catch (IOException e) {
e.printStackTrace();
}
}
return content.toString();
}
// 파일 내용을 대문자로 변환하는 메서드
public static String transformContentToUppercase(String content) {
return content.toUpperCase();
}
}
try블럭에서 readFile 메서드에서 예외 없이 정상 수행된다면, 파일의 텍스트를 대문자로 변환하는 transformContentToUppercase 메서드를 수행한 뒤에 대문자로 변환된 문자열을 출력한다.
반면,readFile 메서드에서 예외가 발생한다면 예외 객체가 호출한 메서드로 throw 하여 transformContentToUppercase 메서드와 대문자로 변환된 문자열을 출력은 수행하지 않고 catch 블럭으로 즉시 이동하여 예외 발생에 대한 알림 문자열을 출력한다.
이렇게 메서드에 예외를 선언하여 예외를 호출한 메서드로 던지는 것은, 각 메서드 마다의 기능을 명확히할 수 있도게한다. 이는 객체에 단일 책임을 분배하도록하여 코드의 재사용성과 가독성을 높이는 데 도움이 된다. 이를 통해 프로그램의 안정성과 유지보수성을 향상시킬 수 있다.
예외 되던지기(Exception Rethrowing)
한 메서드에서 발생할 수 있는 예외가 여러개인 경우, 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록하여 양쪽에 나눠서 처리되도록 할 수 있다.
import java.io.IOException;
import java.sql.SQLException;
// 여러 예외 중 일부는 메서드 내에서 처리하고, 나머지는 호출한 메서드에서 처리
public class Example1 {
public static void method() throws SQLException {
try {
if (true)
throw new IOException("입출력 오류 발생");
if (true)
throw new SQLException("데이터베이스 오류 발생");
} catch (IOException e) {
// IOException은 이 메서드 내에서 처리
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
method();
} catch (SQLException e) {
// SQLException은 호출한 메서드에서 처리
e.printStackTrace();
}
}
}
그리고 단 하나의 예외에 대해서도 예외가 발생한 메서드와 호출한 메서드 양쪽에서 처리할수 있다. try 블럭에서 발생한 예외에 대해서 catch 블럭에서 필요한 작업을 수행한 후에 throw 키워드를 사용하여 예외 객체를 던질 수 있다. 그러면 호출한 메서드로 예외 객체가 전달되고 호출한 메서드의 try-catch문에서도 예외를 처리한다.
public class Example2 {
public static void method() {
try {
// NullPointerException 예외 던지기
if (true) {
throw new NullPointerException("널 포인터 예외 발생");
}
} catch (NullPointerException e) {
// 예외를 로깅하거나 필요한 작업 수행
e.printStackTrace();
// 예외를 다시 던지기
throw e;
}
}
public static void main(String[] args) {
try {
method();
} catch (NullPointerException e) {
// 호출한 메서드에서 예외 처리
e.printStackTrace();
}
}
}
반환값이 있는 return 문의 경우, try 블럭 뿐만 아니라, catch 블럭에도 return 문이 있어야한다. 예외가 발생 했을 경우에도 값을 반환해야 하기 때문이다.
public class ReturnThrow1 {
public static int method() {
try {
return 0;
} catch (ArithmeticException e) {
e.printStackTrace();
return 5;
}
}
public static void main(String[] args) {
int result = -1;
try {
result = method();
} catch (RuntimeException e) {
e.printStackTrace();
}
System.out.println(result);
}
}
또는 catch 블럭에서 throw로 예외 던지기를 해서 호출한 메서드로 예외를 전달하면, return문이 없어도된다. 왜냐하면 catch 블럭에서 예외를 던진다면, 호출한 메서드의 catch 블럭에 진입할 것이기 때문에 반환값은 필요없어지기 때문이다.
public class ReturnThrow2 {
public static int method() throws Exception {
try {
int x= 0;
int y= 10 / x;
return y;
} catch (ArithmeticException e) {
e.printStackTrace();
throw new Exception();
}
}
public static void main(String[] args) {
int result = -1;
try {
result = method();
} catch (Exception e) {
// 호출한 메서드에서 예외 처리
e.printStackTrace();
}
System.out.println(result);
}
}
그리고 finally 블럭에도 return문을 사용하여, try나 catch 블럭에서 값을 반환하지 않을 수도 있다. 예외 발생 여부와 상관없이 finally 블럭이 실행되기 때문이다.
public class ReturnThrow3 {
public static int method() throws Exception {
try {
int x= 0;
int y= 10 / x;
} catch (ArithmeticException e) {
// 예외를 로깅하거나 필요한 작업 수행
e.printStackTrace();
} finally {
return 10;
}
}
public static void main(String[] args) {
int result = -1;
try {
result = method();
} catch (Exception e) {
// 호출한 메서드에서 예외 처리
e.printStackTrace();
}
System.out.println(result);
}
}
챰고 자료
- 자바의 정석 (남궁성 지음)
'Java > Java Language' 카테고리의 다른 글
| [Java] CLI와 IntelliJ 소스코드 실행 비교 (0) | 2024.07.08 |
|---|---|
| [Java] Custom Exception과 Chained Exception (0) | 2024.07.06 |
| [Java] 예외 처리 (Exception Handling) (0) | 2024.07.06 |
| [Java] 프로그램의 에러 (Exception, Error 클래스) (0) | 2024.07.06 |
| [Java] Inner Class (내부 클래스) (0) | 2024.06.28 |