Throw and Throws

throw keyword를 사용해 코드에서 직접 예외를 던질 수 있습니다. 문법을 보면

throw new Exception("Exception message");

와 같이 이루어져있습니다. 이때 ExceptionThrowable의 하위 클래스여야합니다. 예외를 만들어서 던질 수 있으므로 사용자가 만든 예외를 던질 수 있다는 장점이 있습니다.


custom exception을 언제 사용해야할까?

일단 표준 예외를 사용할 시 어떠한 장점을 얻을 수 있는지 부터 살펴보겠습니다.

1. 표준 예외를 사용할 경우 가독성이 높아진다.

부적절한 인자가 들어오는 경우 사용하는 IllegalArgumentException, 일을 수행하기에 적합하지 않은 상태의 객체인 경우 던지는 IllegalStateException, 요청받는 작업을 지원하지 않는 경우 던지는 예외인 UnsupportedOperationException 등, 오래 전부터 정의된 표준 예외들이 존재합니다. 협업하는 상황에서 이러한 예외들을 사용하면 쓰임에 대해 익숙하므로 능률이 올라갑니다.

2. 지나치게 많은 예외 클래스가 만들어지는 것을 방지할 수 있다.

image

사용자와 관련된 Custom Exception입니다. 이외에도 여러가지 문제가 발생할 때마다 클래스를 만들면 메모리 문제도 발생할 수 있고 클래스 로딩에도 더 많은 시간이 소요될 것입니다.

그렇다면 사용자 정의 예외를 사용하는 것은 잘못된 걸까요? 분명히 사용자 정의 예외를 사용하는 것에도 장점이 있습니다.

1. 상세한 예외 정보를 제공할 수 있다.

만약 리스트의 범위를 벗어난 요청이 있다면 표준 예외는 IndexOutOfBoundsException이나 IllegalArgumentException을 통해서 처리할 수 있습니다.

구체적으로 벗어난 범위를 파악하기 위해 소스코드마다 리스트의 크기 및 접근한 인덱스를 출력할 수 있지만 리스트가 소스코드의 여러군데에서 사용된다면 사용될 때마다 해당 메세지를 작성해야하므로 리팩토링이 어려워 진다는 단점이 있습니다.

이렇게 사용하는 대신 사용자 예외를 추가해 사용할 수 있습니다.

public class IllegalIndexException extends IndexOutOfBoundsException {
    private static final String message = "범위를 벗어났습니다.";

    public IllegalIndexException(List<?> target, int index) {
        super(message + " size: "  + target.size() + " index: " + index);
    }
}

만약 범위를 벗어난 접근을 하게 된다면 아래와 같은 예외 메세지를 확인할 수 있습니다.

image

메세지의 수정이 필요하다면 해당 예외 클래스만 수정하면 되므로 리팩토링이 간단해집니다.

2. 예외 발생 위치를 명확히 할 수 있습니다.

재사용성이 높은 것은 표준 예외의 장점이지만 어느 곳에서 해당 예외가 발생했는지 정확히 파악하기 어렵다는 단점이 있습니다.

@Controller
public class SomeController {
    // ...
    @PostMapping("/some")
    public ResponseEntity<Void> Some(@RequestBody SomeRequest request) {
        Something something = someService.someMethod(request);
        if (somevalidate(something)) {
            throw new IllegalArgumentException();
        }

        A.doSomething();
        B.doSomething();

        return ...;
    }
    // ...
}

이 소스코드에서 IllegalArgumentException이 발생했다면 어디서 발생했을까요? if 문에서 발생했다고 장담할 수는 없습니다. IllegalArgumentException의 경우 여러 메서드에서 발생할 수 있기 때문입니다.

사용자 정의 예외를 사용한다면 CustomException.class을 구현한 뒤 @RestControllerAdvice를 선언한 클래스 내에서 예외 처리를 함으로써 원하는 소스코드에 대한 예외 처리를 명확히 할 수 있습니다.

상황에 따라서 적절한 것을 선택해 사용하면 됩니다.

Throws는 메서드 선언에서 사용합니다. 현재 메서드에서 발생하는 예외를 자신을 호출한 메서드에게 처리를 위임할 때 사용합니다.

public static void execute() throws SocketException, ConnectionException, Exception

현재 발생한 예외를 try-catch로 해결하는 대신 throws로 상위 메서드에서 넘겨야 하는 이유는 무엇일까요? execute를 사용하는 메서드가 여러 군데 일 때 발생하는 예외를 try-catch로만 처리한다면 모든 소스코드마다 아래와 같이 작성해야합니다.

try{
    execute();
    a.someMethod();
}catch(SocketException e){

}catch(ConnectionException e){

}catch(Exception e){

}

코드의 중복이 발생하여 리팩토링이 어렵게 됩니다. 개별적으로 low-level에서 처리하는 대신 throws를 사용한다면 상위 메서드에서 공통적으로 처리가능하므로 소스코드의 중복이 줄어들게 됩니다.

Exception Wrapping

다음으로는 throw와 함께 사용하면 예외의 추상화가 가능해집니다. 우리가 메서드를 만들어서 사용자에게 제공한다고 해봅시다.

class Manager {

    public void sendPerson() throws SQLException, SocketException, OutputStreamException, SerializationException{
        Person person = dao.readPerson();
        Socket socket = getSocket();
        OutputStream os = socket.getOutputStream();
        String personJson = objectMapper.writeValueAs(person);
        os.write(personJson);
    }
}

