API서버를 통해 데이터를 주고받을 때 정해진 규격으로 전달하는 것은 중요하다. 그래야 요청하는 쪽에서도 그 폼을 기준으로 개발할 것이기 때문이다.
그런데 의외로 이것을 많이 간과하고 넘어가는 경우가 있다. Controller 안에 들어온 후에는 결과 폼을 지정해서 내보내는건 규칙적으로 한다. 그런데 Contoller로 진입하지 못하고 실패했을 때는 Spring의 기본 form에 맞춰서 리턴되는 경우가 있다. 예를들면 다음과 같은 경우다
Controller에 접속하고 난 후의 결과 format
{
"resultCode": "SUCCESS",
"resultMsg": "정상"
}
진입에 실패한 후의 format
(trace는 내용이 너무 길어 생략)
{
"timestamp": "2020-08-04T23:04:45.936+0000",
"status": 400,
"error": "Bad Request",
"message": "Required request body is missing: public kr.sample.demo.demo202008.ResultForm kr.sample.demo.demo202008.Test202008Controller.test202008Test(kr.sample.demo.demo202008.RequestTest202008)",
"trace": ...
}
위의 에러는 POST로 보내는데 값을 아무것도 보내지 않아 body가 없어 발생한 에러다.
일단 Controller를 살펴보면 다음과 같이 코딩했다.
@RestController
public class Test202008Controller {
@PostMapping("/202008/test")
public ResultForm test202008Test(@RequestBody @Valid RequestTest202008 request) {
System.out.println("request = " + request);
return ResultForm.success();
}
}
RequestTest202008은 다음과 같이 구성되어 있다.
@Getter
@ToString
public class RequestTest202008 {
@NotEmpty(message = "name은 필수입니다")
private String name;
@Min(value = 10, message = "age는 10살 이상부터 가능합니다")
private int age;
}
그리고 리턴할때의 폼은 다음 클래스에서 잡는다
@Getter
public class ResultForm<T> {
private ResultCode resultCode;
private String resultMsg;
public ResultForm(ResultCode resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}
public static ResultForm success() {
return new ResultForm(ResultCode.SUCCESS, "정상");
}
public static ResultForm fail(ResultCode resultCode, String resultMsg) {
return new ResultForm(resultCode, resultMsg);
}
public enum ResultCode {
SUCCESS, UNKNOWN, ERROR, INVALID_PARAMETER, NO_BODY
}
}
위의 에러는 요청할때 body부분에 아무런 파라미터도 포함시키지 않았기 때문에 발생한 에러다.
(Postman 으로 테스트 한 결과)
또는 Valid체크한 파라미터가 통과되지 못했을때는 다음처럼 데이터포멧이 생성된다
{
"timestamp": "2020-08-04T23:10:49.783+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotEmpty.requestTest202008.name",
"NotEmpty.name",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"requestTest202008.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "name은 필수입니다",
"objectName": "requestTest202008",
"field": "name",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotEmpty"
},
{
"codes": [
"Min.requestTest202008.age",
"Min.age",
"Min.int",
"Min"
],
"arguments": [
{
"codes": [
"requestTest202008.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
},
10
],
"defaultMessage": "age는 10살 이상부터 가능합니다",
"objectName": "requestTest202008",
"field": "age",
"rejectedValue": 0,
"bindingFailure": false,
"code": "Min"
}
],
"message": "Validation failed for object='requestTest202008'. Error count: 2",
"trace": ...
}
이같은 것들을 처음 보여준 Form으로 전달하면 좋을 것이다. 그래서 @ControllerAdvice를 통해 이 문제를 해결할 것이다.
다음과 같이 @ControllerAdvice를 구현할 클래스를 생성한다.
@ControllerAdvice(basePackages = "[패키지경로]")
public class TestControllerAdvice {
...
}
Body에 아무것도 넣지않아 발생한 에러를 캐치하여 ResultForm 에 담아 전달할 것을 설정한다
@ControllerAdvice(basePackages = "[패키지경로]")
public class TestControllerAdvice {
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ResultForm httpMessageNotReadableExceptionHandler(HttpMessageNotReadableException ex) {
ex.printStackTrace();
return ResultForm.fail(ResultForm.ResultCode.NO_BODY, "파라미터가 없습니다");
}
}
포스트맨으로 테스트 시작
의도한대로 form이 구성되어 리턴되었다.
콘솔에는 다음과 같은 로그가 찍혀있다.
(Required request body is missing)
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public kr.sample.demo.demo202008.ResultForm kr.sample.demo.demo202008.Test202008Controller.test202008Test(kr.sample.demo.demo202008.RequestTest202008)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:161) at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:131) at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121) at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167) ... |
이번에는 Vaild에 통과하지 못할 파라미터로 전송해보자.
만들어놓은 ControllerAdvice로 필터되지 못하고 이전처럼 리턴한다.
이것을 필터할 ExceptionHandler를 추가한다.
...
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
@ResponseBody
public ResultForm methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
ex.printStackTrace();
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
String message = "";
if (fieldErrors.size() > 0) {
message = fieldErrors.get(0).getDefaultMessage();
}
return ResultForm.fail(ResultForm.ResultCode.INVALID_PARAMETER, message);
}
...
이제 다시 테스트해보면 form에 맞춰 전달된다.
한가지 알아둬야 할 점은 message를 구성할때 에러필드 1개만 메세지에 담아 리턴했는데, 사실 Valid를 사용하면 현재 문제가 된 에러들을 모두 보여준다. 예를들어 name에도 빈값을 넣으면 다음과 같이 name과 age 둘다 표기가 된다.
error 메세지
2020-08-05 08:26:44.167 WARN 7304 --- [nio-8080-exec-7] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public kr.sample.demo.demo202008.ResultForm kr.sample.demo.demo202008.Test202008Controller.test202008Test(kr.sample.demo.demo202008.RequestTest202008) with 2 errors: [Field error in object 'requestTest202008' on field 'name': rejected value []; codes [NotEmpty.requestTest202008.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [requestTest202008.name,name]; arguments []; default message [name]]; default message [name은 필수입니다]] [Field error in object 'requestTest202008' on field 'age': rejected value [1]; codes [Min.requestTest202008.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [requestTest202008.age,age]; arguments []; default message [age],10]; default message [age는 10살 이상부터 가능합니다]] ] |
name과 age가 같이 언급되어 있다. 이것을 한번에 보여줄 것인지, 이번 사례처럼 1개만 보여줄 것인지는 프로젝트별로 정하기 나름이다.
그 외에도 다양한 Exception을 캐치하여 정해진 폼으로 전달할 수 있으니 잘 응용하여 사용하길 추천한다.
끝.
함께보면 도움될 글:
https://lemontia.tistory.com/942
'공부 > 프로그래밍' 카테고리의 다른 글
[junit5] Mock을 이용한 단위 테스트 (@InjectMocks 과 @Mock 차이) (0) | 2020.08.13 |
---|---|
[gradle] 외부 jar파일 추가하기 (1) | 2020.08.11 |
[aws] Jenkins + CodeDeploy 로 로드밸런스 환경 자동배포하기 (0) | 2020.08.04 |
[aws] CodeDeploy 를 이용해 로드밸런서 환경에서 배포하기 (0) | 2020.07.23 |
[aws] CodeDeploy 중 BeforeBlockTraffic 진행이 안될 때 (0) | 2020.07.21 |
댓글