본문 바로가기
언어/자바의 정석

[08-1] 예외 처리, try-catch, throw, throws

by chan10 2021. 2. 17.

예외처리(exception handling)

프로그램 오류

o  프로그램 오류는 발생 시점에 따라 나눌 수 있다.

컴파일 에러 : 컴파일 시에 발생하는 에러

런타임 에러 : 실행 시에 발생하는 에러

논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

 

o  소스코드를 컴파일 시 컴파일러가 소스코드에 대해 오타나 잘못된 구문, 자료형 체크 등의 기본적인 검사를 수행하여 오류가 있는지를 알려준다. 알려준 오류를 수정하고 컴파일을 마치면 클래스 파일이 생성되어 실행할 수 있다.

o  컴파일러가 소스코드의 오류는 검사하지만 실행 중에 발생할 수 있는 에러는 검사하지 못하기에 실행 중 에러가 발생할 수 있다.

o  자바는 프로그램 실행 시 발생할 수 있는 에러를 두 가지로 구분하였다.

에러(error)

- 프로그램 코드에 의해서 수습될 수 없으며 발생하면 복구할 수 없는 심각한 오류

- 메모리 부족(OutOfMemoryError), 스택오버플로우(StackOverflowError)

- 주로 JVM에서 발생시키며 프로그램의 비정상적인 종료를 막을 수 없다.

예외(exception)

프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

발생하더라도 수습될 수 있는 비교적 덜 심각한 오류

적절한 코드를 미리 작성함으로써 비정상적인 종료를 막을 수 있음

 

예외 클래스의 계층구조

o  자바는 실행 시 발생할 수 있는 오류(Exception, Error)를 클래스로 정의했으며 모든 클래스는 Object클래스의 자손이므로 Exception, Error클래스역시 Object클래스의 자손클래스. 모든 예외의 최고 조상은 Exception클래스.

o  예외 클래스는 다음과 같이 두 분류로 나눌 수 있다.

Exception 클래스들

사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외

ex) 존재하지 않는 파일의 이름 입력, 잘못된 클래스 이름 작성, 입력한 잘못된 데이터 형식 경우

RuntimeException클래스들

프로그래머의 실수로 발생하는 예외

ex) 배열의 범위를 벗어남, null값의 참조변수 호출, 잘못된 클래스 형변환, 정수를 0으로 나누려고 하는 경우

 

예외처리하기 : try-catch

o  예외처리(exception handling)

정의 : 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것

목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

o  발생한 예외를 처리하지 못하면 프로그램은 비정상 종료되며 처리되지 못한 예외(Uncaught Exception)JVM예외처리기(Uncaught Exception Handler)’가 받아서 예외의 원인을 출력한다.

o  예외 처리를 하기위해서는 try-catch문을 사용한다.

try {
        //예외가 발생할 수 있는 문장들을 넣는다.
        } catch (Exception1 e1) {
            // Exception1이 발생한 경우 이를 처리하기 위한 문장을 작성한다.
        } catch (Exception2 e2) {
            // Exception2가 발생한 경우 이를 처리하기 위한 문장을 작성한다.
        }

o  하나의 try블록에는 여러 catch블록이 올 수 있으며 이 중 예외의 종류와 일치하는 한 개의 catch블록만 수행된다. 발생한 예외의 종류와 일치하는 것이 없으면 예외는 처리되지 않는다.

o  try, catch 블록은 블록 내에 포함된 문장이 하나뿐 이더라도 괄호 { }를 생략할 수 없다.

 

public class Test {    
    public static void main(String[] args) {
        try {
            try { } catch (Exception e) { }
        } catch (Exception e) {
//            try { } catch (Exception e) { }    //에러, catch문의 변수가 중복되었다.
            try { } catch (Exception e1) { }
        } 
    }

-       위의 예제는 단순히 사용 예를 보여주기 위한 코드이다.

-       catch블럭 내에서도 예외가 발생할 수 있기에 하나의 메서드 내에 또다른 try-catch문이 포함될 수 있다. 그러나 또 다른 try-catch문이 포함된 경우 같은 이름의 참조변수를 선언할 수 없다. catch블록에 선언된 참조변수 영역이 서로 겹치므로 구별이 되지 않기 때문에 다른 이름을 사용해야 한다.

 

public class Test {    
    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);    
        }
    }
}

