...
예외 던지기
예외 발생시키기 (throw)
만일 프로그램적으로 에러가 아니라도 로직상 개발자가 일부러 에러를 내서 로그에 기록하고 싶은 상황이 올 수 있다.
자바에서는 throw 키워드를 사용하여 강제로 예외를 발생시킬 수 있다.
원래는 프로그램이 알아서 에러를 탐지하고 처리 하였지만, 이번에는 사용자가 일부러 에러를 throw하여 에러를 catch 한다는 개념으로 보면 된다.
이때 new 생성자로 예외 클래스를 초기화하여 던져는데, 이 클래스 생성자에 입력값을 주게되면, catch문의 getMessage() 메서드에서 출력할 메세지를 지정하게 된다.
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try {
Scanner s = new Scanner(System.in);
System.out.print("음수를 제외한 숫자만 입력하세요 : ");
int num = s.nextInt(); // 사용자로부터 정수를 입력 받음
if (num < 0) {
// 만일 사용자가 말을 안듣고 음수를 입력하면 강제로 에러 발생 시켜버리기!!
throw new ArithmeticException("왜 하지말라는 짓을 하시는 거죠? ㅡㅡ"); // ArithmeticException 예외 클래스 객체를 생성해 catch문으로 넘겨버린다고 생각하며 된다
}
System.out.println("음수를 입력하지 않으셨군요. 감사합니다");
} catch (ArithmeticException e) {
System.out.println(e.getMessage());
} finally {
System.out.println("프로그램을 종료합니다");
}
}
}
예외 떠넘기기 (throws)
예외가 발생할 수 있는 코드를 작성할 때 try - catch 블록으로 처리하는 것이 기본이지만, 경우에 따라서는 다른 곳에서 예외를 처리하도록 호출한 곳으로 예외를 떠넘길 수도 있다.
이때 사용하는 키워드가 throws이다. throws 는 메소드 선언부 끝에 작성되어 메소드에서 예외를 직접 처리(catch)하지 않은 예외를 호출한 곳으로 떠넘기는 역할을 한다.
예외를 발생시키는 키워드는throw고 예외를 메서드에 선언하는 키워드는throws이다.
예를들어 다음과 같이 메서드 3개가 있고 각 메서드 마다 예외 처리를 위해 try - catch 블록으로 일일히 감싸주었다.
public class Main {
public static void main(String[] args) {
method1();
method2();
method3();
}
public static void method1() {
try {
throw new ClassNotFoundException("에러이지롱");
} catch (ClassNotFoundException e) {
System.out.println(e.getMessage());
}
}
public static void method2() {
try {
throw new ArithmeticException("에러이지롱");
} catch (ArithmeticException e) {
System.out.println(e.getMessage());
}
}
public static void method3() {
try {
throw new NullPointerException("에러이지롱");
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
}
}
하지만 괜히 코드가 길어지고 가독성도 그렇게 좋지가 않아 보인다.
다음과 같이 메서드 선언 코드 오른쪽에 throws 예외클래스명 을 기재해주면, 만일 해당 메서드 안에서 예외가 발생할 경우 try - catch 문이 없으면 해당 메서드를 호출한 상위 스택 메서드로 가서 예외 처리를 하게 된다.
void method() throws Exception, Exception2, ... {
에러발생코드; // 예외가 발생하면 현 메소드를 호출한 쪽으로 올라간다.
}
public class Main {
public static void main(String[] args) {
try {
method1();
method2();
method3();
} catch (ClassNotFoundException | ArithmeticException | NullPointerException e) {
System.out.println(e.getMessage());
}
}
public static void method1() throws ClassNotFoundException {
throw new ClassNotFoundException("에러이지롱");
}
public static void method2() throws ArithmeticException {
throw new ArithmeticException("에러이지롱");
}
public static void method3() throws NullPointerException {
throw new NullPointerException("에러이지롱");
}
}
즉, 예외클래스를 메서드의 throws에 명시하는 것은 이곳에서 예외를 처리하는 것이 아니라 자신을 호출한 메서드에게 예외를 전달하여 예외 처리를 떠맡기는 것이다. 또한 예외를 전달받은 메서드가 또다시 자신을 호출한 메서드에게 전달할 수 있다. 이런 식으로 계속 호출 스택에 있는 메서드들을 따라 전달되다가, 제일 마지막에 있는 main 메소드에서 throws를 사용하면 가상머신에서 처리된다.
예외 발생과 코드의 트랜잭션
트랜잭션(Transaction)은 하나의 작업 단위를 뜻한다.
즉, 자바 코드에서 메서드 블럭내의 코드들이 예외가 발생해도 모두 실행되느냐 아니면 예외가 발생하면 그상태로 중지하느냐의 작업 단위를 개발자가 어떤 형태의 예외 처리 방법을 사용하느냐에 따라 달라지게 된다.
예를들어 각 메서드에서 일일히 try - catch 하면, 메인 메소드에 있는 메서드 실행 코드 부분은 3개 모두 실행 자체는 된다.
왜냐하면 예외처리를 각 메서드에서 하기 때문에 상위의 메인 메서드의 코드들은 모두 실행되게 된다.
반면에 throws를 통해 예외처리를 상위 메서드에서 모아 처리를 한다면, 코드 어느 한곳에서 예외가 발생하면 그 뒤의 나머지 코드들은 당연히 실행되지 않게 된다. (바로 catch로 점프하니까)
이처럼 try - catch 문은 어디에 사용하냐 어디서 throws 하느냐에 따라 자바 코드의 작업 단위(트랜잭션)가 완전히 달라질 수 있게 되는 것이다. 따라서 자신의 프로젝트에 따라 적절한 예외 처리 로직을 짜 주어야 하는 방향으로 나아가야 한다.
연결된 예외 (Chained Exception)
예외를 다른 예외로 감싸 던지기
연결된 예외(chained exception)는 한 예외가 다른 예외를 발생시킬 수 있는 기능이다.
우리가 클래스를 상속하여 다형성을 이용하여 부모 클래스 타입으로 다뤄온 것 처럼, 예외도 마치 부모 예외로 감싸서 보내 마치 예외의 다형성 처럼 다룰 수 있다.
예를 들어 예외 A가 발생했다면 이를 예외 B로 감싸서 throw하는 식으로, 마치 예외를 다른 예외로 감싸서 던진다고 보면된다. 그래서 예외 A가 예외 B를 발생시켰다면, A를 B의 '원인 예외(cause exception)'라고 한다.
Exception 클래스가 상속하고 있는 Throwable 클래스에는 getMessage() 와 printStackTrace() 이외에도 chained exception을 가능하게 해주는 다음 메서드를 지원한다.
Throwable initCause (Throwable cause): 지정한 예외를 원인 예외로 등록Throwable getCause(): 원인 예외를 반환
이 메서드를 이용해서 예외를 다른 예외로 포장해 던질수 있게 된다.
당연히 Exception 클래스의 부모인 Throwable 클래스에 정의되어있기 때문에 모든 예외에서 사용 가능하다.
다음 아래 예시 코드를 보자.
class InstallException extends Exception { ... }
class SpaceException extends Exception { ... }
class MemoryException extends Exception { ... }
public class Main {
public static void main(String[] args) {
try {
install();
} catch (InstallException e) {
System.out.println("원인 예외 : " + e.getCause()); // 원인 예외 출력
e.printStackTrace();
}
}
public static void install() throws InstallException {
try {
throw new SpaceException("설치할 공간이 부족합니다."); // SpaceException 발생
} catch (SpaceException e) {
InstallException ie = new InstallException("설치중 예외발생"); // 예외 생성
ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
throw ie; // InstallException을 발생시켜 상위 메서드로 throws 된다.
} catch (MemoryException e) {
// ...
}
}
}
- startInstall() 메서드에서 SpaceException 이라는 예외가 발생
- catch 문에서 InstallExceptoin 예외 클래스를 새로 생성
- 그리고 InstallException 객체의 메서드
initCause()를 이용해 SpaceException 타입의 객체를 넣어 실행 - 그러면 SpaceException 예외는 InstallException 예외에 포함되게 된다. (원인 예외)
- InstallException 예외 객체를 밖으로 throws 한다.
- 메인메서드에서 InstallException 예외를 catch하고
getCause()메서드를 통해 원인 예외 로그를 출력한다
발생한 예외를 그대로 처리하면 될 것을, 왜 굳이 원인 예외로 포장해서 다른 예외로 던지는 이유는, 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서다. 위에서 언급했던 것 처럼 다형성 예외 버전이라고 봐도 된다.
그리고 처음 부터 명확한 에러 정보를 주는 것 보다는 단계별로 어떠한 원인의 에러에 의해서 에러가 났다는 정보를 주는 것이 더 좋기 때문이다. 예를 들어 위의 사진 처럼, 단순히 '설치할 공간이 부족합니다.' 라는 에러만 띄우면 어떠한 원인 동작으로 인해 갑자기 공간이 부족하는지 추적을 못하기 때문에, 예외를 감싸서 '설치중에 → 설치할 공간이 부족해 예외 발생' 이런식으로 추적이 용이하기 때문이다.
Checked 예외를 Unchecked 예외로 변환
연결된 예외(chained exception)를 사용하는 또 다른 이유는 checked예외를 unchecked예외로 바꿀 수 있도록 하기 위함이다.
예를 들어 checked exception의 종류의 예외를 포함한 코드를 작성하면 컴파일러가 예외 처리(try - catch)를 강제한다.
가장 대표적인 예로 FileWriter 클래스를 이용해 파일을 불러오는 코드를 작성하면 반드시 try - catch로 감싸주어야 컴파일이 된다.
이런식으로 설계한 이유는, 처음 자바 언어를 개발 했을때 프로그래밍 경험이 적은 사람도 보다 견고한 프로그램을 작성할 수 있도록 유도하기 위해서인데, 실제로 별것 아닌 예외도 checked exception 으로 등록한 것이 꽤 많다.
자바 프로그래밍 언어가 처음 개발되던 1990년대와 지금의 컴퓨터 환경은 많이 달라졌기 때문에, 실제로 런타임 예외로 처리해도 될 것들이 아직도 checked exception으로 등록되어 강제적으로 try - catch 문을 사용해야 하는 불편함이 있고, 또한 로직상 Runtime Exception으로 할 수 밖에 없는 경우가 있기 때문에, 추가된 기법이라고 생각하면 된다.
따라서 연결된 예외(chained exception)을 이용해, checked 예외를 unchecked 예외로 바꾸면 예외처리가 선택적이 되므로 억지로 거추장 스러운 예외처리를 하지 않아도 된다.
class MyCheckedException extends Exception { ... } // checked excpetion
public class Main {
public static void main(String[] args) {
install();
}
public static void install() {
throw new RuntimeException(new IOException("설치할 공간이 부족합니다."));
// Checked 예외인 IOException을 Unchecked 예외인 RuntimeException으로 감싸 Unchecked 예외로 변신 시킨다
}
}
# 참고자료
https://www.youtube.com/watch?v=I4XrVgCzKM4
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.