[자바] 예외처리

June·2021년 1월 3일
1

자바

목록 보기
21/36

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다. 이를 발생 시점에 따라 컴파일 에러런타임 에러로 나눌 수 있다.

자바는 런타임에러를 에러예외로 구분하였다.

에러는 메모리부족이나 스택오버플로우와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 프로그램 코드에 의해서 수습될 수 있다.

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

RuntimeException 클래스들은 주로 프로그래머의 실수에 의해 발생될 수 있는 예외들로 자바의 프로그래밍 요소들과 관계가 깊다. 예를 들면, 배열의 범위를 벗어난다던가(ArrayOutOfBoundsException), 값이 null인 참조변수의 멤버를 호출하려 했다던가(NullPointerException), 클래스간의 형변환을 잘못했다던가(ClassCastException), 정수를 0으로 나누려고(ArithmeticException)하는 경우에 발생한다.

Exception클래스들은 주로 외부의 영향으로 발생할 수 있는 것들로서, 프로그램의 사용자들의 동작에 의해서 발생하는 경우가 많다. 예를 들면 존재하지 않는 파일의 이름을 입력했다던가(FileNotFoundException), 실수로 클래스의 이름을 잘못 적었다던가(ClassNotFoundException), 또는 입력한 데이터 형식이 잘못된(DataFormatException)경우에 발생한다.

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

발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외는 JVM의 '예외처리기(UncaughtExceptionHandler)'가 받아서 예외의 원인을 화면에 출력한다.

try{
     //예외가 발생할 가능성이 있는 코드
} catch (Exception1 e1) {
     // Exception1이 발생했을 경우, 처리하는 코드 
} catch (Exception2 e2) {
     // Exception2이 발생했을 경우, 처리하는 코드 
}
...

catch 블록은 발생한 예외의 종류와 일치하는 단 한개의 catch블럭만 수행된다. 발생한 예외의 종류와 일치하는 catch 블럭이 없으면 예외는 처리되지 않는다.

try-catch문에서의 흐름

  • try 블럭내에서 예외가 발생한 경우
    1. 발생한 예외와 일치하는 catch블럭이 있는지 확인한다.
    2. 일치하는 catch 블럭을 찾게 되면, 그 catch 블랙 내의 문장들을 수행하고 전체 try-catch 문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블럭을 찾지 못하면 예외는 처리되지 못한다.
  • try 블럭내에서 예외가 발생하지 않은 경우
    1. catch 블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.

printStackTrace()와 getMessage()

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

멀티 catch 블럭

JDK1.7부터 여러 catch블럭을 | 기호를 통해서, 하나의 catch 블러긍로 합칠 수 있게 되었다.

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

만일 멀티 catch 블럭의 | 기호로 연결된 예외 클래스가 조상과 자손의 관계에 있다면 컴파일 에러가 발생한다.

그리고 멀티 catch는 하나의 catch 블럭으로 여러 예외를 처리하는 것이기 때문에, 발생한 예외를 catch 블럭으로 처리하게 되었을 때, 멀티 catch 블럭 내에서는 실제로 어떤 예외가 발생한 것인지 알 수 없다. 그래서 참조변수 e로 멀티 블럭에 | 기호로 연결된 예외 클래스들의 공통 분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.

예외 발생시키기

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

class ExceptionEx10 {
    public static void maibn(String[] args) {
        throw new Exception(); // Exception을 고의로 발생시킨다.
    }
}

이 예제를 작성한 후 실행하면 컴파일조차 되지 않는다. Exception클래스들(Exception 클래스와 그 자손들)이 발생할 가능성이 있는 문장들에 대해 예외처리를 해주지 않으면 컴파일이 되지 않는다.

class ExceptionEx10 {
    public static void maibn(String[] args) {
        throw new RuntimeException(); // Exception을 고의로 발생시킨다.
    }
}

이 예제는 예외처리를 하지 않아도 성공적으로 컴파일된다. 이유는 RuntimeExcpetion 클래스들은 프로그래머에 의해 실수로 발생하는 것들이기 때문에 예외처리를 강제하지 않는 것이다.

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

메서드에 예외 선언하기

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

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

메서드에 예외를 선언할 때 일반적으로 RuntimeException 클래스들은 적지 않는다. 이 들을 메서드 선언부의 throws에 선언한다고 해서 문제가 되지는 않지만, 보통 반드시 처리해주어야 하는 예외들만 선언한다.

사실 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.

finally 블럭

finally 블럭은 try-catch문과 함꼐 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.

try {
} catch (Exception e) {
} finally {
	//예외 발생여부와 관계 없이 항상 수행
}

자동 자원 반환 try-with-resources문

주로 입출력에 사용되는 클래스 중에서는 사용한 후에 꼭 닫아 줘야 하는 것들이 있다. 무넺는 close()가 예외를 발생시킬 수 있다는데 있다. try-with-resources문의 괄호 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동으로 close()가 호출된다.

try (FileInputStream fis = new FileInputStream("score.dat");
     DataInputStream dis = new DataInputSTream(fis)) {
     ///
}

사용자 정의 예외 만들기

보통 Exception 클래스로부터 상속받는 클래스를 만들지만, 필요에 따라서 알맞은 예외 클래스를 선택할 수 있다.