-       예제는 랜덤 값을 받아서 나누는 number값을 나누는 예제인데 랜덤 값에서 0이 나오면 위와 같은 ‘ArithmeticException’가 발생한다. 정수를 0으로 나누는 것은 금지되어 있으며 실수를 0으로 나는 것은 금지되어 있지 않다.

-       9번째줄에서 해당 예외가 발생했음을 알 수 있다.

 

public class Test {    
    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");
        }
    }
}
}

-       이전 예제에 try-catch문을 추가하여 ‘ArithmeticException’이 발생 시 0을 출력하도록 했다. 1,7번째에서 ArithmeticException’이 발생하여 0이 출력되었다.

-       ArithmeticException’이 발생 시 try-catch문에서 ‘ArithmeticException’와 일치하는 catch블록을 찾아 블록 내의 문장을 실행한 다음 try-catch문을 벗어나 for문을 실행한다. 예외 처리가 없었다면 중간이 프로그램이 비정상적으로 종료되었을 것이다.

-       예외 처리를 하였으나 ArrayIndexOutOfBoundsException 해당 사항에 일치하지 않은 에러가 발생한 경우에는 똑같이 예외 출력 비정상적으로 프로그램이 종료된다.

 

try-catch문에서의 흐름

o  try-catch문에서 예외가 발생한 경우와 발생하지 않은 경우에 따라 흐름이 달라진다.

o  try블록에서 예외가 발생하면, 예외가 발생한 다음 위치의 문장부터는 try블록의 문장을 수행하지 않으므로 try블록에 포함시킬 코드를 잘 정해야 한다.

- try블럭 내에서 예외가 발생한 경우

1. 발생한 예외와 일치하는 catch블록이 있는지 확인한다.

2. 일치하는 catch블록을 찾게 되면, catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블록을 찾지 못하면, 예외는 처리되지 못한다.

 

- try블럭 내에서 예외가 발생하지 않은 경우

1. catch블록을 거치지 않고 try-catch문을 빠져나가서 수행을 계속한다.

 

public class Test {    
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);
        } catch(ArithmeticException e) {
            System.out.println(5);
        }
    }
}

-       예제에서 3까지 출력을 한 다음 try블록에서 예외가 발생(0/0)했기 때문에 다음 문장(4)은 실행되지 않고 바로 catch문으로 넘어간다.

-       catch문에서 동일한 예외를 찾아 해당 블록을 수행한 후 try-catch문을 벗어나 다음 문장을 수행한다.

 

예외의 발생과 catch블럭

o  catch블록의 괄호( )내에는 처리하고자 하는 예외와 같은 타입의 참조변수를 선언하며 예외 발생 시 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다. 그래서 예외가 발생한 문장에 try블록이 포함되어 있다면, 이 예외를 처리할 수 있는 catch블록을 찾는다.

o  catch블록을 차례로 내려가면서 괄호( )에 선언된 참조변수예외클래스의 인스턴스‘instanceof연산자를 이용하여 검사true, false결과를 받는다,

o   검사 결과가 true인 예외는 처리되지만 검사 결과가 truecatch블록이 없으면 하나도 실행되지 않는다.

 

public class Test {    
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);
        } catch(Exception e) {
            System.out.println(5);
        }
    }
}

-       모든 예외 클래스는 Exception의 자손이므로 catch블럭 괄호( ) Exception클래스 타입의 참조변수를 선언하면 어떤 종류의 예외가 발생해도 catch에서 처리한다.

 

