예외(Exception)

예외 처리에서 '예외'는 프로그램 실행 중 예상치 못한 일이 발생하여 프로그램이 비정상적으로 종료되거나 잘못 작동하는 상황을 말합니다. 그렇다면 예외 처리는 무엇일까요? 예외 처리는 예외가 발생하는지 검사하고 만약 예외가 발생하면 비정상적으로 종료되는 것을 막기 위해서 이를 처리하는 것을 말합니다.

예외 사례 살펴보기

예를 들자면, 우리가 사칙연산이 가능한 계산기를 만들고 이를 배포했다고 가정해봅시다. 사용자들은 이 프로그램을 다운로드받고 사용하던 도중 본의 아니게 제수에 0을 넣고 나눗셈 버튼을 누르게 되었습니다. 하지만 수를 0으로 나눌 수는 없으므로 에러가 발생하고 프로그램이 비정상적으로 종료됩니다. 아래의 예제에서는 사용자에게 피제수와 제수를 입력받아 피제수와 제수와의 나눗셈 결과를 출력합니다.

import java.util.Scanner;

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("피제수를 입력하세요: ");
        int dividend = sc.nextInt();
        System.out.print("제수를 입력하세요: ");
        int divisor = sc.nextInt();

        System.out.println(dividend + "을 " + divisor + "로 나눈 값은 " + dividend / divisor + "입니다.");
    }
}

위의 예제에서 피제수에 아무 수나 입력하고 제수에 0을 입력하면 결과를 출력하지 못하고 아래와 같이 12행에서 ArithmeticException 예외가 발생합니다. 이는 어느 수를 0으로 나눌 수 없기에 발생하는 전형적인 예외입니다.

피제수와 제수로 정수만 입력받으려고 했는데 문자열이 들어온 경우에도 아래와 같이 8행이나 10행에서 InputMismatchException 예외가 발생합니다.예외명 그대로 입력이 일치하지 않아서, 즉 입력된 타입이 정수라고 예상했지만 실제로는 문자열이기 때문에 예외가 발생한 것입니다.

이처럼 예외가 발생하여 프로그램이 예기치 않게 종료되는 상황을 막기 위해서 자바에서는 이를 처리하는 try~catch 구문을 제공하고 있습니다. 이 구문에 대해서 알아보기 전에 어떤 예외가 있는지 살펴보도록 하겠습니다.

예외 계층(Exception Hierarchy)

자바에서 예외는 크게 검사 예외(Checked Exception)와 비검사 예외(Unchecked Exception)로 나눌 수 있습니다.

검사 예외(Checked Exception)

Exception 클래스의 자식 클래스이면서 RuntimeException 클래스를 상속받지 않은 모든 예외는 컴파일러가 컴파일 중에 프로그래머가 해당 예외를 처리했는지를 검사하기 때문에 검사 예외 혹은 컴파일 타임 예외라고 합니다.

컴파일 에러를 해결하기 위해서는 try-catch문으로 예외를 잡아서 처리하거나, 메서드 선언에 throws 키워드를 사용하여 예외 처리를 다른 곳으로 넘겨야 합니다. 검사 예외에는 파일이 존재하지 않을 때 발생하는 FileNotFoundException 등이 있습니다.

비검사 예외(Unchecked Exception)

RuntimeException 클래스를 상속하며, 프로그램 실행 중에 발생하는 예외를 비검사 예외 혹은 런타임 예외라고 합니다. 이 예외는 컴파일 타임이 아니라 런타임에 검사되므로 컴파일 에러는 발생하지는 않으며, 검사 예외와는 다르게 예외 처리를 강제하진 않습니다. 비검사 예외에는 어떤 수를 0으로 나누는 것과 같이 비정상적인 계산 도중에 발생하는 ArithemeticException, 인덱스가 배열의 범위를 넘어설 경우에 발생하는 IndexOutOfBoundsException 등이 있습니다. 검사 예외와 비검사 예외의 차이는 후반부에서 살펴볼 수 있습니다.

에러(Error)

대부분의 에러는 언제 발생하는지 알 수 없으며 프로그래머가 이를 처리하거나 추측할 수 없습니다. 에러는 심각한 상태에 이르렀다는 것을 의미하므로 프로그래머가 이를 처리하려고 해서는 안 됩니다. 원인으로는 주로 하드웨어나 시스템의 오작동, 메모리 부족 등과 같이 프로그램을 실행 중인 환경에 있습니다. 대표적으론 OutOfMemoryError이 있는데 이 에러는 메모리가 부족하여 JVM이 객체를 할당할 수 없을 때 발생합니다.

