티스토리 뷰

학교 네트워크 프로그래밍 실습 수업이나 인강을 들을 때 원하는 로직을 짜고 빨간줄이 그어져있는 구문에 try-catch문이나 메서드에 throws를 날려줌으로써 예외처리를 하는 것을 많이 봤다. 하지만 이해가 되지 않았다. 예외 처리의 정의는 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것인데, 그럼 처음부터 빨간줄이 그어져있을 때 정상적으로 작동하는 로직으로 다시 바꾸면 되지 않나? 라고 생각했다.

 

내 말은 모든 경우의 수(혹시 모를 오류)를 대비하는 것 보단 처음부터 완성된 로직을 짜면 되지 않을까 ? 라는 생각이었다.(여기서 말하는 오류는 RuntimeException클래스들 이다)

 

하지만 책을 조금만 읽어봐도 내 의문에 대한 답을 찾을 수 있었다. 오늘은 예외처리에 대해 알아보려고 한다.

 

1. 프로그램 오류

프로그램이 실행 중 어떤 원인에 의해 오작동을 하고나 비정상적으로 종료되는 경우의 원인을 프로그램 에러 또는 오류라고 한다. 오류에는 크게 3가지가 있다.

 

컴파일 에러 : 컴파일 시에 발생하는 에러
런타임 에러 : 실행 시에 발생하는 에러
논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

 

우리는 코드를 짤 때 실행이 되지 않아요! 라고 말하는 에러는 대부분 컴파일 에러이다. 하지만 컴파일을 에러없이 성공적으로 마쳤다고 해서 프로그램의 실행 시에도 에러가 발생하지 않는 것은 아니다. 컴파일이 잠재적인 오류까지 걸러줄 수 없기 때문이다.

 

가끔씩 코드를 다 짜고 실행했을 때, 동작을 멈춘 상태로 오랜 시간 지속되거나, 갑자기 실행을 멈추고 종료되는 경우가 이에 해당한다.

그래서 이를 대비하는 것이 필요한데, 자바에서는 실행 시 발생할 수 있는 프로그램 오류를 '에러'와 '예외' 두가지로 구분 하였다.

 

에러는 메모리부족이나 스택오버플로우와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 수습될 수 있는 비교적 덜 심각한 것이다.


한마디로 에러는 발생하면 프로그램의 비정상적인 종료를 막을 방법이 없다.

 

2. 예외 클래스의 계층 구조

계층 구조는 정확히 알 필요가 없다. 만약 프로그램을 짜다 어떠한 Exception이 뜨면 인터넷에 찾으면 그만이다. 하지만 내가 굳이 계층 구조를 짚고 넘어가는 이유는 앞으로 계속 말할 예외의 정의에 대해 정확히 알고 넘어가면 이해하기가 훨씬 수월할 것이기 때문이다. 예외 클래스들은 다음과 같이 두 그룹으로 나눠질 수 있다.

 

Exception클래스와 그 자손들
RuntimeException클래스와 그 자손들

 

앞으로 RuntimeException클래스와 그 자손들은 'RuntimeException클래스들' 이라고 하고 제외한 나머지 클래스들을 'Exception클래스들'이라고 하겠다.


RuntimeException클래스들은 주로 프로그래머의 실수에 의해서 발생될 수 있다. 예를 들면 배열의 범위를 벗어난다던가, 값이 null인 참조변수의 멤버를 호출하려 하는 경우에 발생한다.


Exception클래스들은 사용자의 동작에 의해서 발생하는 경우가 많다. 다른 말로 버그라고 할 수 있겠다. 예를 들면 존재하지 않는 파일의 이름을 입력했다던가 입력한 데이터 형식이 잘못된 경우에 발생한다.

 

3. 예외처리하기 try-catch문

예외처리란, 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것이며, 예외처리의 목적은 예외의 발생으로 인한 비정상 종료를 막고 정상적인 실행상태를 유지하는 것이다.


예외처리를 하지 못하면 JVM의 예외처리기가 받아서 원인을 화면에 출력하는 동시에 프로그램은 종료한다.

예외를 처리하기 위해서는 try-catch문을 사용하며 작성은 아래와 같다.

try {
    //예외가 발생할 가능성이 있는 문장
}catch (Exception e) {
    //Exception이 발생했을 경우 처리하기 위한 문장
}

 

