API 예외 처리 (1)

달래·2024년 1월 22일
0

Spring

목록 보기
4/7

오류페이지는 사용자 화면만 고려하면 된다.

하지만 API 오류처리는 각 오류상황(정상/오류 등..)에 맞는 응답스펙을 정하고, json으로 데이터를 내려주어야 한다!

예)

{
	"id" : "dallae",
	"pwd" : "password!@#"    
}

의 JSON 형태로 API 데이터를 받았다면, 응답도

{
	"message" : "잘못된 사용자",
    "status" : 500
}

과 같은 JSON형식으로 내려주는게 일반적이다.

스프링부트에서 제공하는 기본 오류방식은 어떨까?

스프링 부트의 BasicErrorController를 살펴보자.

이것을 사용해서 JSON 요청을 보내게 되면,

{
"timestamp": "2021-04-28T00:00:00.000+00:00", "status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"trace": "java.lang.RuntimeException: 잘못된 사용자\n\tat
  hello.exception.web.api.ApiExceptionController.getMember(ApiExceptionController
  .java:19...,
"message": "잘못된 사용자",
      "path": "/api/members/ex"
  }

오류API를 기본적으로 생성해 주어, 위와 같은 JSON 형식의 응답을 받을 수 있다.

하지만, API 예외 처리는 이렇게 하나로 처리하기 어렵다.
API마다, 각각 컨트롤러/예외마다 서로다른 응답결과를 출력하는 것이 일반적이다!
예) 회원API의 예외응답과 상품API의 예외응답은 달라질 수 있음.

이런 경우에는 어떻게 할까?

HandlerExceptionResolver

컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작 방식을 변경하기 위하여 사용되는 인터페이스이다.

컨트롤러에서 예외가 발생해도 서블릿까지 전달되지 않고, 스프링MVC에서 예외처리가 마무리되기 때문에 WAS 입장에서는 정상처리요청이 된다.

  1. 적용 전
WAS -> 서블릿 -> preHandle -> 핸들러 어댑터 -> 핸들러(예외발생) -> 서블릿(예외전달) -> postHandle호출x(예외전달) -> afterCompletion -> WAS에 예외전달
  1. 적용 후
WAS -> 서블릿 -> preHandler -> 핸들러 어댑터 -> 핸들러(예외발생) -> 서블릿(예외전달) -> ExceptionResolver는 예외해결시도, ModelAndView 전달 -> postHandler호출x -> afterCompletion -> render(model)후 View -> WAS 정상응답

ExceptionResolver 구현

public class ExceptionResolverName implements HandlerExceptionResolver {
	@Override
    public ModelAndView resolveException(...,Object handler, Exception ex){
    	try{
        	if(ex instanceof 익셉션){
            	response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                return new ModelAndView();
            }
        } catch {...}
        return null;
    }
}
  • try-catch를 사용하여, 말그대로 Exception을 Resolve(해결)해서 정상흐름처럼 변경한다.
  • 익셉션 종류의 예외가 발생하면, SC_BAD_REQUEST(400)상태코드&메시지와 함께 빈 ModelAndView를 반환한다.

반환값에 따른 동작방식

  1. 빈 ModelAndView
  • 뷰를 렌더링하지않고, 정상흐름으로 서블릿 리턴
  • response.sendError() -> 서블릿에서 상태코드에 따른 오류처리 위임 -> WAS는 서블릿 오류페이지를 찾아내 다시 호출한다.
  1. ModelAndView 정보 지정
  • View나 Model정보 지정-> 뷰 렌더링
  • 예외에 따른 새로운 오류 화면을 뷰 렌더링해서 클라이언트에게 제공한다.
  1. null
    다음 ExceptionResolver를 찾아서 실행
    처리할수있는 ExceptionResolver가 없으면 서블릿밖으로 던짐!

그 외에도, response.getWriter().println()처럼 응답바디에 직접 데이터를 넣어줄 수 있다. JSON으로 응답하면 API 응답처리 가능.

WebConfig 등록

WebMvcConfigurer 인터페이스의 extendHandlerExceptionResolvers() 메소드를 구현해 등록할 수 있다.
configureHandlerExceptionResolvers() 메소드를 구현하면 스프링이 기본으로 등록하는 ExceptionResolver가 제거되므로 주의하자.

@Configuration
public class WebConfig implements WebMvcConfigurer {
	@Override
	publi void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    	resolvers.add(new ExceptionResolverName());
    }    
}

=> 하지만 ModelAndView를 반환해야함 -> API 응답에는 필요하지 않음
HttpServletResponse에 직접 응답데이터를 넣어주는 것은 매우 원시적(순수서블릿 사용) & 불편

