개발머해니

[스프링] 예외처리 종류과 효과적인 사용 방법 본문

백엔드

[스프링] 예외처리 종류과 효과적인 사용 방법

왕행님 2023. 8. 29. 21:30
728x90
반응형

스프링 예외처리 종류

스프링에서 예외처리는 크게 3가지로 나눌 수 있습니다.

① @ExceptionHandler : Controller Level 
② @ControllerAdvice : Global Level 
③ try ~ catch문 : Method Level 


@ExceptionHandler : 컨트롤러단에서 처리

  • @ExceptionHandler는 Controller계층에서 발생하는 에러를 잡아서 메서드로 처리해주는 기능입니다.
  • Service, Repository에서 발생하는 에러는 제외합니다.
@RestController
@RequestMapping("/person")
public class PersonController {
    @GetMapping
    public String test(){
        return "test";
    }

    @GetMapping("/exception")
    public String exception1(){
        throw new NullPointerException();
    }

    @GetMapping("/exception2")
    public String exception2(){
        throw new ClassCastException();
    }

    @ExceptionHandler({NullPointerException.class, ClassCastException.class})
    public String handle(Exception ex){
        return "Exception Handle!!!";
    }
}

 

@ControllerAdvice : 전역에서 처리

  • @ControllerAdvice는 @Controller와 handler에서 발생하는 에러들을 모두 잡아줍니다.
  • @ControllerAdvice안에서 @ExceptionHandler를 사용하여 에러를 잡을 수 있습니다.
@RestControllerAdvice
public class ControllerSupport {

    @ExceptionHandler({NullPointerException.class, ClassCastException.class})
    public String handle(Exception ex) {
        return "Exception Handle!!!";
    }
}
@RestController
@RequestMapping("/school")
public class SchoolController {
    @GetMapping
    public String test(){
        return "test";
    }

    @GetMapping("/exception")
    public String exception1(){
        throw new NullPointerException();
    }

    @GetMapping("/exception2")
    public String exception2(){
        throw new ClassCastException();
    }

}
@RestController
@RequestMapping("/person")
public class PersonController {
    @GetMapping
    public String test(){
        return "test";
    }

    @GetMapping("/exception")
    public String exception1(){
        throw new NullPointerException();
    }

    @GetMapping("/exception2")
    public String exception2(){
        throw new ClassCastException();
    }

}

 

 

try ~ catch문 : 메서드 단위에서 처리

  • throw 키워드를  사용하여 프로그래머가 고의로 예외를 발생시킬수 있습니다.
  • try 문에서 Exception 예외가 발생할 경우 catch (Exception e) 로 빠져서 그 안의 실행문을 실행한다.
  • 마지막 finally블럭은 try-catch문과 함께 예외발생 여부과 관계없이 "항상. 무조건" 실행되어야할 코드를 적는다.
  • 예외 발생시 try → catch → finally 순으로,  발생 하지 않은 경우 try → finally 순으로 실행됩니다.  

 

스프링 Exception 전략

 아래 글에 효과적인 Exception 전략 내용이 인상적이라 중요한 핵심 내용 몇가지만 요약해보겠습니다. 

 

Spring Guide - Exception 전략 - Yun Blog | 기술 블로그

Spring Guide - Exception 전략 - Yun Blog | 기술 블로그

cheese10yun.github.io

 

통일된 Error Response 객체

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {

    private String message;
    private int status;
    private List<FieldError> errors;
    private String code;
    ...

    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public static class FieldError {
        private String field;
        private String value;
        private String reason;
        ...
    }
}

이처럼 통일된  POJO 객체로 Error Response를 관리하면, 리턴 타입을 ResponseEntity<ErrorResponse>으로 반환하여 무슨 데이터가 어떻게 있는지 명확하게 추론하기 쉽도록 구성할 수 있습니다.

@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    log.error("handleMethodArgumentNotValidException", e);
    final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
    return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

 

@ControllerAdvice로 모든 예외를 핸들링

