Exception 처리하기

박건우·2022년 12월 30일
0

예외처리하기

try {
	...
} catch (예외1) {
	...
} catch (예외2) {
	...
}코드를 입력하세요

try문 안의 수행할 문장들에서 예외가 발생하면 예외에 해당되는 catch문이 수행된다.

finally

어떠한 예외가 발생하더라도 반드시 실행되어야 하는 부분이 있다.

public class Sample {
    public void shouldBeRun() {
        System.out.println("ok thanks.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c;

        try {
            c = 4 / 0;
            sample.shouldBeRun(); //이 코드는 실행되지 않는다.
        } catch (ArithmeticException e) {
            c = -1;
        }
    }
}

위 코드를 보면 sample.shouldBeRun() 메소드는 절대로 실행될 수 없을 것이다. 왜냐하면 4/0 에 의해 ArithmeticException이 발생하여 catch 구문으로 넘어가기 때문이다.

shouldBeRun() 메소드는 반드시 실행되어야 하는 메소드라고 가정하고, 이런 경우를 처리하기 위해 자바는 finally 구문을 제공한다.

public class Sample {
    public void shouldBeRun() {
        System.out.println("ok thanks.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c;

        try {
            c = 4 / 0;
            sample.shouldBeRun();
        } catch (ArithmeticException e) {
            c = -1;
        } finally {
            sample.shouldBeRun(); //예외에 상관없이 무조건 수행된다.
        }
    }
}

RuntimeException과 Exception

public class Sample {
    public void sayNick(String nick) {
        if ("fool".equals(nick)) {
            return;
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("bichon");
    }
}

sayNick() 메소드는 fool이라는 문자열이 입력되면 return으로 메소드를 종료해 별명이 출력 되지 않게 하고 있다.

RuntimeException

이제 “fool” 문자열이 입력되면 단순히 return으로 종료하지 않고 적극적으로 예외를 발생시켜 보자.

public class Sample {
    public void sayNick(String nick) {
        if ("fool".equals(nick)) {
            throw new FoolException();
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("bichon");
    }
}

단순히 return 했던 부분을 throw new FoolException() 이라는 문장으로 변경했다.

위 프로그램을 실행하면 “fool” 이라는 입력 값으로 sayNick 메소드 실행 시 다음과 같은 예외가 발생한다.

Exception in thread "main" FoolException
		at Sample.sayNick(Sample.java:7)
    at Sample.main(Sample.java:14)

FoolException이 상속받은 클래스는 RuntimeException이다. Exception은 크게 두 가지로 구분된다.

1. RuntimException
2. Exception

RuntimeException은 실행 시 발생하는 예외이고 Exception은 컴파일 시 발생하는 예외이다.
즉, Exception은 프로그램 작성 시 이미 예측 가능한 예외를 작성할 때 사용하고 RuntimeException은 발생할 수도 안 할 수도 있는 경우에 작성한다.

그래서 Exception을 Checked Exception, RuntimeException은 Unchecked Exception 이라고도 한다.

Exception

class FoolException extends Exception {
	//...
}

위처럼 RuntimeException을 상속하던 것을 Exception을 상속하도록 변경하면 컴파일 오류가 발생할 것이다. 왜냐하면 예측 가능한 Checked Exception이기 때문에 예외 처리를 컴파일러가 강제하기 때문이다.

다음과 같이 변경하면 정상적으로 컴파일이 될 것이다.

public class Test {
    public void sayNick(String nick) {
        try {
            if("fool".equals(nick)) {
              throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }catch(FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.sayNick("fool");
        test.sayNick("genious");
    }
}

예외 던지기 (throws)

위 예제를 보면 sayNick 메소드에서 FoolExcpetion을 발생시키고 예외처리도 해당 메소드에서 하였는데, 이렇게 하지 않고 sayNick() 을 호출한 곳에서 FoolException을 처리하도록 예외를 위로 던질 수 있는 방법이 있다.

class FoolException extends Exception {
    //...
}

public class Sample {
    public void sayNick(String nick) {
				//sayNick 메소드에서 try~catch문으로 FoolExcpetion을 처리
        try {
            if ("fool".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 " + nick + " 입니다.");
        } catch (FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("bichon");
    }
}

class FoolException extends Exception {
    //...
}

public class Sample {
    public void sayNick(String nick) {
				//sayNick 메소드에서 try~catch문으로 FoolExcpetion을 처리
        try {
            if ("fool".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 " + nick + " 입니다.");
        } catch (FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("bichon");
    }
}

sayNick 메소드 뒷부분에 throws 구문을 이용해 FooleException을 위로 보낼 수가 있다.

위와 같이 sayNick 메소드를 변경하면 main 메소드에서 컴파일 에러가 발생할 것이다. throws 구문때문에 FoolException의 예외를 처리해야 하는 대상이 sayNick 메소드에서 main 메소드로 변경되었기 때문이다.

따라서 컴파일 오류를 해결하려면 다음과 같이 main 메소드를 변경해야 한다.

public class Sample {
    public void sayNick(String nick) throws FoolException {
        if ("fool".equals(nick)) {
            throw new FoolException();
        }
        System.out.println("당신의 별명은 " + nick + " 입니다.");
    }
	
		//main 메소드에서 try catch로 sayNick 메소드에 대한
		//FoolException 예외를 처리했다.
    public static void main(String[] args) {
        Sample sample = new Sample();
        try {
            sample.sayNick("fool");
            sample.sayNick("bichon");
        } catch (Exception e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }
}

위의 모든 과정을 살펴봤을 때, FoolException 처리를 sayNick 메소드에서 하는 것이 좋을까? 아니면 main 메소드에서 하는 것이 좋을까? 예외를 어디서 처리하냐는 것에는 아주 큰 차이가 있다.

sayNick 메소드에서 예외를 처리하는 경우엔 다음 두 문장이 모두 수행된다.

sample.sayNick("fool");
sample.sayNick("genious");

물론 sample.sayNick("fool"); 문장 수행 시에는 FoolException이 발생하겠지만 그 다음 문장 역시
수행된다.

하지만 main 메소드에서 예외처리를 한 경우, 두 번째 문장인 sample.sayNick("genious"); 가 수행되지 않는다. 왜냐하면 이미 첫 번째 문장에서 예외가 발생해 catch문으로 빠져버리기 때문이다.

try {
    sample.sayNick("fool");
    sample.sayNick("genious"); //이 문장은 수행되지 않는다.
} catch (FoolException e) {
    System.err.println("FoolException이 발생했습니다.");
}

이러한 이유로 프로그래밍 시 Exception을 처리하는 위치는 대단히 중요하다. 프로그램의 수행 여부를 결정하기도 하고 트랜잭션 처리와도 밀접한 관계가 있기 때문이다.

트랜잭션 (Transaction)

트랜잭션은 하나의 작업 단위를 뜻한다.

예를 들어 쇼핑몰의 “상품발송”이라는 트랜잭션을 가정해보자. “상품발송”이라는 트랜잭션에는 다음과 같은 작업들이 있을 수 있다.

  • 상품포장
  • 영수증발행
  • 상품발송

쇼핑몰의 운영자는 이 3가지 일들 중 하나라도 문제가 발생하면 3가지 모두 취소하고 “상품발송” 전의 상태로 되돌리고 싶을 것이다.

모두 취소하지 않으면 데이터의 정합성이 크게 흔들리게 된다. 이렇게 모두 취소하는 행위를 롤백(Rollback) 이라고 한다.

프로그램이 다음과 같이 작성되어 있다고 가정하자.

상품발송() {
    포장();
    영수증발행();
    발송();
}

포장() {
   ...
}

영수증발행() {
   ...
}

발송() {
   ...
}

쇼핑몰 운영자는 포장, 영수증발행, 발송이라는 세가지 메소드 중 1가지라도 실패하면 모두 취소하고 싶어한다. 이런 경우 어떻게 예외 처리를 하는 것이 좋을까?

상품발송() {
    try {
        포장();
        영수증발행();
        발송();
    } catch (예외) {
        모두취소();  //하나라도 실패하면 모두 취소한다.
    }
}

포장() throws 예외 {
   ...
}

영수증발행() throws 예외 {
   ...
}

발송() throws 예외 {
   ...
}

위와 같이 코드를 작성하면 포장, 영수증발행, 발송이라는 세 개의 단위 작업 중 하나라도 실패할 경우 “예외”가 발생되어 상품발송이 모두 취소될 것이다.

만약 위처럼 “상품발송” 메소드가 아닌 포장, 영수증발행, 발송 메소드에 각각 예외 처리가 되어 있다고 가정 해 보자.

상품발송() {
    포장();
    영수증발행();
    발송();
}

포장(){
    try {
       ...
    } catch (예외) {
       포장취소();
    }
}

영수증발행() {
    try {
       ...
    } catch (예외) {
       영수증발행취소();
    }
}

발송() {
    try {
       ...
    } catch (예외) {
       발송취소();
    }
}

이렇게 각각의 메소드에 예외 처리가 되어 있다면 ‘포장은 되었는데 발송은 안되고’, ‘포장도 안되었는데 발송이 되고’ 이런 뒤죽박죽의 상황이 연출 될 것이다.

Throwable 클래스

Exception과 Error의 공통 부모 클래스는 당연히 Object 클래스이다. 그리고 공통 부모 클래스가 또 하나 있는데, java.lang 패키지에 선언된 Throwable 클래스다. 다시 말해서 Exception과 Error 클래스는 Throwable 클래스를 상속 받아 처리하도록 되어 있다.

상속 관계기 이렇게 되어 있는 이유는 Exception과 Error의 성격은 서로 다르지만, 모두 동일한 이름의 메소드를 사용해 처리 가능하도록 하기 위함이다. Throwable 클래스에 어떤 생성자가 선언되어 있는지 살펴보자.

  • Throwable()
  • Throwable(String message)
  • Throwable(String message, Throwable cause)
  • Throwable(Throwable cause)

아무런 매개 변수가 없는 기본 생성자, 예외 메시지를 String으로 넘겨주는 생성자, 그리고 별도로 예외의 원인을 Throwble 객체로 넘겨줄 수도 있다.

Throwable 클래스에 선언되어 있고, Exception 클래스에서 오버라이딩한 메소드 중 가장 많이 사용되는 메소드는 다음과 같다.

  1. getMessage()

예외 메시지를 String 형태로 제공 받는다. 예외가 출력 되었을 때 어떤 예외가 발생되었는지를 확인할 때 매우 유용하다. 즉, 그 메시지를 활용해 별도의 예외 메시지를 사용자에게 보여줄 수도 있다.

  1. toString()

예외 메시지를 String 형태로 제공 받고 getMessage() 메소드보다 약간 더 자세하게 예외 클래스 이름도 같이 제공한다.

  1. printStackTrace()

첫 줄에는 예외 메시지를 출력하고 두 번째 줄부터는 예외가 발생하게 된 메소드들의 호출 관계(stack trace)를 출력해준다.

public class ThrowableSample {
    public static void main(String[] args) {
        ThrowableSample sample = new ThrowableSample();
        sample.throwable();
    }

    public void throwable() {
        int[] intArray = new int[5];
        try {
            intArray = null;
            System.out.println(intArray[5]);
        } catch (Throwable t) {
            System.out.println(t.getMessage());
            System.out.println(t.toString());
            t.printStackTrace();
        }
    }
}

실행 결과

0개의 댓글