public class Test {    
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);    //예외발생
            System.out.println(4);    //실행되지 않는다.
        } catch(ArithmeticException ae) {
            if(ae instanceof ArithmeticException)
                System.out.println("true");
            System.out.println("ArithmeticException");
        } catch(Exception e) {    //ArithmeticException을 제외한 모든 예외 처리
            System.out.println("Exception");
        }
        System.out.println(6);
    }
}

-       두 개의 catch블록으로 ArithmeticException클래스 타입의 참조변수 Exception클래스 타입의 참조변수를  사용하였다.

-       try에서 ArithmeticException 발생하여 번째 검사에서 일치하는 catch블록을 찾았기에 다음 catch 블록은 검사를 하지 않는다. 만일 첫번째 catch블록에서 일치하지 않았다면 두번째 catch블록인 Exception클래스 타입의 참조변수에서 처리되었다.

-       이처럼 try-catch문의 마지막에 Exception클래스 타입의 참조변수를 선언한 catch블록을 사용하면 어떤 종류의 예외가 발생해도 처리 있다.

 

printStackTrace( )getMessage( )

o  예외가 발생했을 때는 예외 클래스의 인스턴스에 발생한 예외에 대한 정보가 담겨져 있다. catch블록의 괄호 ( )안에 선언된 참조 변수를 통해 이 인스턴스에 접근할 수 있으며 catch블럭 내에서만 사용이 가능하다.

o  자주 사용하는 메서드는 다음과 같다.

printStackTrace( ) : 예외발생 당시의 호출 스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.

getMessage( ) : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

 

public class Test {    
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);    //예외발생
            System.out.println(4);    //실행되지 않는다.
        } catch(ArithmeticException ae) {
            ae.printStackTrace();
            System.out.println("예외메시지 : "+ae.getMessage());
        }
        System.out.println(6);
    }
}

-       예외가 발생해서 비정상 종료했을 때와 비슷하지만 예외는 try-catch문에 의해 처리되어 정상으로 종료되었다.

-       ArithmeticException인스턴스의 printStackTrace( ) 사용하여 호출 스택에 대한 정보와 예외 메시지를 출력하였다.

-       이처럼 예외가 발생하여도 프로그램이 비정상적으로 종료하지 않도록 해주는 동시에 예외의 발생원인도 있다.

 

멀티 catch블럭

o  JDK1.7부터 멀티 catch블럭이라고 하여 catch블록을 ‘|’기호(논리 연산자X)을 이용하나의 catch블록으로 합칠 수 있게 하였다.

o  중복된 코드를 줄일 수 있으며 ‘|’기호로 연결할 수 있는 예외 클래스의 개수에는 제한이 없다.

o  그러나 멀티 catch블록의 ‘|’기호로 연결된 예외 클래스가 조상과 자손 관계에 있다면 조상 클래스만 써주어도 똑같기에 컴파일 에러가 발생한다. (불필요한 코드를 제거하라는 의미)

catch블럭

멀티 catch블럭

try {
    ...
    } catch (ExceptionA e) {
        e.printStackTrace();
    } catch (ExceptionB e2) {
        e2.printStackTrace();
    }
try {
    ...
    } catch (ExceptionA | ExceptionB e) {
        e.printStackTrace();
    }

 

try {
    System.out.println(0/0);
    } catch (Exception | ArithmeticException e) {    //에러 
        
    }

-       Exception은 모든 예외의 최고 조상클래스 이기에 조상, 자손 관계에 의해 에러가 발생한다.

 

o  멀티 catch블록은 하나의 catch블록으로 여러 예외를 처리하는 것이기에 예외 처리 시 어떤 예외가 발생한 것인지 알 수 없다. 따라서 참조변수로 catch블록에 ‘|’기호로 연결된 예외 클래스의 공통 분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.

o  필요하다면 instanceof로 어떤 예외가 발생할 것인지 확인하고 개별적으로 처리할 수 있다.