스프링 및 라이브러리 등 자체적으로 발생하는 예외는 @ExceptionHandler 으로 추가해서 적절한 Error Response를 만들고 비즈니스 요구사항에 예외일 경우 BusinessException 으로 통일성 있게 처리하는 것이 좋습니다.

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     *  javax.validation.Valid or @Validated 으로 binding error 발생시 
     *  HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할경우 발생
     *  주로 @RequestBody, @RequestPart 어노테이션에서 발생
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /**
     * @ModelAttribut 으로 binding error 발생시 BindException 발생
     * ref https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-modelattrib-method-args
     */
    @ExceptionHandler(BindException.class)
    protected ResponseEntity<ErrorResponse> handleBindException(BindException e) {
        log.error("handleBindException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /**
     * enum type 일치하지 않아 binding 못할 경우 발생
     * 주로 @RequestParam enum으로 binding 못했을 경우 발생
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        log.error("handleMethodArgumentTypeMismatchException", e);
        final ErrorResponse response = ErrorResponse.of(e);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    /**
     * 지원하지 않은 HTTP method 호출 할 경우 발생
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        log.error("handleHttpRequestMethodNotSupportedException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED);
        return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
    }

    /**
     * Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생
     */
    @ExceptionHandler(AccessDeniedException.class)
    protected ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
        log.error("handleAccessDeniedException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.HANDLE_ACCESS_DENIED);
        return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.HANDLE_ACCESS_DENIED.getStatus()));
    }

    /**
     * 비즈니스 요규사항에 따른 Exception
     */
   @ExceptionHandler(BusinessException.class)
    protected ResponseEntity<ErrorResponse> handleBusinessException(final BusinessException e) {
        log.error("handleEntityNotFoundException", e);
        final ErrorCode errorCode = e.getErrorCode();
        final ErrorResponse response = ErrorResponse.of(errorCode);
        return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus()));
    }

    /**
     * 개발자가 직접 핸들링해서 다른 예외로 던지지 않으면 모두 이곳으로 모인다.
     */
    @ExceptionHandler(Exception.class)
    protected ResponseEntity<ErrorResponse> handleException(Exception e) {
        log.error("handleEntityNotFoundException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 

컨트롤러 예외 처리

컨트롤러에서 모든 요청에 대한 값 검증을 진행하고 이상이 없을 시에 서비스 레이어를 호출해야 합니다. 위에서도 언급했듯이 잘못된 값이 있으면 서비스 레이어에서 정상적인 작업을 진행하기 어렵습니다. 무엇보다 컨트롤러의 책임을 다하고 있지 않으면 그 책임은 자연스럽게 다른 레이어로 전해지게 되며 이렇게 넘겨받은 책임을 처리하는데 큰 비용과 유지보수 하기 어려워질 수밖에 없습니다.

@RestController
@RequestMapping("/members")
public class MemberApi {

    private final MemberSignUpService memberSignUpService;

    @PostMapping
    public MemberResponse create(@RequestBody @Valid final SignUpRequest dto) {
        final Member member = memberSignUpService.doSignUp(dto);
        return new MemberResponse(member);
    }
}

Reuqest Body @Valid 어노테이션으로 예외를 발생시킬 수 있습니다. 이 예외는 @ControllerAdvice에서 적절하게 핸들링 됩니다. 

public class SignUpRequest {
    @Valid private Email email;
    @Valid private Name name;
}

public class Name {
    @NotEmpty private String first;
    private String middle;
    @NotEmpty private String last;
}

public class Email {
    @javax.validation.constraints.Email
    private String value;
}

 

 

Try Catch 전략

 Checked Exception이 발생하면 더 구체적인 Unchecked Exception을 발생시키고 예외에 대한 메시지를 명확하게 전달하는 것이 효과적입니다.
 
Checked Exception과 Unchecked Exception : 

  Checked Exception Unchecked Exception
처리 여부 반드시 예외 처리 해야함 예외 처리 하지 않아도됨
트랜잭션 Rollback 여부 Rollback 안됨 Rollback 진행
대표 Exception IOException, SQLException NullPointerException, IllegalArgumentException

 

 

Spring Exception, 제대로 처리하기

안녕하세요. 오늘은 Spring에서의 Exception 처리에 대해 다루도록 하겠습니다. 본 내용은 토비의 스프링 3.1 을 학습하고 정리한 글입니다. 자세한 정리본은 깃허브를 참고해주세요. 문제의 예외 처

gngsn.tistory.com

 

 

728x90
반응형