티스토리 뷰
@Valid는 자바 표준 진영에서 지원하는 검증 애노테이션이다.
단계별로 검증을 추가하는 식으로 진행해보자
# 검증 1단계
userId = 공백 X, 최대 30자
userPassword = 공백 X, 최대 30자
닉네임 = 공백 X, 최대 15자
이메일 = 공백 X
전화번호 = 공백 X, 최대 15자
@ToString
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserSignUpRequestDto {
@Size(max = 30)
@NotBlank
private String userId;
@Size(max=30)
@NotBlank
private String userPassword;
@Size(max = 15)
@NotBlank
private String nickname;
@NotBlank
private String email;
@Size(max = 15)
@NotBlank
private String phoneNumber;
}
@RequiredArgsConstructor
@RequestMapping("/user")
@Controller
public class UserAccountController {
private final UserAccountService userAccountService;
...
@ResponseBody
@PostMapping("/sign-up")
public ResponseEntity<?> signUp(@Valid UserSignUpRequestDto dto, BindingResult bindingResult) {
userAccountService.create(dto);
return ResponseEntity.ok("OK");
}
}
* @NotBlank는 @NotNull을 포함한다 (null, "", white space 모두 걸러냄)
대충 동작 흐름은 아래와 같다.
0. 클라이언트가 서버로 요청을 보낸다
1. DispatherServlet이 먼저 값을 받는다
2. ArgumentResolver를 이용해 UserSignupResuestDto 파라미터를 처리한다
- 먼저 클라이언트로부터 받은 값을 객체에 바인딩 시킨다.
- 이때 필드의 타입이 맞지 않는다면 typeMismatch를 발생시키고, 해당 필드에 대한 검증은 더이상 수행되지 않는다!
3. 필드가 객체 바인딩에 성공했다면 이제 각 필드에 붙은 애노테이션을 이용해 Validator를 적용한다.
- 검증에 실패했다면 BindingResult에 필드에러를 추가한다
4. BindingResult가 에러를 가지고 있다면 예외를 반환시킨다
- @RequestBody의 경우 MethodArgumentNotValidException을 반환한다.
- @ModelAttribute의 경우 BindException을 반환한다
(MethodArgumentNotValidException은 BindException을 extends 하고있다)
이 예외를 @ExceptionHandler와 @RestControllerAdvice를 이용해 잡아서 규격화된 응답으로 반환해줄 수 있다.
@Builder
@Data
public class ErrorResult {
private String path; // 요쳥 경로
private LocalDateTime timestamp; // 에러 발생 시각
private int status; // 에러 코드
private String error; // 에러명 (ex: Bad Request, Forbidden...)
private List<ErrorDetail> errors; // 각 필드 에러 정보
private String message; // 에러 메세지
}
@Builder
@Data
public class ErrorDetail {
private String field; // 에러 발생 필드 이름
private String code; // 어떤 타입의 에러인지 (ex: Size, NotBlank...)
private String message; // 에러 메세지
public static ErrorDetail from(FieldError e) {
return ErrorDetail.builder()
.field(e.getField())
.code(e.getCode())
.message(e.getMessage())
.build();
}
}
@Slf4j
@RequiredArgsConstructor
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException exception, HttpServletRequest request) {
log.error("[MethodArgumentNotValidException] handle -> {}", exception.getObjectName());
// 필드 에러 정보 모음
List<ErrorDetail> fieldErrorList = exception.getFieldErrors().stream()
.map(error -> ErrorDetail.from(error))
.collect(Collectors.toList());
// 객체 반환
return ErrorResult.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.errors(fieldErrorList)
.message(exception.getMessage())
.path(request.getRequestURI()).build();
}
...
}
그럼 이렇게 응답이 내가 원하는 대로 이쁘게 만들어져서 나온다.
# 참고 사이트
- https://kdhyo98.tistory.com/m/80
- https://jyami.tistory.com/m/55
- https://wildeveloperetrain.tistory.com/m/158
'웹 > Spring' 카테고리의 다른 글
스프링 + Thymeleaf로 게시글 비밀번호 기능 구현하기 (0) | 2022.10.29 |
---|---|
DTO는 대체 어디서 변환하는 것이 좋을까? (2) | 2022.10.27 |
Swagger (Springfox)를 이용해 스프링 프로젝트의 API 문서 만들어보기 (0) | 2022.09.08 |
스프링 부트 | 상대 경로 현재 디렉토리(./)는 어디를 뜻하는 것일까? (1) | 2022.09.06 |
application.properties 설정 파일 분리해보기 (0) | 2022.08.24 |