-
3. 스프링 Custom Validation - 좀 더 친절한 Error Response스프링개발자/203 - Validation 2020. 8. 10. 04:44
[배경]
Validation Error(4XX)에서는 가능한 많은 정보를 제공해주면, client입장에서 감동을 받는다
client request의 어떤 필드 값이 잘못되었는지, 어떻게 되어야 하는지 등등
다음의 트위터 developer docs을 참고해보자
https://developer.twitter.com/en/docs/basics/response-codes
1. 에러메세지와 에러코드
트위터에서 발췌
우리는 message와 code이외에 value를 넣도록 한다
2. ValidationException을 정의하자
RunTimeException와 비슷한, 하지만 우리만의 Validation Response Object를 return하기 위한 예외를 정의하자
@ResponseStatus를 이용하여 HttpStatus 400에러가 나오면 ValidationException이 발생되도록 하자
tracking 하는 값으로는 이전 글에서 만든 ValidationErrorResponse를 쓴다
상황에 맞게 몇가지 constructor를 만든다.
package me.ndPrince.routeOptimization.exception; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; import java.util.List; @ResponseStatus(code = HttpStatus.BAD_REQUEST) @Getter @Slf4j public class ValidationException extends RuntimeException { private List<ValidationErrorResponse> validationErrors; public ValidationException(String message) { super(message); } public ValidationException(List<ValidationErrorResponse> errors, String requestId){ this.validationErrors = errors; log.warn("The following request {} has validation exceptions", requestId); } }
3. ValidationAdvisor를 만들자
앞의 글에서 만든 ControllerAdvice 어노테이션을 쓰는 클래스를 만들자
package me.ndPrince.routeOptimization.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice public class ValidationExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(ValidationException.class) @ResponseBody public ValidationErrorResponse handleValidationExceptions(ValidationException e) { return ValidationErrorResponse.builder() .build(); } }
두가지의 다른점은,
1) @ExceptionHandler 어노테이션을 이용하여 ValidationException클래스를 ExceptionHandler로 받는 다는 점
2) RunTimeException의 하나인 ValidationException을 argument로 받는다는 점
4. ValidationErrorResponse에 살을 붙이자
이전 글에서 만든 VadliationErrorResponse는 위와 같이 단순하다.
글 도입부의 배경에서 보았던 트위터 예제처럼, 우리도 ErrorMessage와 ErrorCode를 넣자
결과물은 다음과 같다.
package me.ndPrince.routeOptimization.exception; import lombok.Builder; import lombok.Data; import me.ndPrince.routeOptimization.model.ValidationError; // 이전 글에서는 스프링에서 제공하는 ValidationErrors를 썼고 여기는 우리가 만든 ValidationError이다 import java.util.List; @Builder @Data public class ValidationErrorResponse { private String timestamp; private Integer status; private List<ValidationError> validationErrorsList; }
ValidationError 컨테이너 클래스에 넣고 싶은 만큼 정보를 넣자.
package me.ndPrince.routeOptimization.model; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public class ValidationError { private final Type type; private final String field; private final String message; private final Integer errorCode; public enum Type{ SPRING_VALIDATION, BAD_RIDE, BAD_VEHICLE } }
5. ValidationError를 실제로 불러오는 로직을 구현한다 (총 3가지 클래스를 구현한다)
먼저는 DispatchController를 다음과 같이 바꾼다.
Service를 하나 추가해서 Service에 Validate 로직이 있다. 리턴 로직은 아직 손대지 않았다.
package me.ndPrince.routeOptimization; import me.ndPrince.routeOptimization.model.DispatchRequest; import me.ndPrince.routeOptimization.model.DispatchResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @RestController @RequestMapping(value = "/dispatch") public class DispatchController { @Autowired private DispatchService dispatchService; @PostMapping public DispatchResponse postDispatchRequest(@Valid @RequestBody DispatchRequest request){ dispatchService.validate(request); return new DispatchResponse(); } }
DispatchService 클래스는 다음과 같다
package me.ndPrince.routeOptimization; import me.ndPrince.routeOptimization.exception.ValidationException; import me.ndPrince.routeOptimization.model.DispatchRequest; import me.ndPrince.routeOptimization.model.ValidationError; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @Service public class DispatchService { public void validate(DispatchRequest request) { List<ValidationError> errorList = new ArrayList<>(); request.getRides().stream() .flatMap(r -> Stream.of(r.getPickup(), r.getDropOff())) .collect(Collectors.toSet()) .forEach(location -> validateLocations(location, errorList)); if(!errorList.isEmpty()){ throw new ValidationException(errorList, request.getId()); } } public void validateLocations(Location location, List<ValidationError> errors){ Double longitude = location.getLongitude(); Double latitude = location.getLatitude(); boolean isLonValid = (-180d <= longitude) && ( longitude <= 180d); boolean isLatValid = (-90d <= latitude) && ( latitude <= 90d); if(!isLatValid) errors.add(new ValidationError(ValidationError.Type.BAD_RIDE, "Location", "Invalid Lat Location. Check your Lat", 1)); if(!isLonValid) errors.add(new ValidationError(ValidationError.Type.BAD_RIDE, "Location", "Invalid Lon Location. Check your Lon", 1)); } }
일단은 Ride에 있는 pickup 과 dropOff Location의 coordinate만 검사하는 로직이다. 향후에 추가 가능하도록 코드를 깔끔하게 짰다. 향후에 stream관련해서 포스팅을 해야겠다.
그리고 마지막으로 DispatchRequest에 id값을 추가했다.
package me.ndPrince.routeOptimization.model; import lombok.Builder; import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.util.List; @Data @Builder public class DispatchRequest { @Builder.Default @NotNull protected String id; @NotNull(message = "this field must not be null") @Valid protected List<RideRequest> rides; protected List<VehicleRequest> vehicles; }
6. Postman 검증
다음의 json request를 보내보자
{ "id": "1", "rides": [ { "rideId": "1", "pickup": { "latitude": 0.9, "longitude": 0.2 }, "dropOff": { "latitude": 0.1, "longitude": 0.5 }, "capacity": 5 } ] }
json request에 에러가 있는 값을 전달한다
{ "id": "1", "rides": [ { "rideId": "1", "pickup": { "latitude": -999.9, "longitude": 0.2 }, "dropOff": { "latitude": 0.1, "longitude": 535.5 }, "capacity": 5 } ] }
최대한 많은 정보를 담은 친절한 Error Response가 탄생되었다~
심심하신 분들은 coordiate 에러가 pickup에서 오는 것인지, dropOff에서 오는 것인지 구분하는 로직을 구현해보자.
소스코드
https://github.com/2ndPrince/routeOptimization/tree/spring_validation_3
5편으로 계획했던 스프링개발자103 Validation 시리즈가 생각보다 일찍 끝났다. 3,4,5편을 여기에 한꺼번에 넣다보니, 생각보다 복잡하지 않아서이다. 여튼 Validation 시리즈 끝!
----------------------
[내용 추가]
validationErrorsList에 value를 넣지 않았다.
value가 있으면, client 입장에서는 request의 무엇이 잘못되었는지 직관적으로 알 수 있다. 다음과 같이 수정한다.
String value로 하는 것이 어떤 데이터 타입의 값도 쉽게 String으로 변환가능해서 편하다.
public void validateLocations(Location location, List<ValidationError> errors){ Double longitude = location.getLongitude(); Double latitude = location.getLatitude(); boolean isLonValid = (-180d <= longitude) && ( longitude <= 180d); boolean isLatValid = (-90d <= latitude) && ( latitude <= 90d); if(!isLatValid) errors.add(new ValidationError( ValidationError.Type.BAD_RIDE, "Location", "Invalid Lat Location. Check your Lat", 1, latitude.toString())); if(!isLonValid) errors.add(new ValidationError( ValidationError.Type.BAD_RIDE, "Location", "Invalid Lon Location. Check your Lon", 1, longitude.toString())); }
validate method또한 위처럼 수정해준뒤, postman확인
value값이 보인다. 추가 끝.
https://github.com/2ndPrince/routeOptimization/tree/spring_validation_3_value_added
'스프링개발자 > 203 - Validation' 카테고리의 다른 글
2. 스프링 Validation with custom message and custom response container (0) 2020.08.10 1. 스프링 Validation - 가장 기본뼈대 (0) 2020.08.09 스프링개발자 203 - Validation (0) 2020.08.09