try~catch

try~catch 구문은 예외가 발생할 위험이 있는 부분(try)과, 예외가 처리되는 부분(catch)으로 나뉩니다. 아래는 try~catch문의 기본 구성입니다.

try {
	// 예외가 발생할 위험이 있는 부분
} catch (예외타입 변수명) {
	// 예외를 처리하는 부분
}

만약에 try 블록에서 예외가 발생하면 catch절에서 지정한 예외를 잡아서 처리하게 됩니다. 예외가 발생하지 않으면 catch절 내의 코드는 실행되지 않습니다. try 블록 내에서 여러 종류의 예외가 발생할 수 있는 경우에는 아래와 같이 catch절을 제한없이 여러 개 작성할 수 있습니다.

try {
	// 예외가 발생할 수 있는 부분
} catch (예외타입1 변수명1) {
	// 예외 타입1에 해당하는 예외를 처리하는 부분
} catch (예외타입2 변수명2) {
	// 예외 타입2에 해당하는 예외를 처리하는 부분
}
... catch (예외타입N 변수명N) {
	// 예외 타입N에 해당하는 예외를 처리하는 부분
}

바로 try~catch문을 사용한 예제를 살펴보도록 하겠습니다.

import java.util.Scanner;

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("피제수를 입력하세요: ");
        int dividend = sc.nextInt();
        System.out.print("제수를 입력하세요: ");
        int divisor = sc.nextInt();

        try {
            System.out.println(dividend + "을 " + divisor + "로 나눈 값은 " + dividend / divisor + "입니다.");
        }
        catch (ArithmeticException ex) {
            System.out.println("제수가 0이므로 나눌 수 없습니다.");
        }
        System.out.println("프로그램을 종료합니다.");
    }
}

위의 코드의 12~17행을 보시면 try~catch문이 등장했습니다. try 블록에서 만약 ArithmeticException 예외가 발생한다면 바로 해당하는 catch절로 이동하여 예외 처리 코드를 실행하게 됩니다. 어떤 수를 0으로 나눌 때 예외가 발생하더라도 이를 처리하기 때문에, 프로그램이 13행에서 비정상적으로 종료되지 않고 18행의 문자열을 출력하는 것을 볼 수 있습니다. 이어서 주의할 점을 살펴보도록 하겠습니다.

public class ExceptionHandlingExample {
    public static void main(String[] args) {
    	try {
    		int a = 10, b = 0;
    		
    		System.out.print("a / b = ");
    		int result = a / b;
    		System.out.println(result);
    	} catch (ArithmeticException ex) {
    		System.out.println("올바르지 않은 나눗셈 연산");
    	}
    }
}

결과를 보시면 아시겠지만, try 블록 내에서 예외가 발생하면 남은 코드는 실행하지 않고 바로 해당하는 catch절로 이동합니다. 이를 그림으로 살펴보면 다음과 같습니다.

하나의 catch절에서 여러 타입의 예외를 처리하기

자바 6 이전에는 catch절이 단 한 가지 타입의 예외만을 처리할 수 있었습니다. 그러나 자바 7부터는 하나의 catch절이 여러 타입의 예외를 처리할 수 있습니다. 여러 타입의 예외를 처리하려면 catch절에서 아래와 같이 예외 타입을 | 문자로 구분해야 합니다.

try {
	// 예외가 발생할 위험이 있는 부분
} catch (예외타입1 | 예외타입2 | 예외타입3 변수명) {
	// 예외를 처리하는 부분
}

위와 같이 작성하면 예외 처리 코드가 동일한 경우 하나로 합칠 수 있으므로 중복되는 코드를 제거할 수 있습니다. 예를 들면 아래와 같이 작성할 수 있습니다.

try {
    // ...
} catch (ArithmeticException | InputMismatchException ex) {
    ex.printStackTrace();
}

finally

finally절을 사용하면 try 블록에서 예외가 발생하는 것과 상관없이 반드시 실행되는 코드를 작성할 수 있습니다. try-catch-finally문의 기본 구성은 다음과 같습니다.

try {
	// 예외가 발생할 위험이 있는 부분
} catch (예외타입 변수명) {
	// 예외를 처리하는 부분
} finally {
	// 예외가 발생하든 발생하지 않든 반드시 실행되는 부분
}