catch블럭은 여러개 쓸 수 있고 그 중 발생한 예외의 종류와 일치하는 catch블럭만 실행된다.
Exception이 발생했을 경우 처리할 때 필요한 또 다른 예외가 있을 수 있다. 이 때 catch블럭 안에 try-catch문을 작성하는 것이 가능하지만 참조변수의 이름이 같으면 안된다.


그럼 무슨 경우에 try-catch문을 사용할까 ? 예제를 통해 알아보자.

class ExceptionEx2 {
    public static void main(String args[]) {
        int number = 100;
        int result = 0;

        for(int i=0; i<10; i++) {
            result = number / (int)(Math.random() * 10);
            System.out.println(result);
    }
}
/*실행결과*/
20
100
java.long.ArithmeticException: / by zero
    at ExceptionEx2.main(ExceptionEx2.java:7)

 

이 코드는 100을 0~9사이의 임의의 정수로 나눈 결과를 출력하는 일을 10번 반복한 코드이다.
나누려는 숫자는 실행할 때마다 결과가 다르겠지만, 대부분의 경우 10번 출력되기 이전에 예외가 발생하여 비정상적으로 종료될 것이다. 결과에 나타난 메세지를 보면 0으로 나누려 했기 때문에 'ArithmeticException'이 발생했고, 발생위치는 7번째 라인이란 것을 알 수 있다. 정수는 0으로 나누는 것이 금지되어있기 때문에 발생한 것이다.


이제 어디서 왜 예외가 발생하는지 알았으니 try-catch문을 써서 예외가 발생해도 실행이 종료되지 않도록 수정해보자.

class ExceptionEx3 {
    public static void main(String args[]) {
        int number = 100;
        int result = 0;

        for(int i=0; i<10; i++) {
            try {
                result = number / (int)(Math.random() *10);
                System.out.println(result);
            }catch (ArithmeticException e) {
                System.out.println("0");
             }
    }
}

 

ArithmeticException이 발생했을 경우 화면에 0을 출력하도록 했다. ArithmeticException이 발생되면 해당하는 catch블럭을 찾아가서 문장을 실행하고 for문을 계속 반복하여 정상적으로 종료된다. 만일 예외처리를 하지 않았다면 랜덤숫자에 0이 뜨는 순간 예외가 발생해 비정상적으로 종료되었을 것이다.


여기서 한가지 의문이 든다. 정상적으로 종료되지 않았다는 것은 코드 내에 잘못된 코드가 있다는 것인데 출력만 하고 예외를 처리하지 않고 넘어가면 나중에 큰 문제로 번지지 않을까 ?


위의 예제는 어떤 상황에서 예외처리를 하는지 알려주기 위함으로 사용을 했고, 절대로 출력만 하고 넘어가면 안된다.
출력만 하고 넘어간다는 것은 콘솔창에 띄워주기만 하고 원래 정상적인 코드였던 것처럼 넘어간다는 의미이다.
만약 개발자가 콘솔창에 있는 에러문장을 보지 않으면 오류를 잡지 않고 무시해버리는 무시무시한 코드가 되버린다.

 

절대로 catch블럭 안을 그냥 두거나 출력만 하고 넘어가지 말자. 무책임한 개발자나 다름없다. 
다른 글에서 좋은 비유가 있어서 가져다쓰겠다. 
catch블럭 안을 그냥 두는 것은 횡단보도를 건너다 차에 치였는데 병원에 가지 않고 그대로 갈 길 가는 것과 똑같고, 
출력만 한다는 것은 교통사고가 났어요!하는 표지판을 세워두기만 하는 것이다.

 

예외가 발생했을 떄, catch블럭의 괄호 안에 참조변수를 그대로 출력하는 것보다 발생한 예외에 대한 정보가 담겨있는 인스턴스에서 메서드를 이용해 화면에 뿌리는 것이 그나마 좋다. 자주 사용되는 메서드는 다음과 같다.

 

printStackTrace() : 예외발생 당시 호출스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

 

4. 메서드에 예외 선언하기

예외를 처리하는 방법에는 try-catch문을 사용하는 것 외에 예외를 메서드에 선언하는 방법이 있다.
메서드에 예외를 선언하려면 메서드의 선언부에 throws를 쓰고 발생할 수 있는 예외를 적어주기만 하면 된다.
예외가 여러 개일 경우 쉼표로 구분한다.

void method() throws Exception1, Exception2, ... ExceptionN {
    //메서드 내용
}

 

메서드의 선언부에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드의 선언부를 보고 어떤 예외들이 처리되어져야 하는지 쉽게 알 수 있다. 메서드를 사용하는 쪽에서 이에 대한 처리를 하도록 강요하기 때문에 프로그래머들의 짐을 덜어준다.


사실 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다. 그럼 계속해서 자신을 호출한 메서드에게 전달되다가 제일 마지막에 있는 main메서드에서도 예외가 처리되지 않으면 main메서드마저 종료되어 프로그램 전체가 종료된다.

 

이해를 쉽게 하기 위해 예제를 들겠다.

class ExceptionEx12 {
    public static void main(String[] args) throws Exception {
        method1(); //같은 클래스내 static멤버이므로 객체생성없이 호출가능
    }

