Exception Handling

겔로그·2023년 1월 15일
0

Java

목록 보기
10/10

개요

어플리케이션의 장애로 인해 생기는 유저의 불편함은 크며, 장애가 지속될 경우 이는 곧 유저 이탈로 이어집니다. 따라서 서비스를 개발하고 운영하는 개발자로서 사전에 발생할 수 있는 장애를 차단하고 예외를 다루는 능력은 매우 중요하다고 생각합니다.

이번 글에서는 Java의 예외에 대해 알아보고, Spring Boot에서는 예외를 어떻게 핸들링하는지에 대해 구체적으로 알아보는 시간을 가져보겠습니다.

예외(Exception)란?

예외란, 어플리케이션이 정상적인 실행 과정에서 방해받을 수 있는 이벤트들을 의미합니다.

Java Exceptions

Java에서는 계층 구조로 예외를 다루고 있습니다. 분류 기준은 다음과 같습니다.

  • Error: 어플리케이션이 시도하지 않을만한 심각한 문제들
  • Exceptions: 어플리케이션이 잡아야 하는 조건(예외)들
    • Checked Exceptions: 컴파일 과정에서 명시적으로 확인할 수 있는 예외
    • Unchecked Exceptions: 어플리케이션 실행 도중에만 확인할 수 있는 예외

Java의 Exception 계층 구조

Java에서는 다양한 예외를 다루고 있습니다. 아래의 그림을 통해 Java에서 처리하는 모든 예외에 대한 계층 구조를 확인하실 수 있습니다.

Exception Handling - Java

일반적으로는 try-catch-finally 혹은 throw를 이용한 예외 처리를 진행하고 있습니다.

try-catch-finally

예외 처리의 대표적인 예시로는 try-catch-finally 구문이 있습니다. 각 구문은 다음과 같은 의미를 지니고 있습니다.

  • try: 시도하는 로직, 예외가 발생할 경우 catch block에서 처리
  • catch: 매개변수로 특정한 타입의 에러 변수를 가진다.
    - 예외가 발생할 경우 해당 예외가 발생한 에러의 super type인지를 확인한다.
    • super type일 경우 catch 문에 작성된 code가 실행된다.
    • 아닐 경우, 다음 catch문 혹은 로직 처리
  • finally: try, catch문을 모두 실행하고 최종적으로 동작되는 코드
try { 
	//code (로직 처리)
    
} catch(Exception e) {
	// code (위에 호출한 Exception 발생시 실행)
    
} finally {
	// code (try, catch 문이 끝난 이후 무조건 실행)
    
}

throw, throws

메서드에서 Exception이 발생할 경우 해당 메서드는 Exception Object를 생성하고 JVM에 넘깁니다. 이러한 과정을 Exception을 던진다(throwing an Exception)라 표현합니다. 기본적으로 Exception Object에는 다음과 같은 정보들이 포함됩니다.

  • Exception 이름과 설명
  • 프로그램의 현재 사태
  • Exception 발생 위치
  • exception 발생시 메소드 호출 스택(call stack)

앞서보신 try-catch-finally 구문을 통해 예외를 어떻게 처리할지 다룰 수 있었다면 이번에 볼 throw, throws예외를 어디에서, 어떻게 JVM에게 던질지 정의할 수 있는 방법입니다.

throw

throw는 개발 도중 예외처리시 JVM에 던질 Exception Object를 정의할 수 있는 방법입니다. 사용자는 throw를 통해 원하는 시점에 원하는 예외를 처리할 수 있습니다.

public class main {
    public static void main(String[] args) {
        RuntimeTest test = new RuntimeTest();
        test.testThrowRuntimeException();
    }
}


public class RuntimeTest {
    public void testThrowRuntimeException() {
        exceptionThrow();
    }
    
    public void exceptionThrow() {
        throw new RuntimeException("Exception thrown");
    }
}

호출 구조 및 결과

throws

throwsthrow와 달리 해당 예외를 이 곳에서 처리하지 않고 던지겠다의 의미를 가지고 있습니다. 위의 throw 예시와 유사한 구조로 진행하겠습니다.

다음과 같이 throws IOException을 추가할 경우 해당 메소드를 호출한 testThrowIOException에서 오류가 발생하는 것을 확인하실 수 있습니다.

exceptionThrow()에서 IOException에 대해 처리하지 않고 던져 testThrowIOException에서 해당 예외에 대해 처리해야 됨을 알 수 있습니다.

따라서 해당 메소드를 호출하기 위해서는 try-catch-finally 구문 또는 throws를 통해 상위 호출 메소드에 역할을 전가시킬수도 있습니다.