예외가 발생했을 때 실행 순서는 try → catch → finally, 발생하지 않았거나 예외를 잡지 못했을 때는 try → finally와 같습니다. 아래와 같이 catch절을 제외한 try-finally문을 사용할 수도 있습니다.

try {
	// 예외가 발생할 위험이 있는 부분
} finally {
	// 예외가 발생하든 발생하지 않든 반드시 실행되는 부분
}

이번엔 try-catch문의 예제 코드에 finally문을 추가해보도록 하겠습니다.

import java.util.Scanner;

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("피제수를 입력하세요: ");
        int dividend = sc.nextInt();
        System.out.print("제수를 입력하세요: ");
        int divisor = sc.nextInt();

        try {
            System.out.println(dividend + "을 " + divisor + "로 나눈 값은 " + dividend / divisor + "입니다.");
        } catch (ArithmeticException ex) {
            System.out.println("제수가 0이므로 나눌 수 없습니다.");
        } finally {
        	System.out.println("이 문장은 예외가 발생하든 발생하지 않든 상관없이 출력됩니다.");
        }
        System.out.println("프로그램을 종료합니다.");
    }
}

결과를 보면 try 블록 내에서 예외가 일어나든 일어나지 않든 finally 블록 내의 코드가 실행되는 것을 확인할 수 있습니다. 덧붙여서 아래와 같이 return문을 만나도 finally가 실행됩니다.

public class ExceptionHandlingExample {

    public static void main(String[] args) {
        someMethod();
    }

    public static void someMethod() {
        try {
            System.out.println("try 블록");
            return;
        } finally {
            System.out.println("finally 블록");
        }
    }
}

스택 트레이스(Stack Trace)

스택 트레이스는 어떤 메서드에서 처리되지 않은 예외가 발생했을 때 그 전에 어떤 메서드가 호출되었는지를 보여줍니다. 스택 트레이스는 예외가 발생한 위치를 알려주며, 어떻게 해서 예외가 발생하게 되었는지 추적할 때 유용하게 사용할 수 있습니다.

스택 트레이스를 읽어보면 프로그램의 시작부터 처리되지 않은 예외가 발생한 지점까지의 메서드 호출 목록을 살펴볼 수 있습니다. 가장 최근에 호출된 메서드가 목록의 맨 위에 있습니다.

예제 살펴보기

이해를 돕기 위해서 간단한 예제를 보도록 하겠습니다.

public class ExceptionHandlingExample {
    static void methodA() {
        methodB();
    }

    static void methodB() {
        methodC();
    }

    static void methodC() {
        int result = 0 / 0;
    }

    public static void main(String[] args) {
        methodA();
    }
}

이 코드를 컴파일 후 실행하면 아래와 같은 스택 트레이스를 볼 수 있습니다.

methodC()에서 ArithmeticException 예외가 발생했으며 코드의 11행에 문제가 있다는 것을 볼 수 있으며, 11행에는 어떤 수를 0으로 나누는 코드가 보입니다. 11행의 코드를 먼저 확인했다가 해결되지 않으면 methodC()를 호출한 7행으로 이동하여 다시 문제를 살펴볼 수 있습니다. 그 다음에는 methodB()를 호출한 3행, 마지막으로 methodA()를 호출한 15행을 살펴볼 수 있습니다.

catch절에서 스택 트레이스 출력하기

Throwable 클래스를 상속하는 모든 예외 클래스에는 스택 트레이스를 출력하는 printStackTrace() 메서드가 있습니다. 스택 트레이스가 필요한 경우에 이 메서드를 이용하여 출력할 수 있습니다.

public class ExceptionHandlingExample {
    // ...

    public static void main(String[] args) {
        try {
            methodA();
        } catch (ArithmeticException ex) {
            ex.printStackTrace();
        }
    }
}

throw

throw는 사전적 의미 '던지다'라는 뜻을 가지고 있는 것처럼, 자바에서도 호출자에게 예외를 던질 때, 즉 예외를 발생시킬 때 사용합니다. throw 키워드는 보통 아래와 같이 사용할 수 있습니다.

// 구체적으로 말하면 Throwable 클래스를 상속받는 클래스의 인스턴스다.
throw 예외객체;
// 예외 객체를 넘기기만 하면 되므로 아래와 같이 쓸 수도 있다.
throw new 예외객체();

Exception 클래스를 보면 아래와 같은 생성자들이 있습니다. 구체적인 메시지를 담거나, 기존에 발생했던 예외를 감싸서 다시 던질 수 있습니다.