    static void method1() throws Exception {
        method2();
    }

    static void method2() throws Exception {
        throw new Exception(); //자체적으로 예외생성
    }

}

 

이처럼 예외를 자신을 호출한 메서드에게 넘겨줄 수는 있다. 하지만 이 말은 예외가 처리된 것이 아니고 단순히 전달만 한 것이라고도 말할 수 있다. 결국 어느 한 곳에는 반드시 예외처리를 해주어야 한다.

class ExceptionEx13 {
    public static void main(String[] args) {
        method1();
    }

    static void method1() {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("method1메서드에서 예외처리됨");
            e.printStackTrace();
        }
    }
}

 

5. finally블럭

finally블럭은 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적을 가지고 있다.
try-catch-finally 순서로 구성이 되며 예외가 발생하지 않은 경우에는 try-finally순으로 실행된다.

try {
    //예외가 발생할 가능성이 있는 문장
}catch (Exception e) {
    //예외처리를 위한 문장
}finally {
    //예외의 발생여부 관계없이 꼭 실행되어야 하는 문장
}

 

내가 예외처리에 대해 설명하기 전에 궁금해했던 오류에 대해 처리하는 거라면 처음부터 빨간줄이 그어져있을 때 정상적으로 작동하는 로직으로 다시 바꾸면 되지 않나? 에 대한 답은 질문부터 잘못된 것이였다.

 

내가 봐왔던 예외처리를 하는 방법이 원하는 코드를 짜고 빨간 줄을 try-catch문으로 바꾸거나 throw로 넘기는 것인데 그냥 빨간 줄을 없애려고 하는 거라고 생각했다. (저렇게 생각하면 큰일난다. 앞서 말했던 무책임한 개발자가 되는 것이다.) 이렇게 바로 넘기는 건 예외처리를 하는 순서에 대한 오해였다. 예외가 된 문장(빨간줄)을 예외처리를 하는 것이 아닌 예외가 발생할 가능성이 있는 문장을 사전에 방지하기 위함이다.

 

finally블럭은 try블럭에서 return문이 실행되는 경우에도 finally블럭의 문장들이 먼저 실행 된 후에, 현재 실행 중인 메서드를 종료한다.

 

6. 예외 되던지기

한 메서드에서 발생할 수 있는 예외가 여러개인 경우가 있다. 이 경우엔 몇개는 try-catch문으로 처리하고 나머지는 선언부에 지정함으로써 양쪽에서 나눠서 처리되도록 할 수 있다. 심지어는 단 하나의 예외에 대해서도 나눠서 처리할 수 있는데, 사실은 하나의 예외를 동시에 양쪽에서 처리하는 것이 아닌, try-catch문에서 예외를 처리한 후에 인위적으로 다시 발생시켜 throws를 통해서 처리가 가능하다 이것을 '예외 되던지기(exception re-throwing)' 라고 한다.

 

작성하는 방법은 생각보다 간단하다. try-catch문을 사용해서 예외처리를 해줌과 동시에 메서드 선언부에 throws를 지정만 해주면 된다.

class ExceptionEx4 {
    public static void main(String[] args) {
        try {
            method1();
        }catch (Exception e) {
            System.out.println("main메서드에서 예외가 처리되었습니다.");
        }
    }

    static void method1() throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("method1메서드에서 예외가 처리되었습니다.");
            throw e; //다시 예외발생
        }
    }
}

 

예외 되던지기 말고도 자동으로 자원을 반환하는 try-catch-resources문, 필요에 따라 프로그래머가 새로운 예외 클래스를 정의해서 사용하는 사용자 정의 예외, 한 예외가 다른 예외를 발생시키는 연결된 예외(chained exception) 등 많은 예외처리 방법이 있지만 여기까지 하도록 하겠다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday