Spring/Spring

Spring] 예외처리 (@ExceptionHandler , @ControllerAdvice,@RestControllerAdvice), 에러코드,실제 적용 [미완]

backend dev 2023. 1. 24.

@ExceptionHandler

@ExceptionHandler는 @Controller, @RestController가 적용된 Bean내에서 발생하는 예외를 잡아서 

하나의 메서드에서 처리해주는 기능을 한다.

 

 @ExceptionHandler라는 어노테이션을 쓰고 인자로 캐치하고 싶은 예외클래스를 등록해주면 끝난다.

모든 예외를 캐치하고싶다면 Exception.class로

@ExceptionHandler(NullPointerException.class)
public Object myExceptionHandler() {
    log.info("핸들러가 실행되었습니다.");
    System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    return "컨트롤러안에서 예외가 발생하였을때 동작!";
}

→ @ExceptionHandler({ Exception1.class, Exception2.class}) 이런식으로 두 개 이상 등록도 가능하다.

위의 예제에서 처럼하면 MyRestController에 해당하는 Bean에서 NullPointerException이 발생한다면, @ExceptionHandler(NullPointerException.class)가 적용된 메서드가 호출될 것이다.

 

1. Controller,RestController에만 작동한다 ( @service같은 빈에서는 안된다.)

2. 리턴타입은 자유롭게 해도 된다. 

3. @ExceptionHandler를 등록한 Controller에서만 동작한다.

4. @ExceptionHandler가 적용된 메서드의 파라미터를 자유롭게 받아와도 된다. (Exception같이)

@ExceptionHandler(Exception.class)
public String myExceptionHandler(Exception e, HttpServletRequest request) {
    log.info("exception message : {} , httpservletRequest : {}",e.getMessage(),request.toString());
    return "컨트롤러안에서 예외가 발생하였을때 동작!";
}

5. controller에서 service 메소드를 실행하고, 해당 메소드가 예외를 발생시켜도 결국 컨트롤러내에서 발생한것과 같으므로 @ExceptionHandler가 예외처리를 해준다.

 

 

테스트해보기

@RestController
@Slf4j
public class ExceptionController {

    @GetMapping("/exception/param")
    public String exceptionTest(@RequestParam("username")String username) {
        log.info("username : {}", username);
        return "OK";
    }
    @GetMapping("/exception/hi")
    public String exceptionTest2() {
        log.info("get");
        int[] arr = new int[2];
        log.info("{}",arr[5]);
        return "OK";
    }

    @ExceptionHandler(RuntimeException.class)
    public Object myExceptionHandler() {
        log.info("핸들러가 실행되었습니다.");
        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        return "컨트롤러안에서 예외가 발생하였을때 동작!";
    }
}

exceptionTest2는 인덱스를 벗어난값을 조회하므로 인덱스exception이 발생할것이고,

해당 exception은 RuntimeException이니까 @ExceptionHandler로인해 예외가 감지되어 그 아래 메소드가 실행될것이다.

 

 

exceptionTest는 파라미터로 넘어온 username을 받아 로그로 찍어보는 테스트이다.

하지만 @ExceptionHandler가 RuntimeException.class로 설정되어있으면 예외를 감지못하고, 

Exception.class로 되어있어야 감지가 되어 @ExceptionHandler가 잘 동작한다. (MissingServletRequestParameterException는 RuntimeException이 아니기때문)

@ExceptionHandler(Exception.class)
public String myExceptionHandler() {
    log.info("핸들러가 실행되었습니다.");
    System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    return "컨트롤러안에서 예외가 발생하였을때 동작!";
}

이렇게 수정하니 잘 동작하는것을 확인할 수 있었다.

 

RuntimeException(unchecked exception)

 

[Java] 체크 예외(Check Exception)와 언체크 예외/런타임 예외 (Uncheck Exception, Runtime Exception)의 차이와

1. 체크 예외(Check Exception)와 언체크 예외/런타임 예외 (Uncheck Exception, Runtime Exception)의 차이 [ 예외(Exception)의 종류 ] 에러(Error) 예외(Exception) 체크 예외(Check Exception) 언체크 예외(Uncheck Exception) 에

mangkyu.tistory.com

 