public class Exception extends Throwable {
    public Exception() {
        super();
    }

    public Exception(String message) {
        super(message);
    }

    public Exception(String message, Throwable cause) {
        super(message, cause);
    }

    public Exception(Throwable cause) {
        super(cause);
    }
    // ...
}

이해를 돕기 위해서 아래 예시를 한 번 살펴봅시다. 호출 순서는 메인 메서드부터 시작하여 마지막으로 methodC()가 호출됩니다. methodC()에서 최초의 예외가 발생하여 methodB(), methodA(), 그리고 메인 메서드로 예외가 전파됩니다. 메인 메서드에서도 예외를 처리하지 못하면 프로그램은 결국 종료됩니다.

public class ExceptionHandlingExample {

    public static void main(String[] args) {
	    // (4) 메인 메서드에서 받은 예외를 처리하지 않으므로
	    // 예외가 넘어가서 결국엔 프로그램이 비정상 종료될 것이다.
        methodA(3, 0);

		// (4) 따라서 이 문장은 출력되지 않는다.
	    System.out.println("메인 메서드 종료");
    }

    public static int methodA(int a, int b) {
        try {
            return methodB(a, b);
        } catch (IllegalArgumentException ex) {
	        // (3) 원한다면 아래와 같이 다른 예외로 변환할 수도 있다.
	        // 기존의 예외는 원인(cause) 예외로 같이 콘솔에 출력될 것이다.
            System.out.println("ExceptionHandlingExample.methodA");
            throw new UnsupportedOperationException("이 연산은 지원되지 않습니다.", ex);
        }
    }

    public static int methodB(int a, int b) {
        try {
            return methodC(a, b);
        } catch (IllegalArgumentException ex) {
	        // (2) 예외를 기록하고 받은 예외를 그대로 호출자에게 다시 던질 수 있다.
            System.out.println("ExceptionHandlingExample.methodB");
            throw ex;
        }
    }

    public static int methodC(int a, int b) {
	    // 매개변수의 값이 조건을 충족하지 않으면 예외를 던진다.
        if (a <= 0 || b <= 0) {
	        // (1) methodC(a: 3, b: 0)인데 b가 0이므로 조건에 맞지 않기 때문에
			// 여기서 첫 번째 예외가 발생할 것이다.
            throw new IllegalArgumentException("a와 b는 0보다 큰 정수여야 합니다.");
        }

        return a / b;
    }
}

위의 예제 코드를 실행하면 아래와 같은 결과를 볼 수 있습니다. 원인 예외, 즉 최초에 발생한 예외(cause)는 methodC()에서 발생한 IllegalArgumentException 예외입니다. 그리고 위에 보이는 UnsupportedOperationException은 methodA()에서 발생한 비교적 최근 예외입니다. 따라서 맨 아래에서 위로 올라갈수록 비교적 최근에 발생한 예외이며, 예외가 시작된 시점을 보고 싶다면 가장 아래의 예외를 확인하면 됩니다.

이를 그림으로 살펴보면 다음과 같습니다. 발생한 예외는 호출자에게 전파되며, 호출자가 이를 처리하지 못하면 호출자의 호출자로 넘어가는 식입니다. 결국 메인 메서드에서도 처리하지 못해서 JVM이 이를 받게되면 (특별한 상황이 아닌 이상) 스택 트레이스를 출력하고 프로그램을 종료하게 됩니다.

사용자 정의 예외(User-defined Exceptions)

말 그대로 사용자가 정의한 예외입니다. Exception 클래스나 다른 자식 예외 클래스를 상속받아 자신만의 예외 클래스를 만들 수 있습니다. 이해를 돕기 위해서 예제를 살펴보면서 설명하도록 하겠습니다.

import java.util.Scanner;

// Exception 클래스를 상속하여 사용자 정의 예외 클래스 NegativeNumberException를 만든다.
class NegativeNumberException extends Exception {
	private int num;
	
	public NegativeNumberException() {
		// 부모 클래스의 생성자, 즉 Exception(String message) 생성자에 메시지를 넘긴다.
		super("수는 음수가 될 수 없습니다.");
	}
	
	public NegativeNumberException(int num) {
		super("수는 음수가 될 수 없습니다: " + num);
		this.num = num;
	}
	
