기억의 저장소
예외처리(Exception) 본문
예외처리란
예외 처리(Exception handling)란 프로그램이 실행되는 도중에 발생하는 예기치 않은 상황 또는 오류를 처리하는 프로그래밍 기술입니다. (런타임 오류 runtime error 또는 예외 exception)이라고 불린다고합니다.
예외 처리는 프로그램의 안정성과 신뢰성을 향상시키기 위해 사용됩니다. 예외 처리를 통해 오류가 발생했을 때 프로그램이 강제로 종료되는 것을 방지하고, 오류를 처리하거나 적절한 조치를 취할 수 있도록 합니다.
일반적인 예외처리 단계
- 예외 발생 : 오류가 발생합니다. 예상치 못한상황이거나 미리 예측하여 처리하지 않은 상황입니다.
- 예외 객체생성 : 해당 예외를 표현하는 예외객체가 생성됩니다.
- 예외 처리 : 프로그래머는 예외 처리 메커니즘을 사용하여 예외를 처리합니다. 이는 예외가 발생했을 때, 프로그램이 오류 상태로 빠지지 않도록 하는 것을 의미합니다.
- 예외 핸들링: 예외 처리 메커니즘을 통해 프로그램은 예외를 적절하게 처리하고, 오류를 수정하거나 사용자에게 알리는 등 적절한 조치를 취합니다.
예외 종류
- 체크 예외(Checked Exception)
- 체크 예외는 컴파일러에 의해 강제적으로 예외 처리를 요구하는 예외입니다.
- 프로그래머가 명시적으로 해당 예외를 처리하거나, 상위 호출자로 전파하여 예외를 처리하도록 요구합니다.
- IOException, FileNotFoundException 등이 체크 예외의 예시입니다.
- 언체크 예외(Unchecked Exception)
- 언체크 예외는 컴파일러가 예외 처리를 강제하지 않는 예외로, 명시적인 예외 처리가 필요하지 않습니다.
- 런타임 시 발생하는 예외로, 주로 프로그램의 오류나 버그에 의해 발생하는 경우가 많습니다.
- 주로 프로그래머의 실수로 발생하는 NullPointerExcetpion, ArrayIndexOutOfBoundsException 등이 언체크 예외의 예시입니다.(흔히 개발자가 코드를 잘못 작성해서 발생하는 이런 오류들은 모두 RuntimeException 을 상속한 예외들입니다.)
- 언체크 예외는 RuntimeException 클래스의 하위 클래스들이 대부분이며, try-catch 블록으로 처리하지 않아도 됩니다.
API에서 예외 처리
API 예외는 같은 예외라도 컨트롤러에 따라 정말 다양하게 응답을 내려줘야 될 수도 있는데 스프링에서는 다양한 상황에서 유연하게 API 예외를 처리할 수 있습니다.
@ExceptionHandler를 이용한 예외 처리
스프링에서 익셉션이 터졌다면 HandlerExceptionResolver라는것이 예외를 잡아 정상적인 흐름으로 만들어줍니다.
만약 정상적인 흐름이 아니라면 WAS까지 전파되었다가 다시 컨트롤러로 오게 되지만 HandlerExceptionResolver는 그런 불필요한 흐름을 제거해 줍니다 .(컨트롤러에서 예외가 던져지면 HandlerExceptionResolver가 잡습니다)
@ExceptionHandler는 HandlerExceptionResolver를 손쉽게 사용할 수 있게 해주는 어노테이션이라 생각하면됩니다.
기본적인 사용방법은 아래와 같습니다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e) {
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler안에 파라미터를 넣어주지 않으면 메서드 파라미터에 해당하는 예외 혹은 그 자식 예외 발생시 해당 메소드가 호출됩니다. 또한 @ExceptionHandler({Exception1.class, Exception2.class}) 여러 예외도 지정할 수 있습니다.
메서드의 응답 타입에는 위와 같이 ResponseEntity , 개발자가 정한 타입등이 올 수 있습니다.
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
클라이언트에게 간단하게 전달해 줄 수 있을때는 위와 같이 정의해도 되지만 만약 회원가입시 아이디도 예외이고 비밀번호도 예외인 상황에서 상세하게 예외 메시지를 보내주기 위해서는 다르게 변경해야합니다.
@RestControllerAdvice
위 어노테이션을 사용하기전에는 @ExceptionHandler를 컨트롤러부분에 놨두어 예외처리를 하였습니다.
API에대한 코드와 섞인다는 단점이 있었는데 @RestControllerAdvice를 사용하면 해당 단점을 없앨 수 있습니다.
@RestControllerAdvice는 @ControllerAdvice와 동일하지만 @ResponseBody가 붙은것을 말합니다.
@ControllerAdvice(annotation = RestController.class)
@ControllerAdvice("org.example.controllers")
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice1 {}
RestController에서 예외가 발생한경우, 해당 패키지에서 발생한경우 ,해당 클래스에서 발생한경우등을 지정할 수 있으며 만약 파라미터에 아무것도 적지않아 대상을 지정하지않은경우 모든 예외를 처리하게됩니다.
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e) {
ErrorResult errorResult = new ErrorResult("BAD", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
}
https://dev-aiden.com/spring/spring-api-exception-handling/
BusinessException
API요청에 따라 개발자가 직접 예외(Exception)를 만들어야 할 경우도 있습니다.
Java나 Spring에서 수많은 RuntimeException 을 지원해주지만 언체크 RuntimeException을 이용해서 개발자가 원하는 예외처리를 만들기도 합니다.
개발자가 의도적으로 예외를 던져야 되는 경우는 어떤 것들이 있을까요?
- 백엔드에서 외부와 연동하여 서비스를 제공하는데 외부 서비스가 먹통인경우
- 시스템 내부에서 조회하려는 리소스가 없는경우
위와 같은 경우 의도한 정상적인 흐름이 아니니 의도적으로 예외를 발생시켜 어떤 상황인지 클라이언트에게 알려줘야합니다.
그렇다면 어떻게 하면 예외를 의도적으로 던질 수 있을까요?
Java에서는 throw 키워드를 사용해서 예외를 메서드 바깥으로 던질 수 있습니다. 던져진 예외는 메서드 바깥 즉, 메서드를 호출한 지점으로 던져지게 되는 것입니다. 호출한지점에서도 처리가 되지 않는다면 그 밖으로 던져 @ExceptionHandler와 같은 방식으로 예외를 처리해주게 됩니다.
예외상황은 정말 다양할텐데 어떤 예외를 던져야할까요?
이럴때 사용자 정의 예외(Custom Exception을 사용할 수 있습니다.
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member Not Found");
@Getter
private int status;
@Getter
private String message;
ExceptionCode(int status, String message) {
this.status = status;
this.message = message;
}
}
먼저 서비스 계층에서 던질 Custom Exception에 사용할 ExceptionCode를 enum으로 정의합니다. 이처럼 ExceptionCode를 enum으로 정의하면 비즈니스 로직에서 발생하는 다양한 유형의 예외를 enum에 추가해서 사용할 수 있습니다
public class BusinessLogicException extends RuntimeException {
@Getter
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
BusinessLogicException 은 RuntimeException 을 상속하고 있으며 ExceptionCode 를 멤버 변수로 지정하여 생성자를 통해서 조금 더 구체적인 예외 정보들을 제공해줄 수 있습니다.
BusinessLogicException 은 서비스 계층에서 개발자가 의도적으로 예외를 던져야 하는 다양한 상황에서 ExceptionCode 정보만 바꿔가며 던질 수 있습니다.
이제 서비스 계층에서 던진 BusinessLogicException을 Exception Advice에서 처리하면 됩니다.
@ExceptionHandler
public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
System.out.println(e.getExceptionCode().getStatus());
System.out.println(e.getMessage());
return new ResponseEntity<>(HttpStatus.valueOf(e.getExceptionCode().getStatus()));
}
부록
1. 예외처리 발전 과정
예전 SSR시절에는 알맞은 예외 페이지를 보여줘야하니 response.SendError을 통해 알맞은 페이지를 보내주었다.
was에서 다시 컨트롤러로 타고내려와 알맞은 페이지를 보여줘야하니 컨트롤러도 생성해야했고 복잡했지만 스프링이 그런 그런 복잡함을 줄여주었다. 그렇게 SSR에서는 예외처리를 하게되었다
그러다가 SSR에서 CSR로 바뀌게되면서 API마다 복잡한 응답을 내려주어야하니 기존에 스프링이 제공하던 BasciErrorController로 하기에는 복잡해지게 되었다
그래서 스프링에서는 HandlerExceptionResolver라는것을 제공하여 처리를 해주게 되었는데 이것도 복잡하다 하여 점점 발전되다가 지금의 @ExceptionHandler가 나오게 되었다.
API 예외 처리를 위해 스프링을 사용하기전에는 WAS까지 전파되었다가 다시 컨트롤러로 타고 내려와서 응답을 보내주어야했습니다.(이게 원래 동작이었음)
하지만 스프링이 적용되고 난 후 익셉션이 터졌다면 WAS로 보내는것이 아니라 HandlerExceptionResolver라는 것이 잡아 처리 한후 정상적인 응답으로 나가게 해주며 다시 WAS 부터 컨트롤러로오는 불필요한 요청을 없애주게 되었습니다.
하지만 HandlerExceptionResolver를 직접 사용하기에는 복잡한데 스프링은 이런 복잡하고 번거로움을 해결해주는 @ExceptionHandler라는 에외 처리 기능을 제공해줍니다.
참고