[Java] Checked Exception vs Unchecked Exception 정리

체크 예외와 언체크 예외(Checked, Unchecked Exception) 자바의 예외는 크게 3가지로 나눌 수 있습니다. 체크 예외(Checked Exception) 에러(Error) 언체크 예외(Unchecked Exception) 자바에서 에러, 예외 관련된 클래

devlog-wjdrbs96.tistory.com

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

직접 예외를 던져줘서 테스트 가능.


@ControllerAdvice,@RestControllerAdvice

@ExceptionHandler가 하나의 클래스에 대한것이라면, @ControllerAdvice,@RestControllerAdvice는 모든 Controller

즉 전역에서 발생할 수 있는 예외를 캐치해 처리해주는 Annotation이다.

@RestControllerAdvice
@Slf4j
public class ExceptionHandleController {

    @ExceptionHandler(Exception.class)
    public void myTest() {
        log.info("모든 예외에 대해 처리할수도있다.");
    }

    @ExceptionHandler(NullPointerException.class)
    public void myTest2() {
        log.info("아니면 특정 예외에 대해 처리할수도있다.");
    }
}

이와 같이 클래스파일을 맏늘어 어노테이션을 붙여주기만 하면 된다. 

@RestControllerAdvice와 @ControllerAdvice가 존재하는데 @RestControllerAdvice안에는 @ResponseBody가 있다는 차이가 있다. @RestController와 @Controller의 차이처럼 @ControllerAdvice는 뷰리졸버를 통해 에러페이지로 리다이렉트하고 싶을때 쓰면되고 @RestControllerAdvice는 API서버여서 에러응답객체나 메시지를 리턴해야할때 사용하면된다.

(즉 예외발생시 응답처리를 어떻게할거냐에 따라 골라쓰면된다)

(예외 발생시 @ControllerAdvice, @RestControllerAdvice 클래스로 넘어와 응답처리를 해준다고 생각하자)

 

참고

@ControllerAdvice 를 쓰던 @RestControllerAdvice를 쓰던간에 @Controller,@RestController 에서 발생하는 예외를 캐치할수있다. "@RestControllerAdvice는 @RestController의 예외만 캐치한다"는 아니라는것

+ 하지만 @RestControllerAdvice(annotation = "RestController.class") 와 같이 적용되는 어노테이션을 설정할수도 있다.

 

+ 예외처리를 할 패키지,클래스 등 범위를 설정가능하다.

@RestControllerAdvice("hello.springmvc.basic")
@Slf4j
public class ExceptionHandleController {

범위가 넓게 지정된 설정은 자세한 설정보다 우선 순위가 낮다.

1. 어떤 컨트롤러안에 @ExceptionHandler가 설정되어있다면 @ControllerAdvice가 설정되어도 자신의 컨트롤러 안에 있는 예외처리메소드가 실행된다.

2. @ControllerAdvice클래스 안에 전체 예외에 대한 메소드와, 특정 예외 처리에 대한 메소드가 있을때 , 특정 예외가 발생한다면 특성 예외처리에 대한 메소드가 실행된다.


 

적용해보기

0.구조

BusinessException -> RuntimeException을 상속받으며, Validation 관련 예외를 처리할 커스텀예외

(비즈니스로직 예외처리용)

1. 에러코드 정의

@JsonFormat(shape = Shape.OBJECT) //직렬화할때 어떤 값의 모습으로 직렬화될지
@RequiredArgsConstructor // 필수(final)변수 파라미터로 가지는 생성자 생성
@Getter
public enum ErrorCode {// enum 클래스

    //Common -> http 요청시 발생할만한 예외
    INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "Common-001", " Invalid Input Value"),
    METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "Common-002", " Invalid Http Method"),
    ENTITY_NOT_FOUND(HttpStatus.BAD_REQUEST,"Common-003", " Entity Not Found"),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Common-004", "Server Error"),
    INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST,"Common-005", " Invalid Type Value"),
    HANDLE_ACCESS_DENIED(HttpStatus.FORBIDDEN, "Common-006", "Access is Denied"),

    //Member Validation
    EMAIL_DUPLICATION(HttpStatus.BAD_REQUEST, "Member-001", "Email is Duplication"),
    LOGIN_INPUT_INVALID(HttpStatus.BAD_REQUEST, "Member-002", "Login input is invalid"),

    //room Validation


    //...등
    ;

    private final HttpStatus status; // http 상태코드
    private final String code;//에러코드
    private final String message;//에러메시지

}