class MyException extends Exception {
    MyException(String msg) { // 문자열을 배개변수로 받는 생성자
        super(msg); // 조상인 Exception 클래스의 생성자 호출
    }
}

사용자정의 예외 클래스도 메시지를 저장할 수 있으려면, 위에서 보는 것과 같이 String을 매개변수로 받는 생성자를 추가해줘야 한다.

기존의 예외 클래스는 주로 Exception을 상속받아서 checked예외로 작성하는 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아서 작성하는 쪽으로 바뀌어가고 있다. 'checked예외'는 반드시 예외처리를 해주어야 하기 때문에 예외처리가 불필요한 경우에도 try-catch문을 넣어서 코드가 복잡해지기 때문이다.

예외 되던지기

반환값이 있는 return문의 경우, catch 블럭에도 return 문이 있어야 한다. 예외가 발생했을 경우에도 값을 반환해야하기 때문이다.

또는 catch 블럭에서 예외 되던지기를 해서 호출한 메서드로 예외를 전달하면, return 문이 없어도 된다.

Checked Exception vs Unchecked Exception

자바에서 RuntimeException과, 이를 상속한 클래스를 조금 특별하게 취급하는데, 명시적으로 예외 처리를 하지 않아도 되기 때문이다. 반면 Unchecked Exception은 다른 곳으로 예외를 던지지 말고 즉시 예외 처리를 해주는 것이 좋다.

    @Override
    public String addImage(int reservationInfoId, MultipartFile file) {
        String filename = createFileName(file);
        try (
            FileOutputStream fileOutputStream = new FileOutputStream(IMAGE_FILE_SAVE_PATH + filename);
             InputStream inputStream = file.getInputStream();) {
            int readCount = 0;
            byte[] buffer = new byte[BYTE_BUFFER_SIZE];

            while((readCount = inputStream.read(buffer)) != -1){
                fileOutputStream.write(buffer,0, readCount);
            }
        } catch (Exception e) {
            LOGGER.error("IOException log = {}", e.toString());
        }
        return filename;
    }

예시 코드인데, 수정하기 전에는 메서드에서 예외를 처리하지 않고, throws IOExcetpion을 했다. 하지만 IOException은 checked exception이기 때문에 이렇게 try-catch를 이용하는 방식으로 발생장소에서 바로 예외 처리를 햊는 것이 좋다.

예외 처리

예외를 처리하는 방법에는 예외 복구, 예외 처리 회피, 예외 전환 방법이 있다.

예외 복구

  • 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 방법

  • 예외를 잡아서 일정 시간, 조건만큼 대기하고 다시 재시도를 반복한다.

  • 최대 재시도 횟수를 넘기게 되는 경우 예외를 발생시킨다.

final int MAX_RETRY = 100;
public Object someMethod() {
    int maxRetry = MAX_RETRY;
    while(maxRetry > 0) {
        try {
            ...
        } catch(SomeException e) {
            // 로그 출력. 정해진 시간만큼 대기한다.
        } finally {
            // 리소스 반납 및 정리 작업
        }
    }
    // 최대 재시도 횟수를 넘기면 직접 예외를 발생시킨다.
    throw new RetryFailedException();
}

예외 처리 회피

  • 예외 처리를 직접 담당하지 않고 호출한 쪽으로 던져 회피하는 방법

  • 그래도 예외 처리의 필요성이 있다면 어느 정도는 처리하고 던지는 것이 좋다.

  • 긴밀하게 역할을 분담하고 있는 관가 아니라면 예외를 그냥 던지는 것은 무책임하다.

// 예시 1
public void add() throws SQLException {
    // ...생략
}

// 예시 2 
public void add() throws SQLException {
    try {
        // ... 생략
    } catch(SQLException e) {
        // 로그를 출력하고 다시 날린다!
        throw e;
    }
}

예외 전환

  • 예외 회피와 비슷하게 메서드 밖으로 예외를 던지지만, 그냥 던지지 않고 적절한 예외로 전환해서 넘기는 방법

  • 조금 더 명확한 의미로 전달되기 위해 적합한 의미를 가진 예외로 변경한다.

  • 예외 처리를 단순하게 만들기 위해 포장(wrap) 할 수도 있다.

// 조금 더 명확한 예외로 던진다.
public void add(User user) throws DuplicateUserIdException, SQLException {
    try {
        // ...생략
    } catch(SQLException e) {
        if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
            throw DuplicateUserIdException();
        }
        else throw e;
    }
}

// 예외를 단순하게 포장한다.
public void someMethod() {
    try {
        // ...생략
    }
    catch(NamingException ne) {
        throw new EJBException(ne);
        }
    catch(SQLException se) {
        throw new EJBException(se);
        }
    catch(RemoteException re) {
        throw new EJBException(re);
        }
}

읽어볼 것
https://madplay.github.io/post/effectivejava-chapter10-exceptions

참고: https://madplay.github.io/post/java-checked-unchecked-exceptions

throws

throws를 메서드에 선언해놓으면 try-catch로 묶어주지 않아도 그 메소드를 호출한 메서드로 예외 처리를 위임하는 것이기 때문에 전혀 문제가 되지 않는다.

public void throwsException(int number) throws Exception {
    if (number > 12) {
        throw new Exception("Number is over than 12");
    }
    System.out.println("Number is " + number);
}

0개의 댓글