	public int getNum() {
		return num;
	}
}

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("수를 입력하세요: ");
        int num = sc.nextInt();
        
        try {
	        if (num < 0) {
	        	throw new NegativeNumberException(num);
	        }
	        System.out.println("입력받은 수는 " + num + "입니다.");
        } catch (NegativeNumberException ex) {
        	System.out.println("수는 음수가 될 수 없습니다. (" + ex.getNum() + ")");
        }
    }
}

4행에서 Exception 클래스를 상속하여 사용자 정의 예외 클래스인 NegativeNumberException 클래스를 선언한 것을 볼 수 있습니다. 우리가 봤던 예외 계층에서 확인할 수 있듯이 Exception 클래스를 상속받으면 '검사 예외(Checked Exception)'가 되므로 예외를 따로 처리해주지 않으면 컴파일 에러가 발생합니다. 만약에 RuntimeException 클래스를 상속시켰으면 '비검사 예외(Unchecked Exception)'가 되므로 따로 처리해주지 않아도 컴파일 에러가 발생하지 않는 것을 볼 수 있습니다. 이어서, 27행에서 수를 하나 입력받고, 해당 수가 음수면 throw문으로 우리가 만든 NegativeNumberException이란 예외를 발생시킵니다. 이렇게 발생시킨 예외는 catch절로 잡아서 처리할 수 있습니다.

throws

throw문과 throws절은 다르므로 잘 구분하셔야 합니다. throws절을 사용하면 아래와 같이 메서드에서 발생할 수 있는 예외를 명시할 수 있습니다. 이를 예외 선언(exception declaration)이라고 합니다. 호출자(호출한 메서드)는 피호출자(호출된 메서드)에서 발생할 수 있는 예외가 무엇이 있는지 모르므로 예외를 선언함으로써 이를 알려줄 수 있습니다.

public void foo() throws 예외타입 {
	// ...
}

런타임 예외는 위처럼 예외 선언을 하지 않아도 괜찮지만, 컴파일 타임에서 확인할 수 있는 예외들은 이를 선언하거나 처리하지 않으면 컴파일 에러가 발생합니다. 즉, throws절은 컴파일 타임 예외가 발생했을 때, 해당 예외를 바로 처리하지 않고 자신을 호출한 메서드로 예외 처리의 책임을 넘길 때 사용합니다.

import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class ExceptionHandlingExample {
	// FileNotFoundException는 IOException을 상속받은 Checked Exception이다.
    static void foo() throws FileNotFoundException {
        new FileInputStream("C:\\example.txt");
    }

    public static void main(String[] args) {
    	try {
    		foo();
    	} catch (FileNotFoundException ex) {
    		System.out.println("지정된 파일을 찾을 수 없습니다.");
    	}
    }
}

위의 코드에서 throws를 사용해서 자신을 호출한 메서드로 처리를 넘겼으니 해당 예외에 대한 처리는 main() 메서드가 맡게 되었습니다.

주의할 점

검사 예외와 비검사 예외

왜 검사 예외(혹은 컴파일 예외)는 예외 처리를 강제해서 개발자를 성가시게 구는 걸까요? 이해를 돕기 위해서 검사 예외 중 하나인 FileNotFoundException의 예를 들어봅시다. 흔하게 일어날 수 있는 상황으로, 사용자가 입력한 파일 경로에 있는 파일을 가지고 다른 파일로 변환하거나, 무언가 유용한 정보를 추출해내는 부류의 프로그램을 생각해봅시다. 

public byte[] convertFileIntoSomethings(String fileName) {
	FileInputStream fis = new FileInputStream(fileName);
	...
	return b;
}

우리는 사용자가 입력한 파일 경로에 처리해야 할 파일이 있으리라 기대하는데, 만약에 사소한 오타 같은 사용자의 실수로 존재하지도 않는 파일의 경로가 입력으로 들어온다면 어떻게 해야 할까요? 이런 상황은 생각보다 정말 흔히 볼 수 있습니다. 그 경로에 실제로 파일이 없다고 해서 프로그램이 비정상 종료되어 여태까지 사용자가 작업한 내역을 날려버려선 안 될 것입니다. 이럴 때는 "해당 파일이 존재하지 않습니다."라는 메시지를 사용자에게 보여주고 다시 입력 화면을 보여주는 등 충분히 복구의 여지가 남아있습니다. 검사 예외는 컴파일러가 "이런 일이 충분히 있을 수 있으니 예외를 처리해달라" 혹은 "이 예외는 충분히 복구할 수 있다"고 말하며 프로그래머에게 예외 상황을 상기시켜주는 역할을 합니다. 그러면 반대로 비검사 예외(혹은 런타임 예외)는 무엇일까요? 아래의 예를 생각해봅시다.