try {
        ...
    } catch (ExceptionA | ExceptionB e) { 
        e.methodA();    //에러, ExceptionA에 선언된 methodA()는 호출불가
        
        if(e instanceof ExceptionA) {
            ExceptionA e1 = (ExceptionA)e;
            e1.methodA();    //OK. ExceptionA에 선언된 메서드 호출가능
        } else {    //if(e instanceof ExceptionB)
            ...
        }
        e.printStackTrace
    }

 

o  멀티 catch블록에 선언된 참조변수는 상수이므로 값을 변경할 수 없는 제약이 있는데 이것은 여러 catch블록이 하나의 참조변수를 공유하기에 생기는 제약이다.

 

예외 발생시키기 - throw

o  키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있다.

1.     먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음

Exception e = new Exception(“고의 발생”);

2.     키워드 throw를 이용해서 예외를 발생시킨다.

throw e;

 

public class Test {    
    public static void main(String[] args) {
        try {
            Exception e = new Exception("에러 고의 발생");
            throw e;
//            throw new Exception("에러 고의 발생");    // 한줄로도 작성 가능
        } catch(Exception e){
            System.out.println("에러메시지 : "+e.getMessage());
            e.printStackTrace();
        }
        System.out.println("프로그램이 정상 종료되었습니다.");
    }
}

-       Exception인스턴스 생성 시 생성자에 String을 넣어주면 에러 메시지로 저장되며, getMessage( )를 이용해서 얻을 수 있다.

            

public class Test {    
public static void main(String[] args) {
    throw new Exception("에러 고의 발생");    // 한줄로도 작성 가능
    }
}

-       위 예제를 작성하면 예외처리가 되어야 할 부분에 예외처리가 되어 있지 않다는 컴파일 에러가 발생한다.

-       Exception클래스들(Exception클래스와 자손들)이 발생할 가능성이 있는 문장들에 대해 예외처리 해주지 않으면 컴파일도 되지 않는다.

 

public class Test {    
    public static void main(String[] args) {
        throw new RuntimeException();
    }
}

-       위 예제는 컴파일은 성공하나 RuntimeException 발생하면서 프로그램이 비정상으로 종료된다.

-       RuntimeException클래스와 자손에 해당하는 예외는 프로그래머에 의해 실수로 발생하는 것이기에 예외처리를 강제하지 않는다.

 

checkedExceptionuncheckedException

o  컴파일러가 예외처리를 확인하지 않는 RuntimeException클래스들은 ‘unchecked예외라고 부르고, 예외 처리를 확인하는 Exception 클래스들은 ‘checked 예외라고 부른다.

구분

Checked Exception

Unchecked Exception

다른 말

Compile Exception

Runtime Exception

확인 시점

컴파일(Compile) 시점

런타임(Runtime) 시점

컴파일 단계에서 확인이 불가하며 프로그램 조직실행 중 발생

처리 여부

반드시 try-catch문이나 throw로 던지는 명시적 예외 처리를 해야 한다.

명시적 예외처리를 하지 않아도 된다.

트랜잭션 처리

예외 발생시 롤백(rollback)하지 않음

예외 발생시 롤백(rollback)해야 함

종류

IOException, CalssNotFoundException

NullPointerException, ClassCastException

참고

Error와 그 자손은 try-catch로 처리할 수 없기에 Unchecked Exception이다.

 

메서드에 예외 선언하기 - throws

o  예외 처리 방법은 try-catch방법 외에 메서드에서 선언하는 방법도 있다. 메서드 선언부에 throws를 사용 후 발생할 수 있는 예외를 적어주면 된다. 예외가 여러 개인 경우 쉼표(,)로 구분한다.

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

 

o  예외의 최고 조상인 Exception클래스를 메서드에 선언하면 이 메서드는 모든 예외가 발생할 수 있다는 뜻이다. 이렇게 선언하면 자손타입의 예외 처리까지 할 수 있다는 점에 주의한다. 오버라이딩 시 예외의 개수가 아니라 상속관계까지 고려해야 한다.