클라이언트는 sendPerson()을 사용하기 위해선 SQLException, SocketException 등 throws에서 던지는 구체적인 예외에 대해서 알고 있어야만 합니다.

try {
    manager.sendPerson();
} catch (SQLException e) {
    // ...
} catch (SocketException e){
    // ...
} catch (OutputStreamException e){
    // ...
} catch (SerializationException e){
    // ...
}

하지만 모든 예외에 대해 하나하나 세부적으로 파악하는 것은 좋지 못하므로 발생하는 예외를 공통적으로 묶어서 추상화 할 수 있습니다.

class Manager {

    public void sendPerson() throws ManagerException {
        try{
            Person person = dao.readPerson();
            Socket socket = getSocket();
            OutputStream os = socket.getOutputStream();
            String personJson = objectMapper.writeValueAs(person);
            os.write(personJson);
        } catch (SQLException | SocketException | OutputStreamException | SerializationException e) {
            throw new ManagerException("error text", e);
        }
    }
}

클라이언트는 추상화 된 ManangerException에만 집중해 처리할 수 있습니다.

try {
    manager.sendPerson();
} catch (ManagerException e) {
    // Handle fail by manager
}

Checked Exception vs Unchecked Exception

간단하게 구분하면 RuntimeException을 상속하면 Unchecked Exception, RuntimeException을 상속하지 않으면 Checked Exception으로 구분할 수 있습니다.

image

Checked Exception의 경우 반드시 예외를 체크해야하는 경우 사용합니다. 즉, 예외 처리를 하지 않게 되면 컴파일 에러가 발생합니다.

image

Unchecked Exception는 명시적인 예외처리를 강제하지 않습니다. 위 소스코드에서 IOException 대신 IllegalArgumentException을 사용하면 컴파일 에러가 발생하지 않습니다.

image

예외는 부정적인 것이므로 Unchecked Exception과 Checked Exception을 구분하는 대신 모든 예외를 처리하도록 강제한다면 어떨까요?

try {
    System.out.println();
} catch (NullPointerException e) {

} catch (SecurityException e) {

} catch (InterruptedException e) {

} catch (ArrayIndexOutOfBoundsException e) {

} catch (StringIndexOutOfBoundsException e) {

} catch (IndexOutOfBoundsException e) {

}

간단한 출력문을 작성하더라도 try-catch를 사용해야하므로 소스코드가 번잡해지는 문제가 있습니다. 이 때문에 자바에서는 두 예외를 구분해서 사용하고 있습니다.


finally

finally block은 JVM이 종료되지 않는 한 예외 발생여부와 상관없이 항상 실행되는 블록입니다. 이 때문에 주로 resource를 clean-up하는 용도로 많이 사용합니다.

만약 FileWriter를 사용하는 코드에서 발생하는 예외의 종류에 따라 resource를 close할지 여부를 finally내에서 결정할 수 있습니다.

finally {
    if (out != null) {
        System.out.println("Closing PrintWriter");
        out.close();
    } else {
        System.out.println("PrintWriter not open");
    }
    if (f != null) {
	    System.out.println("Closing FileWriter");
	    f.close();
	}
}

위 소스코드의 번잡함을 줄이기 위해 try-with-resource 문법이 등장했습니다. 닫는 객체가 AutoCloseable을 구현했다면 사용가능합니다.


Error vs Exception

Error는 시스템에 비정상적인 상황이 발생했을 경우를 의미합니다. 주로 자바 VM에서 발생시키는 것이고 애플리케이션에서 잡아서는 안됩니다.

  • OutOfMemoryError
  • StackOverflowError
  • VirtualMachineError

반면에 예외란 입력 값에 대한 처리가 불가능하거나 프로그램 실행 중에 참조된 값이 잘못된 경우 등 정상적인 프로그램의 흐름을 벗어나는 일을 의미합니다. 개발자가 예외에 대해 미리 예측하여 처리할 수 있어 상황을 핸들링 할 수 있습니다.


Create Custom Exception

Checked Exception을 생성하고 싶다면 Exception을 상속하고 Unchecked Exception을 생성하고 싶다면 RuntimeException을 상속하면 됩니다. 다음은 읽고자 하는 파일명이 잘못되었을 때 던질 수 있는 예외 클래스를 정의한 것입니다.

public class IncorrectFileNameException extends Exception {
    public IncorrectFileNameException(String errorMessage) {
        super(errorMessage);
    }
}

상위 클래스의 생성자를 호출하여 에러 메세지를 남길 수 있습니다. 예외 생성시 아래처럼 작성하게 됩니다.

catch (FileNotFoundException e) {
    if (!isCorrectFileName(fileName)) {
        throw new IncorrectFileNameException("Incorrect filename : " + fileName );
    }
    //...
}

현재 예외에서는 인자 e에 대한 정보를 활용하지 않으므로 이 정보까지 예외에 포함하기 위해서는 Throwable 인자를 추가하면 됩니다.

public class IncorrectFileNameException extends Exception {
    public IncorrectFileNameException(String errorMessage) {
        super(errorMessage);
    }
    public IncorrectFileNameException(String errorMessage, Throwable err) {
        super(errorMessage, err);
    }
}

Reference