본문 바로가기

Java/문법 및 이해

[Java] 자바의 예외처리 (Exception)

프로그램을 만들다 보면 수없이 많은 오류가 발생한다. 물론 오류가 발생하는 이유는 프로그램이 오동작을 하지 않기 하기 위한 자바의 배려이다. 하지만 때로는 이러한 오류를 무시하고 싶을 때도 있고, 오류가 날 때 그에 맞는 적절한 처리를 하고 싶을 때도 있다. 이에 자바는 try ... catch, throw 구문을 이용해 오류를 처리 해야한다.

 

 

1. 예외처리 

아래는 예외처리를 위한 기본적인 아키텍처이다.

try {
    ...
} catch(예외1) {
    ...
} catch(예외2) {
    ...
}

 

예를 하나 들면,

숫자를 0으로 나누었을 때 발생하는 예외를 처리하려면 다음과 같이 할 수 있다.

int c;
try {
    c = 4 / 0;
} catch(ArithmeticException e) {
    c = -1;  // 예외가 발생하여 이 문장이 수행된다.
}

ArithmeticException이 발생하면 c에 -1을 대입하도록 예외를 처리한 것이다. ArithmeticException e에서 e는 ArithmeticException 클래스의 객체, 즉 오류 객체에 해당한다. 이 오류 객체를 통해 해당 예외 클래스의 변수나 메서드를 호출할수도 있다.

 

 

2. finally

프로그램 수행 도중에 예외가 발생하면 프로그램이 오류에 의해 중지되거나 예외 처리에 의해 catch 구문이 실행된다. 하지만 어떤 예외가 발생하더라도 반드시 실행되어야 하는 부분이 있어야 한다면 어떻게 해야할까?

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;
        } catch (ArithmeticException e) {
            c = -1;
        } finally {
            sample.shouldBeRun();  // 예외에 상관없이 무조건 수행된다.
        }
    }
}

finally 구문은 try 문장 수행 중 예외발생 여부에 상관없이 무조건 실행된다. 따라서 위 코드를 실행하면 sample.shouldBeRun() 메서드가 수행되어 "ok, thanks" 문장이 출력된다.

 

 

3. RuntimeExeption과 Exception

예외를 직접 만들어볼 수도 있다.

 

(1) RuntimeException

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

 

class FoolException extends RuntimeException {
}

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("genious");
    }
}

throw new FoolException() 이라는 예외처리 문구가 보일 것이다.

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

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

 

(2) Exception

class FoolException extends Exception {
}

public class Sample {
    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) {
        Sample sample = new Sample();
        sample.sayNick("fool");
        sample.sayNick("genious");
    }
}

RuntimeException을 상속하던 것을 Exception을 상속하도록 변경했다. 이렇게 하면 Sample 클래스에서 컴파일 오류가 발생할 것이다. 예측 가능한 Checked Exception이기 때문에 예외처리를 컴파일러가 강제하기 때문이다.

 

이러한 컴파일 오류를 막기 위해 sayNick 메서드에서 try... catch 구문으로 FoolException을 처리해야한다.

 

 

4. 예외 던지기 (throws)

위에서 sayNick 메서드에서 FoolException을 발생시키고 예외처리도 sayNick 메서드에서 했는데 이렇게 하지 않고 sayNick을 호출한 곳에서 FoolException을 처리하도록 예외를 위로 던질 수 있는 방법이 있다.

 

class FoolException extends Exception {
}

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

    public static void main(String[] args) {
        Sample sample = new Sample();
        try {
            sample.sayNick("fool");
            sample.sayNick("genious");
        } catch (FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }
}

main 메서드에서 try... catch로 sayNick 메서드에 대한 FoolException 예외를 처리하였다.

 

그러면, FoolException 처리를 sayNick 메서드에서 하는것이 좋을까? 아니면 throws를 이용하여 예외처리를 main 메서드에서 하는것이 좋을까? sayNick 메서드에서 처리하는 것과 main 메서드에서 처리하는 것에는 아주 큰 차이가 있다.

 

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

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

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

 

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

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

 

 

5. 트랜잭션

바로 위의 throws의 try... catch 구문은 트랜잭션과 매우 밀접한 관련이 있다.

 

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

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

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

 

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

포장() {
   ...
}

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

발송() {
   ...
}

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

 

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

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

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

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

위에서 상품발송 메서드가 동작할 때, 포장, 영수증발행, 발송 중에 하나라도 실패할 경우 "예외"가 발생되어 상품발송이 모두 취소될 것이다.

 

그런데 다음처럼 "상품발송" 메서드가 아닌 포장, 영수증발행, 발송메서드에 각각 예외처리가 되어 있다고 가정 해 보자.

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

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

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

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

이렇게 각각의 메서드에 예외가 처리되어 있다면 포장은 되었는데 발송은 안되고 포장도 안되었는데 발송이 되고 이런 뒤죽 박죽의 상황이 연출될 것이다. 실제 프로젝트에서도 두번째 경우처럼 트랜잭션관리를 잘못하여 고생하는 경우를 많이 보았는데 이것은 일종의 재앙에 가깝다.

위처럼은 하지말자. ㅇㅇ;;