앞서 배웠던 Validation 과 Exception 을 활용하여, 실무에서 적용되는 모범적인 사례를 살펴보자
계속 발생하는 에러를 뽑고, 뽑은 에러로 메서드 만들어서 에러 처리해보자
전 실습에서 했던 것처럼, 해당 클래스에서 에러 메서드를 처리하면?
우선적으로 에러 메서드를 타기 때문에, 원하는 에러메시지를 출력할 수 없다. 우선 주석처리해두자
@RestController
@RequestMapping("/api/user")
@Validated
public class APIController {
@GetMapping("")
public User get(
// 조건 추가
@Size(min = 1)
@RequestParam String name,
@NotNull
@Min(1)
@RequestParam Integer age) {
User user = new User();
user.setName(name);
user.setAge(age);
// 예외 발생
int a = 10+age;
return user;
}
@PostMapping("")
public User post(@Valid @RequestBody User user) {
System.out.println(user);
return user;
}
/* // 전 시간에 했던 예외 처리 메서드는 주석처리
// 특정 예외를 처리하는 메서드
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e) {
System.out.println("api controller");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}*/
}
@RestControllerAdvice(basePackageClasses = APIController.class)
public class APIControllerAdvice {
// 예외 잡는 메서드
@ExceptionHandler(value = Exception.class) // 모든 예외
public ResponseEntity exception(Exception e) {
System.out.println(e.getClass().getName());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
}
// 특정 예외를 처리하는 메서드
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
// 콘솔창에 친절한 에러 출력
bindingResult.getAllErrors().forEach(error -> {
FieldError fieldError = (FieldError) error;
String fieldName = fieldError.getField();
String message = fieldError.getDefaultMessage();
String value = fieldError.getRejectedValue().toString();
System.out.println(fieldName);
System.out.println(message);
System.out.println(value);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
// 추가적인 에러 처리
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseEntity constraintViolationException(ConstraintViolationException e) {
e.getConstraintViolations().forEach(error ->{
System.out.println(error);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
// 추가적인 에러 처리
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
{
"name" : "",
"age" : 0
}
response body 는 전 예제의 결과와 같고, 콘솔창에도 이제 친절히 설명해준다name
크기가 1에서 10 사이여야 합니다
name
비어 있을 수 없습니다
age
1 이상이어야 합니다
0
ConstraintViolationException
를 추가로 처리@RestControllerAdvice(basePackageClasses = APIController.class)
public class APIControllerAdvice {
.
.
.
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseEntity constraintViolationException(ConstraintViolationException e) {
e.getConstraintViolations().forEach(error ->{
String fieldName = error.getLeafBean().toString();
String message = error.getMessage();
String invalidValue = error.getInvalidValue().toString();
System.out.println("============");
System.out.println(fieldName);
System.out.println(message);
System.out.println(invalidValue);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
에러 처리 메서드마다, 길게 있던 에러 메시지를 각각 추출
@RestControllerAdvice(basePackageClasses = APIController.class)
public class APIControllerAdvice {
. . .
// 특정 예외를 처리하는 메서드
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest httpServletRequest) {
List<Error> errorList = new ArrayList<>();
BindingResult bindingResult = e.getBindingResult();
bindingResult.getAllErrors().forEach(error -> {
FieldError fieldError = (FieldError) error;
String fieldName = fieldError.getField();
String message = fieldError.getDefaultMessage();
String invalidValue = fieldError.getRejectedValue().toString();
Error errorMessage = new Error();
errorMessage.setField(fieldName);
errorMessage.setMessage(message);
errorMessage.setInvalidValue(invalidValue);
errorList.add(errorMessage);
});
// 어떤 요청에 대해 에러 발생 시, 응답 정보
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setErrorList(errorList);
errorResponse.setMessage("");
errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
errorResponse.setResultCode("FAIL");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseEntity constraintViolationException(ConstraintViolationException e, HttpServletRequest httpServletRequest) {
List<Error> errorList = new ArrayList<>();
e.getConstraintViolations().forEach(error ->{
Stream<Path.Node> stream = StreamSupport.stream(error.getPropertyPath().spliterator(), false);
List<Path.Node> list = stream.collect(Collectors.toList());
String fieldName = list.get(list.size() -1).getName();
//String fieldName = error.getPropertyPath().spliterator().;
String message = error.getMessage();
String invalidValue = error.getInvalidValue().toString();
Error errorMessage = new Error();
// 위 코드 동일
errorList.add(errorMessage);
});
// 어떤 요청에 대해 에러 발생 시, 응답 정보
// 위 코드 동일
}
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest httpServletRequest) {
List<Error> errorList = new ArrayList<>();
String fieldName = e.getParameterName();
//String fieldType = e.getParameterType();
String invalidValue = e.getMessage();
Error errorMessage = new Error();
// 위 코드 동일
// 어떤 요청에 대해 에러 발생 시, 응답 정보
// 위 코드 동일
}
}
POST 방식의 브라우저 결과
아주아주 친절하게 response body에 메시지가 담김
{
"name" : "yeppiyeppiyeppiyeppi",
"age" : 11
}
// 응답결과
{
"statusCode": "400 BAD_REQUEST",
"requestUrl": "/api/user",
"code": null,
"message": "",
"resultCode": "FAIL",
"errorList": [
{
"field": "name",
"message": "크기가 1에서 10 사이여야 합니다",
"invalidValue": "yeppiyeppiyeppiyeppi"
},
{
"field": "age",
"message": "1 이상이어야 합니다",
"invalidValue": "0"
}
]
}
클라이언트는 response 만 받아도 에러를 명시적으로 알 수 있음
클라이언트가 서버에 검증을 요청하는 부분을 일관되게 처리한 것
클라이언트는 메시지를 친절하게 받아서 서버를 편리하게 사용할 수 있음
클라이언트의 요청 값도 APIController.java를 통해 @Annotation
를 일관되게 적용하여 관리가 편함
Spring 프레임워크는 엔터프라이즈 프레임워크이다
위 예제처럼
APIController.java
로 컨트롤러는 하나 만들어두고, API는 2개 뿐인데,
굳이 번거롭게 APIControllerAdvice.java
처럼 일일히 다 처리해줄 필요는 없다
컨트롤러에서 if
문같은 분기처리 하는 것이 오히려 효율이 좋다
그런데, 엔터프라이즈는 매우 방대한 데이터를 가지는 서버 프레임워크이기 때문에
API 컨트롤러가 4,5개 이상, 주소가 10개 이상 정도되는 경우에는?
위 예제처럼, 일관되게 적용시키기 위해
Validation
사용, ControllAdvice
로 글로벌 예외처리를 해줘야한다