사용자 정의 exception

위에서 정의한 Exception이 아닌 사용자가 직접 Exception에 대해 정의하는 것을 의미합니다. Custom Exception을 사용할 경우 상황에 알맞게 보다 더 유연한 예외처리가 가능해집니다.

사용자 정의 exception은 예외를 상속하여 구현을 진행합니다.

public class CustomException extends RuntimeException {
    public CustomException() {
        super("Yummy~");
    }
}

Exception Handling - Spring

앞의 내용을 통해 Java의 Exception handling의 여러 케이스를 확인해 보셨습니다. try-catch-finally, throw/throws, custom exception 을 통해 예외 처리를 진행하였고 이를 통해 사용자는 의도에 알맞는 예외를 별도로 정의할수도, JVM에 던질수도, 임의로 처리할 수 있게 되었습니다.

하지만 서비스를 개발하면서 다양한 에러에 대한 처리를 진행하다보면 다음과 같은 에로사항을 경험할 수 잇습니다.

  • 매번 try-catch-finally 문을 사용한다.
    => 코드의 로직이 너무 길어집니다.
  • 에러 발생시 throw 한다.
    => 일부 Exception은 상관없으나,일반적으로는 Exception handling을 별도로 진행해야 합니다.
  • 상위 호출자에 예외를 throws 한다.
    => 예외처리된 메소드를 호출하는 모든 메소드에도 throws를 진행하여 최상위 호출자까지 가야합니다.

개발 과정에서 발생하는 이러한 문제들을 해결하기 위해 Spirng에서는 어노테이션(@ControllerAdvice,@RestControllerAdvice ,@ExceptionHandler)을 통한 예외 처리를 진행할 수 있습니다.

@ControllerAdvice, @RestControllerAdvice , @ExceptionHandler

@ExceptionHandler 는 @Controller , @RestController 가 적용된 Bean 에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능입니다.

@Controller , @RestController 가 적용되는 api, web에서는 Java의 @ExceptionHandler와 함께 이용할 경우 지정한 Exception에 대해 손쉽게 예외처리를 진행할 수 있습니다.

@ExceptionHandler

@ExceptionHandler 에 설정한 예외가 발생하면 handler가 실행됩니다. @ExceptionHandler는 설정된 컨트롤러에서만 동작하며 다른 컨트롤러에는 영향을 주지 않습니다.

@Controller
public class SimpleController {

    // ...

    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}

@ControllerAdvice

@ControllerAdvice는 @Controller가 정의된 모든 컨트롤러의 예외를 잡을 수 있도록 설정해주는 어노테이션 입니다. @ControllerAdvice를 이용할 경우 모든 컨트롤러의 예외 처리를 한 곳에서 관리할 수 있는 이점을 가질 수 있습니다.

@ControllerAdvice
public class ControllerAdvice {

    @ExceptionHandler(DataAccessException.class)
    public ResponseEntity<String> dataExceptionHandle() {
        return ResponseEntity.badRequest().build();
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> exceptionHandle() {
        return ResponseEntity.status(INTERNAL_SERVER_ERROR).build();
    }

}

@RestControllerAdvice

@ControllerAdvice + @ResponseBody → @RestControllerAdvice

@ControllerAdvice와 동일한 기능을 하며 추가로 응답의 body에 객체를 넣어 반환이 가능하다는 이점이 존재합니다. @RestController에서 발생하든 @Controller 에서 발생하든 @RestControllerAdvice에서는 전부 잡을 수 있습니다.

결론

서비스간 예외 처리에 있어 통일성을 가지기 위해서는 기존 서비스에서 사용되는 예외 처리 기법들에 대한 기본적인 분석이 필요합니다.

Java와 Spring에서 제공하는 예외 처리 기능을 결합해 사용할 경우 코드의 간결성어플리케이션의 예외 처리 두 마리 토끼를 잡을 수 있을 것이라 생각됩니다.

Java에서 제공하는 예외 처리 기능

  • try-catch-finally
  • throw-throws
  • Custom Exception

Spring에서 제공하는 예외처리 기능

  • @ControllerAdvice
  • @RestControllerAdvice
  • @ExceptionHandler

Java에서 제공하는 예외 종류 및 처리 기능에 대해 다시 한 번 상기해보시면서 글을 마무리하면 좋을 것 같습니다.

감사합니다.

Reference

https://rollbar.com/blog/java-exceptions-hierarchy-explained/
https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc
https://www.baeldung.com/exception-handling-for-rest-with-spring

profile
Gelog 나쁜 것만 드려요~

0개의 댓글