본문 바로가기
IT/JAVA

Spring boot 내장 인터페이스 ErrorController 구현한 CustomErrorController 만들기

by 뷰IGHT 2020. 6. 30.

20.06.30 

Backend API (RestAPI) 를 만들기 위해 error 처리를 어떻게 할 것인가 고민하다가 아래 글을 참고하여 CustomErrorController를 만들어 보았다.

 

https://supawer0728.github.io/2019/04/04/spring-error-handling/

 

(Spring Boot)오류 처리에 대해

서론오류 처리는 어플리케이션 개발에 있어 매우 큰 부분을 차지한다.오류를 예측하는 것과 예방하는 것, 그리고 오류를 빨리 발견하고 고칠 수 있는 것은 훌륭한 개발자의 필수조건이라고 생��

supawer0728.github.io

https://velog.io/@stampid/REST-API%EC%99%80-RESTful-API

 

REST API와 RESTful API

1. REST란? Representational State Transfe라는 용어의 약자이다. 자원을 URI로 표시하고 해당 자원의 상태를 주고 받는 것을 의미한다. REST의 구성 요소는 자원(Resource): URI 행위(Verb): HTTP METHOD 표현(Representat

velog.io

 

일단 만들기 전, 성공과 실패 status에 대해서 어떤 결과를 보여주고 싶은지 나름의 API 반환규칙을 세웠다.

 

[성공시] status 200

성공시 결과에 대해서는 data를 보여주고 data에 대한 키는 API 요청 자원명으로 한다.

ex ) (Get) /languagues : get 메소드로 languages 리소스를 요청하는 api

ㄴ 요청자원은 languages

{
    "languages" : [
        { 
            "language_no" : 3,
            "language_name" : "스페인어",
            "is_custom" : "F",
            "member_id" : ""
        },
        { 
            "language_no" : 5,
            "language_name" : "헝가리어",
            "is_custom" : "F",
            "member_id" : ""
        },
        { 
            "language_no" : 9,
            "language_name" : "영어 (my version)",
            "is_custom" : "T",
            "member_id" : "lucy74310"
        },
        { 
            "language_no" : 9,
            "language_name" : "영어 (from dictionary)",
            "is_custom" : "T",
            "member_id" : "lucy74310"
        }
    ]
}

 

 

[실패시] status 404를 비롯한 4xx 과 500

실패시 status 항목에는 http status 값을, error 에는 에러메시지를 보여준다.

 

ex1 ) dispatcher servelet 이 uri mapping을찾지 못하는 end point로 요청을 한 경우

ㄴ (Get) /languagesdfdfsa

{
    "status" : 404,
    "message" : "Not Found"
}

 

ex2) end point 는 존재하지만 요청조건이 충족되지 않은 경우

ㄴ (Post) /language

request body

{
    "language_name" : "",
    "member_id" : "lucy74310"
}

response

{
    "status" : 4xx,
    "message" : "There is no language_name value."
}

 

ex3) end point 는 존재하지만, 서버내부에서 에러가 발생

{
    "status" : 500,
    "message" : "Internal Server Error. If this error persist, Contact the administration."
}

 여기서 만드는 CustomErrorController 는 ex1 ) dispatcher servelet 이 uri mapping을찾지 못하는 end point로 요청을 한 경우 를 위한 Controller다.

 

 ex2)와 ex3)는 서버에 존재하는 end point로 요청이 들어와서 로직이 실행되는 중 발생하는 에러일 것이고, 에러가 발생하면 ControllerAdvice를 통해 @ExceptionHandler를 통해 핸들링 할 것이다.

다음처럼..

@ExceptionHandler
public ResponseEntity<JsonError> handle(IOException ex) {
    int status = Integer.parseInt(HttpStatus.INTERNAL_SERVER_ERROR.toString());
    String errorMessage = ex.getMessage();
    JsonError errorResponse = new JsonError(status, errorMessage);

    return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

 

 

 

ex1)의 경우처럼 mapping 된 endpoint 가 없어서 혹은 다른 이유로 컨트롤러 단을 타지 못하고 발생되는 에러는 application.yml이나 application.properties에 설정된 path 로 요청이 가는데

  error:
    include-exception: true
    include-stacktrace: always
    path: '/error'
    whitelabel:
      enabled: true

( default값 : '/error')
Spring Boot의 org.springframework.boot.autoconfigure.web.servlet.error 패키지 의 BasicErrorController가 해당 path 를 mapping 하고 있다.

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

 

나는, BasicErrorController를 조금 바꿔서 내가 설정한 반환규칙에 맞는 error를 리턴하고 싶다.

 

일단 BasicErrorController를 타면 404에러의 경우 다음과 같이 반환된다.

( Headers 에 Content-Type : application/json 일 경우)

{
    "timestamp": "2020-06-30T14:03:44.865+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/languasfda"
}

https://supawer0728.github.io/2019/04/04/spring-error-handling/ 앞에서도 언급한 이 블로그의 글에 
json 에러 반환 또는 html 에러 반환으로 가게 되는 BasicErrorController가 어떻게 구현되어 있는지 잘 나와있다.

(Request Header의  Content-type에 따라 errorHtml()함수 또는 error() 함수를 탄다.)

 

내가 반환하려는 양식은 다음과 같기 때문에 BasicErrorController가 구현하고 있는걸 따라서 CustomErrorController를 만든다.

{
    "status" : 404,
    "message" : "Not Found"
}

 

1. ErrorController 인터페이스를 implements 한 CustomErrorController를 만든다.

