스프링/4. 스프링 MVC-2

73. @ExceptionHandler API 예외

sdafdq 2023. 9. 16. 16:29

Html 화면 오류 vs API 오류

 

html 사용하는 환경의 오류는 BasicErrorController를 사용하는게 편하다. 그냥 리소스의 템플릿에 /error 폴더 만들어서 코드명.html로 에러 페이지 만들어 놓으면 스프링이 자동으로 등록시키는 BasicErrorController에서 자동으로 인식한다.

 

 

근데 API는 시스템마다 응답의 모양도 다르고, 스펙도 다르다. 단순히 예외에 대한 정보를 보여주는 게 아니라, 특정 예외에 따라서 각각 다른 데이터를 보내줘야 할 때도 있다. 

한마디로 세밀한 제어가 필요하다.

예를 들어 상품API와 주문 API 의 오류는 에러정보를 Json으로 내려줄 때 모양새가 완전히 다를 수 있다.

 

 

스프링은 이러 한 문제점을 해결하기 위해, @Exceptionhandler라는 매우 편리한 애노테이션을 제공한다.

이것이 바로 71번째 글의 우선순위에 잠깐 나왔던 ExceptionHandlerExceptionResolver이다.

 

실무에서 API예외처리는 대부분 이 기능을 사용한다.

 

@Slf4j
@RestController
public class ApiExceptionV2Controller {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e){
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }


    @GetMapping("/api2/members/{id}")
    public ApiExceptionController.MemberDto getMember(@PathVariable("id") String id){
        if(id.equals("ex")){
            throw new RuntimeException("잘못된 사용자");
        }

        if(id.equals("bad")){
            throw new IllegalArgumentException("잘못된 입력 값");
        }
        if(id.equals("user-ex")){
            throw new UserException("사용자 오류");
        }

        return new ApiExceptionController.MemberDto(id, "hello " + id);
    }


    @Data
    @AllArgsConstructor
    static class MemberDto{
        private String memberId;
        private String name;
    }
}

그냥 오류가 생길 수 있는 컨트롤러에

@ExceptionHandler(IllegalArgumentException.class)

하고 컨트롤러에서 정의해 두면,

예외발생 시 일단 5번 ExceptionResolver로 갔다가, 

에러에 대해 온 정보 중 해당 컨트롤러에 대한 정보까지 같이 받아서,

그 컨트롤러에 @ExceptionHandler가 있는지 조회해 본다.

 

다음은 실제 HandlerExceptionResolver 인터페이스의 일부 코드이다.

@Nullable
	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

request, response, 에러 뿐만 아니라 핸들러까지 받는다. 저 핸들러 안에 @ExceptionHandler가 있는지 조회해 보는거다.

그래서 만약 있다면, 

 

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e){
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("BAD", e.getMessage());
}

여기에 명시한 대로 처리를 하는 것이다.

단, 여기서 이렇게 깨끗하게 처리를 하였으므로, WAS는 이걸 정상적인 것으로 본다.(상태코드가 200으로 된다.)

그래서 상태코드를 @ResponseStatus(상태코드)해서 바꿔서 넣어주는 것이다.

 

ErrorResult는 API의 스펙? 간단하게 우리가 정의해 놓은 것이다.

@Data
@AllArgsConstructor
public class ErrorResult {
    private String code;
    private String message;
}

 

 

위의 순서도 보면 알겠지만, 응답의 Exception이 WAS로 가기 전에 처리 한 것이라 WAS까지 정상적으로 쭉 간다.

 

 

추가로 더 @ExceptionHandler를 구현해 봤다.

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){
    log.error("[exceptionHandler] ex", e);
    ErrorResult errorResult = new ErrorResult("USER-EX",e.getMessage());
    return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(UserException.class)랑 똑같다. 저렇게 좀 인자 받는 느낌으로 받게도 할 수 있다. 왠지 저게 더 보기 좋다.

 

이거는 아예 Return타입을 ResponseEntity로 Json으로 변환 후 반환하게 해놨다. 

사실 상 거의 보통의 API용 컨트롤러 쓰는 느낌이다.

 

추가로 또 @ExceptionHandler를 구현해 봤다.

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e){
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("EX", "내부 오류");
}

아예 Exception을 받게 해 놨다.

위 처럼 더 구체적인? 더 신세대? 더 자세한? 쪽이 먼저 조회되는 것 같다.

신세대 쪽 부터 조회를 하다가, 맨 마지막을 Exception 쪽으로 조회하는 것 같다.

 

 

정리 

사용법

예외가 발생할 여지가 있는 컨트롤러에,

@ResponseStatus(상태코드) <- 안 넣으면 200으로 정상처리라고 응답하기 때문

@ExceptionHandler(예외클래스.class) 혹은 @ExceptionHandler 하고 인자로 예외 받기

 

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e){
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("EX", "내부 오류");
}

이런 식으로 만들면, 그 컨트롤러 안에서 일어나는 예외는 이렇게 정의해 놓은 얘네가 처리를 하는 것이다.

 

ResponseEntity를 반환타입으로 하여, 다른 컨트롤러 처럼 반환하여도 된다.

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){
    log.error("[exceptionHandler] ex", e);
    ErrorResult errorResult = new ErrorResult("USER-EX",e.getMessage());
    return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}

 

 

@ExceptionHandler(AException.class, BException.class) 이렇게 여러 예외를 묶어서 처리할 수도 있다. 아마도 둘 중 하나의 예외가 저리로 들어오면 처리되는 듯 하다.

 

 

이건 인자도 컨트롤러 처럼 받을 수 있다. 정말 하나의 컨트롤러라고 생각하면 된다. return 타입을 String으로 해 놓으면 똑같이 뷰템플릿을 호출할 수도 있다.

 

하긴 request, response 거쳐서 가는 거니까. 일종의 ModelAndView 반환하는 느낌도 있다.

하지만 받을 수 있는 인자나, 반환타입은 컨트롤러 보단 적다. 아마 예외처리할 때 필요한 정보들만 인자로 받거나 반환하거나 할 수 있는 듯 하다.

 

그래도, 컨트롤러와 유사 하므로 이 컨트롤러에서 예외가 발생하면 호출되는 컨트롤러 라고 생각하면 된다.

 

 

근데 이미 MVC는 BasicErrorController가 있어서, 이건 거의 API만 쓴다고 생각하면 된다.