'handler'에 해당하는 글 2건

Spring API 프로젝트에서 공통적인 에러 코드로 응답하기.

 

API 개발시 어떠한 처리 결과에도 동일한 포맷의 응답(response) 을 리턴하는 것은 중요하다. 아래는 가장 간단한 예이다.

 

{
    "code": 200,
    "message": "ok",
    "result": {}
}

 

code / message 로 미리 정의된 응답 코드와 메시지를 확인할 수 있고, 정상적으로 처리가 되었을 경우 result 에서 결과를 확인할 수 있다. 디테일이 필요하다면 더 다양한 코드에 상세한 설명을 넣으면 되고, 보안을 강화한다면 메시지를 없앨 수도 있다. 이러한 공통적인 포맷을 Spring 에서 구현하는 방법을 살펴 보았다.

 

 

1. whitelabel 에러 페이지

 

Spring Boot 는 기본적으로 text/html 타입의 요청에 대해 whitelabel 이라는 오류 페이지를 제공한다. 기존 톰캣의 보라색 에러 페이지 보다는 괜찮아 보일 수 있다.

 

 

whitelabel-spring

 

브라우저에서 접속했을 때에도 에러 페이지를 커스텀 하려면 properties 파일에서 whitelabel 페이지를 비활성화 하고 에러 페이지를 재정의해야 한다.

 

# application.properties
server.error.whitelabel.enabled=false

 

 

2. 기본 json 에러 응답

 

Spring Boot 는 기본적으로 RESTful 이나 application/json 타입의 요청에 대해 basicErrorController 로부터 아래와 같은 포맷으로 응답한다.

 

{
    "timestamp": "2022-02-15T04:28:46.310+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/a"
}

 

위 속성은 DefaultErrorAttributes 에서 정의되는데 속성만 추가하려면 application.properties 에서 server.error 옵션들을 수정하던지 DefaultErrorAttributes 를 상속하여 커스텀 할 수도 있다.

 

# message 항목 보이기
server.error.include-message=always

 

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> result = super.getErrorAttributes(webRequest, options);
        result.put("description", "abcdefg");
        return result;
    }
}

 

 

3. AbstractErrorController 상속하여 재정의

 

basicErrorController 처럼 ErrorController 인터페이스를 구현한 AbstractErrorController 를 상속하여 에러 응답을 재정의할 수 있다.

 

@RestController
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomErrorController extends AbstractErrorController {

    public CustomErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }

    @RequestMapping
    public ResponseEntity<ErrorMessageEntity> customError(HttpServletRequest request) {
        HttpStatus httpStatus = getStatus(request);

        CustomResponseEntity customResponseEntity = new CustomResponseEntity();
        customResponseEntity.setCode(httpStatus.value());
        customResponseEntity.setMessgae(httpStatus.getReasonPhrase());
        customResponseEntity.setResult(null);

        return new ResponseEntity<>(customResponseEntity, httpStatus);
    }
}

 

@Entity
public class CustomResponseEntity {
    private Integer code;
    private String messgae;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String result;
    ...
}

 

기본 에러 경로인 /error (properties 파일: server.error.path) 는 basicErrorController 에서 매핑을 시도하므로, ErrorController 인터페이스를 구현하지 않고 /error 컨트롤러를 사용한다면 런타임 에러가 발생한다.

 

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'basicErrorController' method

 

 

4. ResponseStatusException 사용

 

위와 같이 AbstractErrorController 를 상속하면 모든 HTTP 상태 코드에 대한 응답을 커스텀 할 수 있다. 여기에 Spring 5+ 를 사용 중이라면 RuntimeException 을 상속하는 ResponseStatusException 를 이용해 어디에서든 원하는 에러를 발생시킬 수 있다.

 

throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path");

 

특정 상황에 특정 에러 코드와 메시지로 응답하고 싶었는데 2번째 파라미터를 응답 엔티티에 넣는 것이 쉽지 않았다.

 

 