2. 에러응답 정의

생성자는 만들어도 접근제한자를 private로 두어 내부에서만 사용하게끔하고,

외부에서는 생성자대신 of라는 메소드를 만들어서 이용하게끔하는 정적 팩토리 메소드를 이용한다.

https://tecoble.techcourse.co.kr/post/2020-05-26-static-factory-method/

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) //파라미터가없는 생성자의 접근제한을 Protectd로 설정 (무엇때문인지는 공부가 필요)
public class ErrorResponse { //에러응답객체

    private String message; //에러메시지
    private int status; // http상태코드
    private String code; //에러코드
    private List<FieldError> erros; // 유효성검증에서 실패했을경우 발생하는 에러정보 리스트
    //생성자는 private해되야 정적 팩토리 메소드가 의미가있다.
    private ErrorResponse(final ErrorCode errorCode) { //에러코드객체만 들어왔다면 그 객체를 이용해서 에러응답객체를 만들어준다.
        this.message = errorCode.getMessage();
        this.status = errorCode.getStatus().value();
        this.code = errorCode.getCode();
        this.erros = new ArrayList<>(); // null값으로 응답할순없고, 빈 리스트를 넣어준다.
    }
    private ErrorResponse(final ErrorCode errorCode,final List<FieldError> errors) { //에러코드객체와, 필드에러리스트가 들어왔을때 응답객체 생성
        this.message = errorCode.getMessage();
        this.status = errorCode.getStatus().value();
        this.code = errorCode.getCode();
        this.erros = errors;
    }

    //위의 만들어둔 private 생성자를 통해,  정적 팩토리 메소드를 만들어 준다. (생성자 대신 생성을 담당할 메소드) , of라는 메소드를 오버로딩해서 생성
    public static ErrorResponse of(final ErrorCode errorCode) { // 에러코드만 받았을때
        return new ErrorResponse(errorCode);
    }

    public static ErrorResponse of(final ErrorCode errorCode, final List<FieldError> errors) { //에러코드와 필드에러 리스트를 받았을때
        return new ErrorResponse(errorCode, errors);
    }

    //에러코드와 BindingResult 객체가 들어왔을경우
    public static ErrorResponse of(final ErrorCode errorCode, final BindingResult bindingResult) {
        return new ErrorResponse(errorCode, FieldError.of(bindingResult));
    }

    public static ErrorResponse of(MethodArgumentTypeMismatchException e) {
        final String value = e.getValue() == null ? "" : e.getValue().toString(); //들어온 값
        final List<ErrorResponse.FieldError> errors = ErrorResponse.FieldError.of(e.getName(), value, e.getErrorCode()); //e.getName() => method이름
        return new ErrorResponse(ErrorCode.INVALID_TYPE_VALUE, errors);
    }



    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    private static class FieldError { //필드에서 발생한 유효성검사 실패에 대한 정보를 저장할 클래스
        private String field; // 클래스명.유효성검사 실패한 변수명이 저장될 변수가 저장된다 (ex) member.name
        private String value; // 들어온 값
        private String reason; // 유효성 검사 조건

        private FieldError(String field, String value, String reason) {
            this.field = field;
            this.value = value;
            this.reason = reason;
        }

        private static List<FieldError> of(final String field,final String value,final String reason) {
            List<FieldError> fieldErrors = new ArrayList<>();
            fieldErrors.add(new FieldError(field, value, reason));
            return fieldErrors;
        }

        private static List<FieldError> of(final BindingResult bindingResult) { //BindingResult는 검증오류가 발생할 경우 검증오류를 보관하는 객체(추가공부필요)
            final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors(); //bindingResult에 저장되는 springframeworkd에서 지원하는 fielderror 가져온다
            return fieldErrors.stream() //내가 만든 fielderror로 매핑
                .map(error -> new FieldError(
                    error.getField(),
                    error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
                    error.getDefaultMessage()))
                .collect(Collectors.toList());
        }
    }

}

 