int[] data = {1, 2, 3, 4, 5};

for (int i = 0; i <= data.length; i++) { // data[5]는 없다!
	/* ... */
}

위의 코드를 실행시키면 당연하게도 비검사 예외인 ArrayIndexOfBoundsException이 발생합니다. 비검사 예외는 이와 같이 주로 프로그래머의 실수 등과 같이 프로그램 로직에 문제가 있으며, 이러한 예외는 프로그램의 모든 곳에서 일어날 수 있습니다. 보통 비검사 예외는 "복구할 수 없거나" 혹은 "복구하는 것이 그렇게 큰 의미가 없을 때" 사용됩니다. 위의 경우에는 마땅한 복구 방법이 없으며, 이를 복구하려고 시도하기 보다는 문제가 되는 부분을 직접 수정하는 게 더 적절합니다. 하지만 이렇게 상황이 딱딱 나뉘는 것은 아닙니다. 컴파일러는 복구할 수 있으리라 기대하지만 처한 상황에 따라서 정작 복구할 수 없거나 의미가 없을 때도 많습니다. 이럴 때는 컴파일러가 검사 예외의 처리를 강제하는 것이 유독 짜증날 수 있습니다.

// 예외가 뭔지는 모르겠지만 일단 던지고 본다.
void doSomething() throws Exception {
	/* ... */
}

// 예외를 그저 삼킨 뒤에 아무 것도 하지 않는다.
void doSomething() {
	try {
		/* ... */
	} catch (Exception ex) { }
}

그러면 보통 귀찮아서 위와 같이 작성하는 개발자도 종종 볼 수 있는데 이런 방법은 지양해야 합니다. 나중에 원인을 파악할 수 없도록 예외를 삼켜버리거나, 처리가 귀찮다고 메서드를 호출하는 쪽에 예외 처리의 책임을 떠넘기는 것은 정말로 무책임한 행동입니다. 처리할 수 없을 때는 예외를 잡아서는 안되며, 정말 무시해야 할 필요가 있다면 아래와 같이 의도적임을 나타내기 위해서 "무시함(ignore)"로 표현할 수 있습니다. 물론 무시하는 구체적인 이유도 들어가 있으면 좋습니다.

try {
	// ...
} catch (ArrayIndexOutOfBoundsException ignore) {
	// 무시함. 두 번째 인수가 없는 경우 기본 딜레이를 사용한다.
}

복구할 수 없거나 무의미할 때는 아래와 같이 비검사 예외(즉, 런타임 예외)로 감싸서 예외를 던지는 것이 좋습니다. 이러면 여기서 던진 예외를 호출자가 처리하도록 강제하지 않으므로, 호출자 코드가 의미 없는 예외 회피 코드로 도배되는 것을 막을 수 있습니다. 물론 이름이 더 잘 드러날 수 있는 비검사 예외면 좋습니다.

try {
	// ...
} catch (ReflectiveOperationException roe) {
	throw new RuntimeException(roe);
}

그러면 아래의 경우는 어떨까요? 적어도 처음에 소개했던 예외를 삼키는 방법보다는 낫습니다. 개발자가 적어도 예외가 발생한 경로는 확인할 수 있기 때문입니다. 하지만 출력된 스택 트레이스를 통해 원인을 상대적으로 파악하기가 힘들 수 있으며, 일반적으로 출력되는 위치가 콘솔이라는 문제가 있습니다. 따라서 개발자가 콘솔을 항상 예의주시 하지 않는 이상은 예외가 발생했다는 사실을 놓치기가 쉽습니다. 콘솔에 출력하는 것 대신에 예외 내용을 파일에 기록하거나 관리자의 이메일로 전송하여 필요할 때 확인할 수 있도록 하는 것이 나을 것입니다. 이를 위해서 보통은 다양한 설정을 지원하는 로깅 프레임워크(예: log4j, logback 등)를 사용합니다. 이에 관해서는 인터넷에 사용법을 구체적으로 설명하는 곳이 많으니 궁금하다면 해당 사이트를 참고합시다.

try {
	// ...
// 포괄적인 Exception 예외보다 더 구체적인 예외를 잡는 것이 좋다.
} catch (Exception ex) {
	ex.printStackTrace();
}