5. Custom Exception 생성

 

ResponseStatusException 을 대신하여 메시지도 응답할 수 있도록 CustomException 을 생성한다.

 

@Component
public class CustomException extends RuntimeException {

    private ErrorCode errorCode;

    public CustomException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return this.errorCode;
    }
}

 

에러 메시지를 enum 으로 정리한다.

 

public enum ErrorCode {
    
    EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Expired Token"),
    INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid Token"),
    SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"),
    ...
    ;

    private final HttpStatus status;
    private final String message;

    ErrorCode(HttpStatus status, String message) {
        this.status = status;
        this.message = message;
    }

    public HttpStatus getStatus() {
        return this.status;
    }

    public String getMessage() {
        return this.message;
    }
}

 

컨트롤러 전역에서 에러를 핸들링할 수 있도록 @ControllerAdvice, @ExceptionHandler 를 작성한다.

 

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<CustomResponseEntity> myCustomException(CustomException ex) {
        CustomResponseEntity customResponseEntity = new CustomResponseEntity();

        ErrorCode errorCode = ex.getErrorCode();
        customResponseEntity.setCode = errorCode.getStatus().value();
        customResponseEntity.setMessage = errorCode.getMessage();

        return new ResponseEntity<>(customResponseEntity, errorCode.getStatus());
    }
}

 

특정 코드에 에러 발생시키기.

 

throw new CustomException(EXPIRED_TOKEN);

// output
{
    "code": 401,
    "message": "Expired Token"
}

 

 

6. 404 error 핸들러 추가

 

여기까지 하면 404 에러 발생시 에러 핸들러를 찾을 수 없다는 500 에러가 발생하게 되는데, dispatcher servlet 에서 요청을 처리할 수 없는 핸들러가 생긴다면 NoHandlerFoundException 예외를 발생시키도록 아래와 같이 수정한다.

 

# application.yaml
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false

 

위 5번 GlobalExceptionHandler 에 아래 NoHandlerFoundException 추가하여 NoHandlerFoundException 예외를 처리한다.

 

@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<DefaultMessageEntity> handle404(NoHandlerFoundException ex) {
    CustomResponseEntity customResponseEntity = new CustomResponseEntity();
    customResponseEntity.setCode = HttpStatus.NOT_FOUND.value();
    customResponseEntity.setMessage = HttpStatus.NOT_FOUND.getReasonPhrase()

    return new ResponseEntity<>(customResponseEntity, HttpStatus.OK);
}

 

 

 

CustomException(EXPIRED_TOKEN) 이라는 에러를 발생시키면, @ExceptionHandler 에서 해당 예외를 처리하는 예제이다. CustomException 을 제외한 예외들은 3번에서 작성한 CustomErrorController 에 의해 처리될 것이다. 3번 5번 6번 조합!

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

Spring MVC 에서는 기본 error 페이지를 제공하지 않아 SimpleMappingExceptionResolver 를 사용해야 하지만, Spring Boot 는 기본적으로 BasicErrorController 를 설정하여 모든 error 를 처리하는 /error 매핑을 제공하며, 서블릿 컨테이너에 전역 error 페이지로 등록된다. 예로, Thymeleaf 를 사용할 경우 /error 와 매핑되는 페이지는 src/main/resources/templates/error.html 가 될 것이다.


시스템 클라이언트의 경우, HTTP 상태, error, 메시지를 담은 JSON 형식으로 응답한다. 브라우저 클라이언트의 경우, 위와 동일한 데이터를 HTML 형식으로 렌더링한 /error 페이지로 응답할 것이며, 존재하지 않을 경우 Spring boot 에서 제공하는 "whitelabel" error 페이지로 응답한다. whitelabel 에는 exception 정보가 명시되므로 다른 error 페이지로 대체하는 것이 좋다. (properties 파일에서 설정이 가능하다. server.error.whitelabel.enabled=false)


