읽는데 약 5분
예외 처리
Throw and Throws
throw
keyword를 사용해 코드에서 직접 예외를 던질 수 있습니다. 문법을 보면
throw new Exception("Exception message");
와 같이 이루어져있습니다. 이때 Exception
는 Throwable
의 하위 클래스여야합니다. 예외를 만들어서 던질 수 있으므로 사용자가 만든 예외를 던질 수 있다는 장점이 있습니다.
custom exception을 언제 사용해야할까?
일단 표준 예외를 사용할 시 어떠한 장점을 얻을 수 있는지 부터 살펴보겠습니다.
1. 표준 예외를 사용할 경우 가독성이 높아진다.
부적절한 인자가 들어오는 경우 사용하는 IllegalArgumentException
, 일을 수행하기에 적합하지 않은 상태의 객체인 경우 던지는 IllegalStateException
, 요청받는 작업을 지원하지 않는 경우 던지는 예외인 UnsupportedOperationException
등, 오래 전부터 정의된 표준 예외들이 존재합니다. 협업하는 상황에서 이러한 예외들을 사용하면 쓰임에 대해 익숙하므로 능률이 올라갑니다.
2. 지나치게 많은 예외 클래스가 만들어지는 것을 방지할 수 있다.
사용자와 관련된 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);
}
}
만약 범위를 벗어난 접근을 하게 된다면 아래와 같은 예외 메세지를 확인할 수 있습니다.
메세지의 수정이 필요하다면 해당 예외 클래스만 수정하면 되므로 리팩토링이 간단해집니다.
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으로 구분할 수 있습니다.
Checked Exception의 경우 반드시 예외를 체크해야하는 경우 사용합니다. 즉, 예외 처리를 하지 않게 되면 컴파일 에러가 발생합니다.
Unchecked Exception는 명시적인 예외처리를 강제하지 않습니다. 위 소스코드에서 IOException
대신 IllegalArgumentException
을 사용하면 컴파일 에러가 발생하지 않습니다.
예외는 부정적인 것이므로 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
- https://tecoble.techcourse.co.kr/post/2020-08-17-custom-exception/
- https://ko.gadget-info.com/difference-between-error
- https://stackoverflow.com/questions/23650854/why-are-unchecked-exceptions-not-checked-by-java-compiler
- https://stackoverflow.com/questions/18491020/what-does-throws-do-and-how-is-it-helpful