Book/스프링부트 핵심가이드

스프링부트 예외 처리

블로그 주인장 2023. 11. 24.

예외처리

자바에서는 예외 처리를 try/catch/throw 구문을 활용하여 처리하는데,

스프링부트에서 적용할 수 있는 예외 처리 방식에 대해 알아보겠습니다.


예외 & 에러


예외(exception)

  • 프로그래밍에서 예외(exception)란 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황을 의미한다.
  • 예외는 개발자가 직접 처리할 수 있는 것이므로 미리 코드 설계를 통해 처리할 수 있다.

 

에러(error)

  • 주로 자바 가상머신에서 발생시키는 것으로서 예외와 달리 애플리케이션 코드에서 처리할 수 있는 것이 거의 없다.
  • 대표적으로 메모리 부족(OutOfMemory), 스택 오버플로(stackOverFlow) 등이 있다.
  • 발생 시점에서 처리하는 것이 아닌, 미리 애플리케이션의 코드를 살펴보면서 문제가 발생하지 않도록 예방해서 원천적으로 차단해야한다.

 

예외 클래스

  • 모든 예외 클래스는 Throwable 클래스를 상속받는다.
  • 흔하게 볼 수 있는 Exception 클래스는 크게 Checked Exception, Uncheck Exception 으로 구분할 수 있다.
  Checked Exception Unchecked Exception
처리 여부 반드시 예외 처리 필요 명시적 처리를 강제하지 않는다.
확인 컴파일 단계 실행 중 단계
대표적인 예외 클래스 IOException
SQLException
RuntimeException
NullPointException
IllegalArgumentException
IndexOutOfBoundException
SystemException
  1. Checked Exception
    • 컴파일 단계에서 확인 가능한 예외 상황이다.
    • 이러한 예외는 IDE에서 캐치해서 반드시 예외 처리를 할 수 있게 표시해준다.
  2. Unchecked Exception
    • 런타임 단계에서 확인되는 예외 상황을 나타낸다.
    • 문법상 문제는 없지만 프로그램이 동작하는 도중 예기치 않은 상황이 생겨 발생하는 예외를 의미한다.

 

예외 처리 방법


예외 복구

  • 대표적인 방법은 try/catch 구문이다.
  • 대체로 외부 라이브러리를 사용하는 경우에는 try 블록을 사용하라는 IDE 알람이 발생하지만 개발자가 직접 작성한 로직은 예외 상황을 예측해서 try 블록에 포함시켜야한다.
  • catch 블록을 통해, try 블록에서 발생하는 예외 상황을 처리하는 내용을 작성한다.
  • catch 블록은 여러 개 작성할 수 있고, 여러 개의 catch 블록을 순차적으로 거치면서 예외 유형에 매칭되는 블록을 찾아 예외 처리 동작 진행한다.
int a = 1;
String b = "b";

try {
   System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
   b = "2";
   System.out.println(a + Integer.parseInt(b));
}

 

예외 처리 회피

  • 예외가 발생한 시점에서 바로 처리하는 것이 아니라 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식이다.
  • throw 키워드를 사용하여 어떤 예외가 발생했는지 호출부에 내용을 전달할 수 있다.
int a = 1;
String b = "b";

try {
   System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
   throw new NumberFormatException("숫자가 아닙니다");
}

 

예외 전환

  • 어떤 예외가 발생했느냐에 따라 호출부로 예외 내용을 전달하면서 좀 더 적합한 예외 타입으로 전달할 필요가 있다.
  • 예외 처리를 좀 더 단순하게 하기 위해서 래핑(wrapping) 해야하는 경우도 있다.
  • 이런 경우에는 tray/catch 방식을 사용하면서 catch 블록에 throw 키워드를 사용하여 다른 예외 타입으로 전달하면 된다.

 

스프링부트 예외 처리 방식


  • 예외가 발생했을 때 클라이언트에 오류 메시지를 전달하려면 각 레이어에서 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야한다.
    1. @(Rest)ControllerAdvice 와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리
    2. @ExceptionHandler를 통해 특정 컨트롤럴의 예외를 처리
    3. @ControllerAdvice 대신에 @RestControllerAdvice를 사용하면 값을 JSON 형태로 반환 가능하다.

 