class CustomErrorController implements ErrorController {

 

2. 내가 만든 CustomErrorController에 현재 설정되어 있는 error path를 requestMapping으로 달아준다.

@Controller
class CustomErrorController implements ErrorController {
    ...
    
    @Value("${server.error.path:${error.path:/error}}")
    private String errorPath;
    
    ...
    
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public ResponseEntity<JsonError> handleError(HttpServletRequest request) {
    }
    
    ...
    
    @Override
    public String getErrorPath() {
        return this.errorPath;
    }
}

3. 위 handleError 함수를 구현해 준다.

 

나는 json 타입으로만 반환해주고 싶기 때문에 BasicErrorController의 error() 함수를 따라하였다. 

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

    ...
    
	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}
    
    ...
}

리턴해줄 데이터(위에선 body 변수) 를 내가 원하는 커스텀 리턴 데이터로 구현해주면 된다.

 

위에서 body 객체를 가져오는 방법을 따라한다.

 

1. ErrorController를 implements 하고 있는 추상클래스 AbstractErrorController의 getErrorAttributes()를 만든다.

나는 추상클래스 단계를 놓지 않았으니 CustomerErrorController에 바로 getErrorAttributes()를 만든다.

@Controller
class CustomErrorController implements ErrorController {
	
    ...
    
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
 	   return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
    }
    
    ...
}

ㄴ  getErrorAttributes()는 HttpServletRequest를 인자로 받아 ErrorAttributes 객체의 getErrorAttributes(HttpServletRequest request)를 호출한다.

 

이때, ErrorAttributes 세팅을 위해 역시 BasicErrorController를 따라 CustomErrorController 성생시 ErrorAttributes 를 세팅하도록 생성자를 만든다.

@Controller
class CustomErrorController implements ErrorController {

    private final ErrorAttributes errorAttributes;
	
    ...
	
    public CustomErrorController(ErrorAttributes errorAttributes) {
	    Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
	    this.errorAttributes = errorAttributes;
	}
    
    ...
}

 

 

3. 다시 handleError() 함수로 돌아가서.. 함수를 구현해준다. 

getErrorAttributes로 요청된 request에 대한 error 정보를 가져오고 ( // 주석1 ), 

그 정보에서 status와 error 를 가져와서 나의 커스텀 JsonError 객체 ( 멤버변수로 status와 message만 가지고 있는) 를 생성하여 ResponseEntity의 body로 반환해준다. 

@Controller
class CustomErrorController implements ErrorController {

    private final ErrorAttributes errorAttributes;
	
    ...
	
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public ResponseEntity<JsonError> handleError(HttpServletRequest request) {
        JsonError errorResponse = getErrorFromRequest(request); //주석1
        return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorResponse.getStatus()));
    }

    //주석1 함수 - error정보 가져오는 함수 
    private JsonError getErrorFromRequest(HttpServletRequest request) {
        WebRequest webRequest = new ServletWebRequest(request);
        Map<String, Object> errorRequest = getErrorAttributes(webRequest, false);

        int errorStatus = (int) errorRequest.get("status");
        String errorMessage =  errorRequest.get("error").toString();

        return new JsonError(errorStatus, errorMessage);
    }
    
    ...
}

 

 

여기까지 하니 404 에러에 대해 목표한 대로 반환이 되었다. 


22.02.22 추신

윗 글은 비교적 처음 Spring Boot를 배울때 나름 구현하고싶은 에러를 어떻게 구현할까 고민하며 적었었는데,
지금의 내가 엔드포인트가 없는 경우 리턴될 에러를 작성하고자 한다면, 
https://www.baeldung.com/exception-handling-for-rest-with-spring#controlleradvice > ControllerAdvice 방법을 사용하여 작성할 것 같다.

엔드포인트가 없는 경우 반환하는 에러는 HttpRequestMethodNotSupportedException 이다.

org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported

그렇다면 RestControllerAdvice 클래스 에서 다음과 같은 식으로 말이다. 

@RestControllerAdvice
@Component
public class ControllerExceptionHandlingAdvice {

    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    public ResponseEntity<JsonError> handleNotFoundException(final HttpRequestMethodNotSupportedException e) {
        JsonError jsonError = new JsonError();
        jsonError.setStatus(HttpStatus.NOT_FOUND);
        jsonError.setMessage("Not Found");

        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(jsonError);
    }

}

 

그리고.. 제목에 "내장 인터페이스 ErrorController 구현한" 이라고 적었는데..
내장 인터페이스 ErrorController는 내용이 없다는것을 알게됨...
제목은 "BasicErrorController 를 참고한 CustomErrorController 만들기" 로 바꿔야 할거같다

22.06.21 추신
22.02 추신에서는  HttpRequestMethodNotSupportedException을 Handler에 등록했는데,

NoHandlerFoundException을 ExceptionHandler에 등록하는 방법도 있다. (경우에 따라 다른 exception이 반환되나 싶은데..)

이걸 하려면, application.yml에 추가해줘야 하는게 있다. 

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

 

이걸해줘야 정상적으로 NoHandlerFoundException 이 발생이 되고 ExceptionHandler가 catch할수 있다.. 
어디서 봤는지 다시 찾으려니 못찾았는데, 적절한 handler를 찾지 못한 경우, 정적리소스로 생각해 classpath로 던진다는 얘기를 봤다. 그런 예외를 막기 위한 설정(resources.add-mappings) 이고, 정적 리소스를 사용중이라면 다른 방법을 찾아봐야할거같다.