예외처리는 프로그램 실행 시 발생할 수 있는 예외에 대비하여 비정상적인 종료를 막고 실행 상태를 유지하는 것이다.
에러(error)
는 비정상적인 상황이 생겼을 때 발생한다. 개발자는 오류를 미리 예측하여 처리할 수 없기 때문에 오류에 대한 처리를 신경 쓰지 않는다.
하지만, 예외(exception)
는 오류와 반대로 비정상적인 상황을 예측할 수 있기 때문에 처리할 수 있다. 자신이 구현한 로직에서 예외를 예측하고 처리할 수 있어야 한다.
예외를 처리하는 방법은 크게 예외 복구, 예외 처리 회피, 예외 전환으로 나눌 수 있다.
예외 복구는 예외가 발생했을 때 이를 처리하고 프로그램의 정상적인 흐름을 유지하는 방법이다. 주로 try-catch
블록을 사용하여 예외를 잡고 복구할 수 있다.
try {
// 에러가 발생할 수 있는 코드
int result = divide(10, 0);
} catch (ArithmeticException e) {
// 에러 발생 시 수행
System.out.println("0으로 나눌 수 없습니다. 기본값으로 설정합니다.");
int result = 0;
} finally {
// Exception 발생 유무에 상관없이 무조건 수행
}
try
블록에서는 예외가 발생할 수 있는 코드를 작성하고, catch
블록에서 해당 예외를 처리한다. finally
블록은 예외 발생 여부와 상관없이 항상 실행된다.
예외 처리 회피는 메서드가 예외를 처리하지 않고, 이를 호출한 상위 메서드로 예외를 전달하는 방식이다. 메서드 선언부에 throws
키워드를 사용하여 특정 예외를 던질 수 있음을 명시한다. 이를 통해 예외를 직접 처리하지 않고 상위 호출자에게 맡길 수 있다.
public void myMethod() throws IOException {
// 예외 발생 가능 코드
FileReader file = new FileReader("nonexistentfile.txt");
}
이 경우, myMethod 메서드에서 IOException이 발생하면 이를 처리하지 않고 호출한 메서드로 던진다. 호출한 메서드는 해당 예외를 처리해야 한다.
예외 전환은 발생한 예외를 잡아서 다른 종류의 예외로 바꾸어 던지는 방식이다. 이는 주로 Checked Exception을 Unchecked Exception으로 변환하거나, 더 의미 있는 예외로 바꾸어 던질 때 사용된다.
try {
// 예외가 발생할 수 있는 코드
someMethodThatThrowsIOException();
} catch (IOException e) {
// 예외 전환 코드
throw new CustomRuntimeException("IO 작업 중 문제가 발생했습니다.", e);
}
Checked Exception과 Unchecked Exception은 각각 다른 방식으로 처리된다.
Checked Exception:
Unchecked Exception:
Checked Exception은 코드에서 예측 가능한 예외 상황을 명확히 처리하도록 강제하는 반면, Unchecked Exception은 개발자의 주의에 맡기는 방식이다.
예외 처리는 성능에 영향을 미칠 수 있다. 특히 예외가 빈번하게 발생하는 경우, 예외를 생성하고 스택 트레이스를 캡처하는 작업이 비용이 많이 들기 때문이다. 예외로 인해 성능 부하가 발생하는 것을 줄이기 위해 몇 가지 방법을 사용할 수 있다.
스택 트레이스는 예외가 발생한 시점에서의 메서드 호출 스택을 나타낸다. 즉, 예외가 발생한 위치와 그 위치에 도달하기까지 호출된 메서드들의 순서를 보여준다. 스택 트레이스를 통해 개발자는 예외가 발생한 정확한 위치와 원인을 파악할 수 있다.
Java에서 예외가 발생하면 JVM은 해당 예외의 스택 트레이스를 캡처하여 예외 객체에 저장한다. 이 과정에서 현재 메서드 호출 스택의 모든 프레임(즉, 메서드 호출)을 추적하고 기록한다. 즉, 스택 트레이스를 캡처하는 작업은 비용이 많이 드는 작업이기 때문에 예외 발생 시 성능에 영향을 미칠 수 있다.
public class StackTraceExample {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void method1() {
method2();
}
public static void method2() {
method3();
}
public static void method3() {
throw new RuntimeException("예외 발생");
}
}
위의 코드가 있다면?
java.lang.RuntimeException: 예외 발생
at StackTraceExample.method3(StackTraceExample.java:18)
at StackTraceExample.method2(StackTraceExample.java:14)
at StackTraceExample.method1(StackTraceExample.java:10)
at StackTraceExample.main(StackTraceExample.java:6)
이런식으로 스택트레이스를 출력하게된다.
예외를 비정상적인 상황에만 사용하고, 정상적인 흐름 제어에는 사용하지 않는다. 예를 들어, 조건문을 통해 예외 상황을 미리 방지하는 것이 좋다.
if (object != null) {
object.doSomething();
} else {
// 예외 발생 대신 다른 처리
}
과유불급.... 필요 없는 예외 처리를 피하고, 예외를 꼭 필요한 곳에서만 처리한다. 이를 통해 예외 발생 빈도를 줄일 수 있다.
구체적인 의미를 지닌 커스텀 예외를 정의하여, 필요 시 예외 처리 로직을 단순화할 수 있다.
public class CustomException extends Exception {
public CustomException(String message) {
super(message);
}
}
예외 발생 시 로그를 남기지만, 너무 많은 로그를 남기지 않도록 주의한다. 과도한 로그는 I/O 비용을 증가시킨다.
try {
// 예외 발생 가능 코드
} catch (Exception e) {
logger.error("An error occurred", e); // 필요할 때만 상세 로그 출력
}
스프링 프레임워크에서는 @ExceptionHandler
, @ControllerAdvice
등을 사용하여 예외 처리를 중앙집중식으로 관리할 수 있다. 이를 통해 코드의 가독성과 유지보수성을 높이고, 성능 부하를 최소화할 수 있다.