[예시] CustomExceptionHandler 클래스

@RestControllerAdvice
public class CustomExceptionHandler {
    private final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionHandler.class);

    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity<Map<String, String>> handlerException(RuntimeException e,
                                                                HttpServletRequest request) {
        HttpHeaders httpHeaders = new HttpHeaders();
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

        LOGGER.error("Advice 내 handleException 호출, {}, {}", request.getRequestURI(), e.getMessage());

        Map<String, String> map = new HashMap<>();
        map.put("error type", httpStatus.getReasonPhrase());
        map.put("code", "400");
        map.put("message", e.getMessage());

        return new ResponseEntity<>(map, httpHeaders, httpStatus);
    }
}
  • @(Rest)ControllerAdvice는 스프링에서 제공하는 어노테이션이다.
  • @(Rest)Controller 에서 발생하는 예외를 한 곳에서 관리하고 처리할 수 있게 하는 기능을 수행한다.
  • @RestControllerAdvice(basePackages = "com.springboot.valid-exception") 같이 별도 설정을 통해 예외를 관제하는 범위를 지정할 수 있다.
  • @ExceptionHandler의 어떤 예외 클래스를 처리할 지는 value 속성으로 등록하고, 배열의 형식으로도 전달 받을 수 있기에 여러 예외 클래스를 등록할 수도 있다.
  • handlerException( ) 메서드는 클라이언트에게 오류가 발생했다는 것을 알리는 응답메시지를 리턴한다.
  • 컨트롤러의 메서드에 다른 타입의 리턴이 설정되어 있어도 핸들러 메서드에서 별도의 리턴 타입을 지정할 수 있다.

 

[예시]ExceptionController 클래스

@RestController
@RequestMapping("/exception")
public class ExceptionController {

    @GetMapping
    public void getRuntimeException() {
        throw new RuntimeException("getRuntimeException 메서드 호출");
    }
}
  • getRuntimeException() 메서드는 컨트롤러로 요청이 들어오면 RuntimeException을 발생시킨다.
  • 컨트롤러에서 던진 예외는 @(Rest)ControllerAdvice가 선언되어 있는 핸들러 클래스에서 매핑된 예외 타입을 찾아 처리하게 된다.
  • 두 어노테이션은 별도 범위 설정이 없으면 전역 범위에서 예외를 처리하기에 특정 컨트롤러에서만 작동하는 @ExceptionHandler 메서드를 생성해서 처리할 수도 있다.

 

[예시]컨트롤러 클래스 내 Exception 생성

@RestController
@RequestMapping("/exception")
public class ExceptionController {
    private final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionHandler.class);

    @GetMapping
    public void getRuntimeException() {
        throw new RuntimeException("getRuntimeException 메서드 호출");
    }

    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity<Map<String, String>> handlerException(RuntimeException e,
                                                                HttpServletRequest request) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

        LOGGER.error("클래스 내 handleException 호출, {}, {}", request.getRequestURI(), e.getMessage());

        Map<String, String> map = new HashMap<>();
        map.put("error type", httpStatus.getReasonPhrase());
        map.put("code", "400");
        map.put("message", e.getMessage());

        return new ResponseEntity<>(map, httpHeaders, httpStatus);
    }
}
  • 컨트롤러 내에 @ExceptionHandler 어노테이션을 사용한 메서드를 선언하면 해당 클래스에 국한해서 예외 처리를 할 수 있다.

 

[예시] Exception 우선순위

  1. Exception 클래스보다 좀 더 구체적인 NullPointerException 클래스가 각각 선언되어 있는 경우 구체적인 클래스가 지정된 쪽이 우선순위를 갖게 된다.
  2. @(Rest)ControllerAdvice의 글로벌 예외 처리와 @(Rest)Controller 내의 컨트롤러 예외 처리에 동일한 타입의 예외 처리를 하게 되면 범위가 좁은 컨트롤러의 핸들러 메서드가 우선순위를 갖게 된다.
반응형

댓글