지금까지 했던 내용은 서버에서는 MethodArgumentNotValidException 이 발생되어 확인할 수 있지만,
클라이언트에서는 오류 사항을 확인할 수 없다.
에러 사항을 클라이언트에서도 확인 시키기 위해 작업해보자.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Api<T>{
private String resultCode;
private String resultMessage;
private T data;
private Error error;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
private static class Error{
private List<String> errorMessage;
}
}
를 작성해주고,
기존 컨트롤러에 Api를 통해서 요청할 수 있도록 코드를 추가하자.
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserApiController {
@PostMapping("")
public Api<UserRegisterRequest> register(
@Valid
@RequestBody
Api<UserRegisterRequest> userRegisterRequest
){
log.info("init: {} ", userRegisterRequest);
return userRegisterRequest;
}
}
이제 서버는 Api라는 스펙을 통해서만 json을 보낼 수 있다.
{
"resultCode": "",
"resultMessage": "",
"data": {
"name": "냥냥이",
"age": 20,
"password": "1111",
"email": "meow@gmail.com",
"phone_number": "123-456-7890",
"register_at": "2025-01-01T13:00:00"
},
"error": {
"errorMessage": []
}
}
이렇게 하고 send하게 되면

200OK가 뜨는데,
여기서 한 번 phone_number 에 있는 - 을 없애보자

양식에 맞지 않는데 에러가 뜨지 않고 잘 내려온다.
이유는 private T data; 여기 data라는 부분이 검증이 안 되어 있기 때문이다.
즉, null값 이어도 되지만 값이 있을 때는 valid라는 annotation을 붙여서 검증을 하겠다고 지정을 해줘야 controller에서 valid가 붙어서, api라는 객체를 검증을 할 때 해당 부분에 valid가 있다면 해당 generic type에 대해서도 validation이 진행되는 것이다.
@Valid
private T data;
이렇게 수정하고 send해보자.

이번엔 에러가 내려왔다. 다시 정확한 규격대로 해보면
정상적인 메시지가 내려온다.

이젠 가공 된 메시지를 출력해보자
api.UserRequest builder generic type을 지정해주고 그 builder를 통해서 data build해서 해당 response를 내린다.
resultcode는 http status의 ok의 value를 넣고 http status ok의 이유를 넣는다.
var body = userRegisterRequest.getData();
Api<UserRegisterRequest> response = Api.<UserRegisterRequest>builder()
.resultCode(String.valueOf(HttpStatus.OK.value()))
.resultMessage(HttpStatus.OK.getReasonPhrase())
.data(body)
.build();
return response;

이젠 OK가 떨어지는 모습
snakecase 적용하는걸 까먹었으니 슬쩍 넣어준다.
리턴할 body가 없기 때문에 Object로 return 한다.
그럼 아래 return에 다 오류가 생기는데,
이 때 generic에 어떤 type인지는 모르겠지만 object를 상속받은 애를 리턴시킬 것이다 라는 의미의 ? extends를 넣어준다. 모든 object 즉, 클래스들은 object를 상속받기 때문에 모든 값을 generic type으로 리턴할 수 있게 된다.
binding result를 통해서 error가 있는지 검사를 한다. error를 가지고 있다면 binding result에 error가 난 field들을 stream을 통해서 가지고 온다.
map 함수로 값을 각각 하나씩 변환하겠다고 하고 format에는 문자열을 넣고 어떠한 값은 이러한 이유로 reject 당했다 라는 메시지를 리스트로 만들 것이다.
그리고 errorMessage를 넣고, errorResponse를 return하는 ApiError에 들어갈 error를 만든다.
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserApiController {
@PostMapping("")
public Api<? extends Object> register(
@Valid
@RequestBody
Api<UserRegisterRequest> userRegisterRequest,
BindingResult bindingResult
){
log.info("init: {} ", userRegisterRequest);
if (bindingResult.hasErrors()) {
var errorMessageList = bindingResult.getFieldErrors().stream()
.map(it -> {
var format = "%s : { %s } 은 %s";
var message = String.format(format, it.getField(), it.getArguments(), it.getDefaultMessage());
return message;
}).collect(Collectors.toList());
var error = Api.Error
.builder()
.errorMessage(errorMessageList)
.build();
var errorResponse = Api.<UserRegisterRequest>builder()
.resultCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
.resultMessage(HttpStatus.BAD_REQUEST.getReasonPhrase())
.error(error)
.build();
return errorResponse;
}
var body = userRegisterRequest.getData();
Api<UserRegisterRequest> response = Api.<UserRegisterRequest>builder()
.resultCode(String.valueOf(HttpStatus.OK.value()))
.resultMessage(HttpStatus.OK.getReasonPhrase())
.data(body)
.build();
return response;
}
}
이름을 비우고 전화번호에 -을 지워서 send해보자

오류 사항이 뜨긴 하는데 200OK가 뜬다..
exception handler로 일부를 옮겨보자
@Slf4j
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public ResponseEntity<Api> validationException(
MethodArgumentNotValidException exception
){
log.error("", exception);
var errorMessageList = exception.getBindingResult().getFieldErrors().stream()
.map(it -> {
var format = "%s : { %s } 은 %s";
var message = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());
return message;
}).collect(Collectors.toList());
var error = Api.Error
.builder()
.errorMessage(errorMessageList)
.build();
var errorResponse = Api
.builder()
.resultCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
.resultMessage(HttpStatus.BAD_REQUEST.getReasonPhrase())
.error(error)
.build();
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}
}
이렇게 작성하고
{
"resultCode": "",
"resultMessage": "",
"data": {
"name": "",
"age": 20,
"password": "1111",
"email": "meow@gmail.com",
"phone_number": "1234567890",
"register_at": "2025-01-01T13:00:00"
},
"error": {
"errorMessage": []
}
}
을 send하면
400에러가 뜬다

예외를 따로 exception handler로 빼게 되면 해당 비즈니스 로직을 처리하는 곳에서는 요청이 들어왔다는 것은 안에 있는 값들은 유효하다는 것이니 비즈니스 로직만 처리하고 해당 값을 리턴해주면 된다.