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) 이고, 정적 리소스를 사용중이라면 다른 방법을 찾아봐야할거같다.
'IT > JAVA' 카테고리의 다른 글
[생활코딩] Java - 4.2동작 원리 (0) | 2019.11.10 |
---|---|
[Error] [JAVA] Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException (0) | 2019.02.28 |