그래서, @ExceptionHandler를 사용한다.

위에서 직접 ExceptionResolver를 구현하였다.
이를 스프링이 제공하는 ExceptionHandler를 사용하여 더 간단하게 구현해보자!

스프링 등록 순서

  1. ExceptionHandlerExceptionResolver
  2. ResponseStautsExceptionResolver
  3. DefaultHandlerExceptionResolver

ExceptionHandlerExceptionResolver

@ExceptionHandler 처리. 대부분의 API 예외 처리

  • 예외 한번에 처리
    @ExceptionHandler({AException.class, BException.class})
  • 예외 생략
    @ExceptionHandler -> 메서드 파라미터의 예외가 지정

ResponseStatusExceptionResolver

@ResponseStatus 사용
HTTP 상태코드 지정

`@ResponseStatus(code = HttpStatus.NOT_BAD_REQUEST, reason = "잘못된 요청 오류 또는 error.bad")
public class BadRequestException extends RuntimeException {...]

결국 response.sendError(statusCode, resolvedReason)을 호출 -> WAS에서 다시 오류페이지를 내부요청
reason은 MessageSource에서 찾는 기능도 제공

  • 개발자가 직접 변경할수 없는 예외에는 적용 불가능
  • 어노테이션 사용 -> 조건에 따라 동적으로 변경하는 것도 어려움

DefaultHanlderExceptionResolver

스프링내부 기본예외 처리

사용

CustomException

public class CustomException extends RuntimeException {
	public CustomException(){super();}
    public CustomException(String message){super(message);}
    public CustomException(String message, Throwable cause){super(message, cause);}
    public CustomException(Throwable cause){super(cause);}
    protected CustomException(String message, Throwable cause, boolean enabelSuppression, boolean writableStackTrace){super(message, cause, enableSuppression, writableStackTrace);}
}

ErrorDto(객체)

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

예외 API 응답 객체

RestContrloler

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorDto exHandle(IllegalArgumentException e) {
	return new ErrorDto("code", e.getMessage());
}

실행흐름
1. 컨트롤러 예외
2. ExceptionResolver 작동, 우선순위가 가장 높은 ExceptionHandlerExceptionResolver 실행 -> 해당 컨트롤러에 @ExceptionHandler 존재여부 확인
3. @ExceptionHandler로 등록된 Controller 메소드 실행
@RestController라 @ResponseBody 적용됨 -> Http컨버터 사용, 응답이 ErrorDto 객체형태로 JSON으로 반환
4. @ResponseStatus(HttpStatus.BAD_REQUEST) 지정 -> 400으로 응답

단, ResponseStatus는 어노테이션이므로 Http응답코드를 동적으로 변경할 수 없음
따라서 아래와 같은 방식으로 사용을 권장한다.

@ExceptionHandler
public ResponseEntity<ErrorDto> customExHandle(CustomException e){
	ErrorDto errorDto = new ErrorDto("code", e.getMessage());
    return new ResponseEntity<>(errorDto, HttpStatus.BAD_REQUEST);
}	
  • 파라미터인 CustomException 예외를 사용
  • ResponseEntity로 Http 바디에 직접 응답 (Http 컨버터 사용)
    ->@ResponseStatus와 다르게 Http 응답 코드를 동적으로 변경 가능

우선순위

자세한 것이 우선순위를 가짐!
부모클래스 지정 -> 자식클래스까지 처리 가능


여기에서 정상코드와 예외코드를 분리하고싶다면? @ControllerAdvice

@ExceptionHandler를 사용하였지만, RestController에 모두 작성하여 클라이언트의 정상 요청코드와 예외처리코드를 분리할 수 없다.
하지만 @ControllerAdvice 또는 @RestControllerAdvice를 사용하면 예외코드를 분리가능!

@RestControllerAdvice
public class CustomControllerAdvice {
	...예외처리코드 (@ExceptionHandler사용)
}

@ControllerAdvice

대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능 부여 -> 대상을 지정하지 않으면 모든 컨틀롤러에 적용(글로벌 적용)

  • @RestControllerAdvice@ResponseBody만 추가되어있음

컨트롤러 지정

@ControllerAdvice(annotations = RestController.class)
public class CustomAdvice {}

@RestController에 지정

@ControllerAdvice("com.package")
public class PackageAdvice {}

com.package 패키지에 지정

@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class AllControllersAdvice {}

모든 컨트롤러에 지정

profile
아좌잣~!

0개의 댓글