error 를 처리할 뷰를 추가하여 커스터마이징 할 수 있는데, @ExceptionHandler 와 @ControllerAdvice 같은 일반적인 Spring MVC 기능을 사용할 수 있다.

@ExceptionHandler 는 단일 컨트롤러 등에서, @ControllerAdvice 는 전역에서 exception 처리를 한다.



@ResponseStatus 과 HTTP 상태 코드 사용


일반적으로 웹 요청을 Spring boot 에서 처리할 때 정의되지 않은 exception 이 발생하면 서버는 HTTP 500 응답을 반환한다. @ResponseStatus 어노테이션을 메소드나 exception 클래스에 사용하면 지정한 상태 코드와 메시지를 반환할 수 있다. 아래는 404 exception 클래스를 만들고, 컨트롤러에 전달된 주문번호(id)가 존재하지 않으면 해당 exception 처리를 하는 예이다.


@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order")  // 404
public class OrderNotFoundException extends RuntimeException {
    // Process Exception
}
 
@RequestMapping(value="/orders/{id}", method=GET)
public String showOrder(@PathVariable("id"long id, Model model) {
    Order order = orderRepository.findOrderById(id);
 
    // Call Exception
    if (order == nullthrow new OrderNotFoundException(id);
 
    model.addAttribute(order);
    return "orderDetail";
}
cs


이렇게 반환되는 에러 정보는 콘솔이나 /error 에 매핑되는 페이지에서 확인할 수 있으며, 없다면 whitelabel 페이지에서 확인할 수 있다.

RESTful 요청의 경우 다음과 같이 error 정보를 json 형식의 응답으로 보여준다.


{
    "timestamp""2018-11-08T05:22:05.440+0000",
    "status"404,
    "error""Not Found",
    "message""No such page",
    "path""/orders"
}
cs



@ExceptionHandler 사용


@RequestMapping 어노테이션을 가진 메소드에서 exception 을 처리하기 위해 동일한 컨트롤러에 @ExceptionHandler 메소드를 추가하여 사용할 수 있다. @ExceptionHandler 을 사용하면, @ResponseStatus 어노테이션이 없는 exception 도 다룰 수 있고, 별로도 작성한 error 페이지로 사용자를 리디렉션 시킬 수 있다. @ExceptionHandler 는 HttpServletRequest, HttpSession, WebRequest, Locale, InputStream, OutputStream, Empty Model, ... 등의 여러가지 파라미터 type 을 가질 수 있다. 또한 ModelAndView, Model, Map, View, String, @ResponseBody, HttpEntiry<?>, ResponseEntity<?>, void, ... 등의 type 을 반환할 수 있다.


다음은 DataIntegrityViolationException 라는 미리 작성한 exception 클래스를 호출시켜 핸들러에서 처리하는 예제이다.


@Controller
public class ExceptionHandlingController {
    ...
    @RequestMapping("/dataIntegrityViolation")
    String throwDataIntegrityViolationException() throws SQLException {
        throw new DataIntegrityViolationException("Duplicate id");
    }
 
    @ResponseStatus(value = HttpStatus.CONFLICT, reason = "Data integrity violation"// 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void conflict() {
        // Nothing to do
    }
}
cs



@ControllerAdvice 사용


@ControllerAdvice 를 사용하면 개별 컨트롤러뿐만 아니라 전체 어플리케이션에서 exception 처리를 할 수 있다.

모든 exception 의 기본 핸들러의 코드는 다음과 같다.


@ControllerAdvice
class GlobalDefaultExceptionHandler {
    public static final String DEFAULT_ERROR_VIEW = "error";
 
    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        // exception 에 @ResponseStatus 를 가진 경우 프레임워크가 처리하도록 한다.
        if (AnnotationUtils.findAnnotation (e.getClass(), ResponseStatus.class!= null)
            throw e;
 
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    }
}
cs




WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,