3. 전역 예외처리 클래스 

@RestControllerAdvice
@Slf4j
public class ExceptionHandlerAdvice {

    /*
    @Valid 또는 @Validated로 binding error 발생시 발생하는 예외
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
        MethodArgumentNotValidException e) {
        log.warn("handleMethodArgumentNotValidException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE,
            e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
    }
    /*
    enum type이 일치하지 않아 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.valueOf(response.getStatus()));
    }
    /*
    지원하지 않은 Http Method방식으로 호출할 경우 발생
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
        HttpRequestMethodNotSupportedException e) {
        log.error("handleHttpRequestMethodNotSupportedException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED);
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
    }

    @ExceptionHandler(BusinessException.class) //비즈니스로직에서 던져지는,발생되는 예외들에 대한 처리 (validation오류들과 같은)
    protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.error("handle내부로직에러(BusinessException)", e);
        final ErrorCode code = e.getErrorCode();
        final ErrorResponse response = ErrorResponse.of(code);
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
    }

    @ExceptionHandler(Exception.class) // 위의 예외들에서 걸러지지않은 나머지 예외들에 대한 처리
    protected ResponseEntity<ErrorResponse> handleException(Exception e) {
        log.error("handleAllException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
    }
}

 

오류해결 : ResponseEntityExceptionHandler를 상속받으면 내부의 @ExceptionHandler(Exception.class)가 동작하지 않는 오류

 

Spring) @ExceptionHandler(Exception.class)가 동작하지않음.

public class ExceptionHandlerAdvice extends ResponseEntityExceptionHandler ResponseEntityExceptionHandler의 상속을 지워준다. @ExceptionHandler(Exception.class) not handling all types of exceptions I am trying to handle all Types of exceptions using

keeeeeepgoing.tistory.com

 

 

4. 커스텀 예외

@Getter
public class BusinessException extends RuntimeException{ //api 내부동작할때(비즈니스 로직에서) 발생하는 일반적인 예외들

    private final ErrorCode errorCode;

    public BusinessException(String message, ErrorCode errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
}

 

비즈니스 로직안에서 예외를 발생시켜야할때 에러코드를 이용하여 만들기 위해서 만든 BusinessException 

 

 

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

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

cheese10yun.github.io

 

GitHub - cheese10yun/spring-guide: Spring 실전 가이드

:octocat: Spring 실전 가이드. Contribute to cheese10yun/spring-guide development by creating an account on GitHub.

github.com

 

[Spring] @RestControllerAdvice를 이용한 Spring 예외 처리 방법 - (2/2)

예외 처리는 robust한 애플리케이션을 만드는데 매우 중요한 부분을 차지한다. Spring 프레임워크는 매우 다양한 에러 처리 방법을 제공하는데, 앞선 포스팅에서 @RestControllerAdvice를 사용해야 하는

mangkyu.tistory.com

 

 

GitHub - MangKyu/InterviewSubscription

Contribute to MangKyu/InterviewSubscription development by creating an account on GitHub.

github.com

 

Spring - Exception 처리 전략 적용기 (+ 에러 코드 문서화!)

요즘에 익셉션에 대한 처리에 관심이 많아져서 관련 포스팅을 썼었다. REST에서 예외를 처리하는 다양한 방법! REST API에서 직접 정의한 Error code를 사용해야 하는 이유! 이번에 공부한 내용들과 추

jaehoney.tistory.com

 

@Valid 와 @ControllerAdvice로 DTO 예외처리하기

@Valid 세팅 및 사용하기 해당편에 이어서 @Valid와 @ControllerAdvice를 이용한 Exception처리를 하려고한다.@Vailid 사용법에 관한 설명은 생략한다. 위의 링크를 참고하면된다 @Valid는 @ControllerAdvice와 같이

cchoimin.tistory.com

 

5.테스트

@RestController
@Slf4j
public class ExceptionController {

    @GetMapping("/exception/1")
    public void exceptionTest1() {
        throw new BusinessException(EMAIL_DUPLICATION);
    }

    @GetMapping("/exception/2")
    public void exceptionTest2() {
        throw new NullPointerException();
    }

    @GetMapping("/exception/3")
    public void exceptionTest3() {
        //HttpRequestMethodNotSupportedException 테스트용
    }

}

1. Validation 에러코드 넣어서 BusinessException생성후 throw

2. 널포인트예외는 전역예외핸들러에서 처리하도록 해놓은게 없으므로, 전체 예외를 캐치하는 

@ExceptionHandler(Exception.class) // 위의 예외들에서 걸러지지않은 나머지 예외들에 대한 처리
protected ResponseEntity<ErrorResponse> handleException(Exception e) {
    log.error("handleAllException", e);
    final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
    return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
}

가 동작했다.

3. http요청시 틀린 method방식으로 요청했을 경우

4. @Valid를 이용한 검증, 검증실패시 예외발생처리

@PostMapping("/exception/4")
public ResponseEntity<String> exceptionTest3(@RequestBody @Valid TestModel testModel) {
    log.info("testModel : {}", testModel);
    return ResponseEntity.ok().body("TestModel!!!");
}
@Getter @Setter
@RequiredArgsConstructor
public class TestModel {

    @NotNull
    private String name;
    @NotNull
    private String email;

}

 

 

 

 

 

 

 

 

 

 


직접 정의한 Error code를 쓰는 이유?

 

 

Spring - REST API에서 직접 정의한 Error code를 사용하는 이유!

네이버나 카카오 Open API를 보면 Error Response 스펙에 서비스가 자체적으로 정의한 code를 사용한다. API 레퍼런스 kakao: https://developers.kakao.com/docs/latest/ko/reference/rest-api-reference#response naver: https://develop

jaehoney.tistory.com

 

 


ResponseStatusException?

추가공부

 

Spring - REST에서 예외를 처리하는 다양한 방법!

Spring에서 예외 처리를 구현하는 다양한 방법에 대해 알아본다. Spring 3.2 이전에는 Spring MVC 애플리케이션에서 예외를 처리하는 대표적인 두 가지 방식이 있었다. HandlerExceptionResolver 또는 @ExceptionHa

jaehoney.tistory.com


예외처리 전략

 

 

[Spring] @RestControllerAdvice를 이용한 Spring 예외 처리 방법 - (2/2)

예외 처리는 robust한 애플리케이션을 만드는데 매우 중요한 부분을 차지한다. Spring 프레임워크는 매우 다양한 에러 처리 방법을 제공하는데, 앞선 포스팅에서 @RestControllerAdvice를 사용해야 하는

mangkyu.tistory.com

 

Spring - Exception 처리 전략 적용기 (+ 에러 코드 문서화!)

요즘에 익셉션에 대한 처리에 관심이 많아져서 관련 포스팅을 썼었다. REST에서 예외를 처리하는 다양한 방법! REST API에서 직접 정의한 Error code를 사용해야 하는 이유! 이번에 공부한 내용들과 추

jaehoney.tistory.com

 

@Valid 와 @ControllerAdvice로 DTO 예외처리하기

@Valid 세팅 및 사용하기 해당편에 이어서 @Valid와 @ControllerAdvice를 이용한 Exception처리를 하려고한다.@Vailid 사용법에 관한 설명은 생략한다. 위의 링크를 참고하면된다 @Valid는 @ControllerAdvice와 같이

cchoimin.tistory.com


출처,더자세한정보

 

Spring - Exception 처리 전략 적용기 (+ 에러 코드 문서화!)

요즘에 익셉션에 대한 처리에 관심이 많아져서 관련 포스팅을 썼었다. REST에서 예외를 처리하는 다양한 방법! REST API에서 직접 정의한 Error code를 사용해야 하는 이유! 이번에 공부한 내용들과 추

jaehoney.tistory.com

 

@ControllerAdvice, @ExceptionHandler를 이용한 예외처리 분리, 통합하기(Spring에서 예외 관리하는 방법, 실

예외 처리 과정 프로그래밍에서 예외 처리는 아주 중요하면서도 아주 어렵다. 과하다할 만큼 상세하고 다양하게 예외를 잡아 처리해준다면, 클라이언트도 그렇고 서버도 그렇고 더 안정적인 프

jeong-pro.tistory.com

 

 

댓글