o  메서드에서 예외가 발생하였는데 try-catch문을 통해 예외 처리가 되지 않을 경우 해당 프로그램을 비정상적으로 종료 시키고 throws를 통해 넘길 경우 해당 메서드를 종료시킨다.

o  JAVA API문서의 java.lang.Object클래스의 wait메서드에 보면 InterruptedExceptionthrows와 같이 적혀있는 것을 볼 수 있는데 InterruptedException가 발생할 수 있으니 메서드 호출 시 InterruptedException처리 해주어야 한다는 뜻이다. 보통 반드시 처리해야 하는 예외만 선언하기에 RuntimeException은 예외처리 해주지 않아도 된다.

o  메서드에 throws를 명시하는 것은 사실 예외를 처리하는 것 보다 자신을 호출한 메서드에게 예외를 전달하여 예외 처리를 맡기는 것이다. 이런 식으로 예외를 전달하여 마지막 main메서드에서도 처리가 되지 않으면 프로그램이 종료된다.

 

public class Test {    
    public static void main(String[] args) throws Exception { //에러 처리X , 에러 발생
        method1();
    }
    static void method1() throws Exception { //main() 에러 넘김
        method2();
    }
    static void method2() throws Exception { //method1() 에러 넘김
        throw new Exception();    //에러 발생
    }
    
}

-       예외 결과에서 예외 발생과 예외 발생 시 호출 스택을 알 수 있으며 위의 결과는 다음과 같다.

1.     예외가 발생했을 때, 모두 3개의 메서드(main,method1,method2)가 호출 스택에 있었으며

2.     예외가 발생한 곳은 제일 윗줄인 method2()라는 것과

3.     main메서드가 method1( ), method1( )method2( )를 호출했다는 것을 알 수 있다.

-       method2( )에서 예외가 발생하여 method1( )에게 예외를 전달하였고 method1( )에서도 예외 처리 없이 main으로 전달했다. 마지막 main메서드까지 예외를 처리하지 않았기에 프로그램이 예외로 인해 비정상적으로 종료되었다.

 

o  예외가 발생한 메서드에서 자신을 호출한 메서드에게 예외를 넘길 수 있지만 예외가 처리된 것은 아니다. 그래서 어느 한 곳에서는 try-catch문으로 예외 처리를 해주어야 한다.

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

-       method1( )에서 예외가 발생했으나 method1메서드에서 처리하지 않고 넘겨주어 main메서드에서 처리되었다.

-       예외를 처리하지 않고 호출한 메서드에게 넘겨주면 호출한 메서드는 메서드를 호출한 라인에서 예외가 발생한 것으로 간주한다.

-       만약 method1( )에서 예외를 처리하게 했다면 main메서드는 예외가 발생한 사실조차 모른다.

 

import java.io.*;
 
public class Test {    
    public static void main(String[] args) {
        try {
            File f = createFile(args[0]);
            System.out.println(f.getName()+"파일이 성공적으로 생성되었습니다.");
        } catch(Exception e) {
            System.out.println(e.getMessage() + "다시 입력해 주시기 바랍니다.");
        }
}
    
    static File createFile(String fileName) throws Exception{
        if(fileName==null || fileName.equals(""))
            throw new Exception("파일이름이 유효하지 않습니다.");
        
        File f = new File(fileName);    //File객체 생성
        //createNewFile()를 이용하여 실제 파일 생성
        f.createNewFile();
        return f;        
    }
}

-       위 예제는 이클립스에서 arguments를 이용해서 파일명을 전달받아 파일을 생성하도록 하였으며 잘못된 파일명(null 또는 빈문자열)을 입력했을 때 예외가 발생하도록 되어 있다.

-       createFile( )에서 예외가 발생하면 createFile( )를 호출한 메서드(main)에 예외를 전달하여 호출한 쪽에서 처리하도록 하였다.