Validation
올바르지 않은 데이터를 걸러내고 보안을 유지하기 위해 데이터 검증(validation)은 여러 계층에 걸쳐서 적용된다.
Client의 데이터는 조작이 쉽고, 모든 데이터가 정상적인 방식으로 들어오는 것이 아니기 때문에, client side 뿐만 아니라 server side에서도 데이터 유효성을 검사해야 할 필요가 있다.
Spring Boot에서는 @Validated를 이용해 유효성을 검증할 수 있다.
Bean Validation
Spring의 기본적인 validation인 bean validation은 클래스 필드에 특정 annotation을 적용하여 필드가 갖는 제약 조건을 정의하는 구조로 이루어진 검사다.
validator가 어떠한 비즈니스적 로직에 대한 검증이 아닌, 그 클래스로 생성된 객체 자체의 필드에 대한 유효성 여부를 검증한다.
@Valid, @Validated의 차이
@Valid는 Java에서 지원해주는 어노테이션이고, @Validated는 Spring에서 지원해주는 어노테이션이다.
@Validated는 @Valid의 기능을 포함하고, 유효성을 검토할 그룹을 지정할 수 있는 기능을 추가로 가지고 있다.
Spring Boot 의존성 추가
Spring Boot 2.3 version 이상부터는 spring-boot-starter-web 의존성 내부에 있던 validation이 사라졌기 때문에 의존성을 따로 추가해주어야 한다.
- Gradle
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.5.6'
- Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.5.6</version>
</dependency>
Controller에서 유효성 검사를 적용할 API의 Request 객체 앞에 @Valid 어노테이션을 추가한다.
@PostMapping("/user/add")
public ResponseEntity<Void> addUser(@RequestBody @Valid AddUserRequest addUserRequest) {
...
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserRequestDto {
@NotBlank(message = "아이디는 필수 입력 값입니다.")
private String username;
@NotBlank(message = "비밀번호는 필수 입력 값입니다.")
@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,16}", message = "비밀번호는 8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.")
private String password;
@Pattern(regexp = "^[ㄱ-ㅎ가-힣a-z0-9-_]{2,10}$", message = "닉네임은 특수문자를 제외한 2~10자리여야 합니다.")
private String nickname;
@Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,6}$", message = "이메일 형식이 올바르지 않습니다.")
@NotBlank(message = "이메일은 필수 입력 값입니다.")
private String email;
...
}
Validation 어노테이션 종류
@Null // null만 혀용합니다.
@NotNull // null을 허용하지 않습니다. "", " "는 허용합니다.
@NotEmpty // null, ""을 허용하지 않습니다. " "는 허용합니다.
@NotBlank // null, "", " " 모두 허용하지 않습니다.
@Email // 이메일 형식을 검사합니다. 다만 ""의 경우를 통과 시킵니다. @Email 보다 아래 나올 @Patten을 통한 정규식 검사를 더 많이 사용합니다.
@Pattern(regexp = ) // 정규식을 검사할 때 사용됩니다.
@Size(min=, max=) // 길이를 제한할 때 사용됩니다.
@Max(value = ) // value 이하의 값을 받을 때 사용됩니다.
@Min(value = ) // value 이상의 값을 받을 때 사용됩니다.
@Positive // 값을 양수로 제한합니다.
@PositiveOrZero // 값을 양수와 0만 가능하도록 제한합니다.
@Negative // 값을 음수로 제한합니다.
@NegativeOrZero // 값을 음수와 0만 가능하도록 제한합니다.
@Future // 현재보다 미래
@Past // 현재보다 과거
@AssertFalse // false 여부, null은 체크하지 않습니다.
@AssertTrue // true 여부, null은 체크하지 않습니다.
Errors
Controller Request 객체 앞에 @Valid 어노테이션을 사용하고 Errors를 통해 유효성 적합 여부를 확인한다.
* 이때 Errors는 반드시 Request 객체 바로 뒤에 위치해야 한다.
(두 개의 객체에 validation 검사를 한다면, 각각 객체 바로 뒤에 Errors를 받도록 한다.)
그리고 hasErrors() 메서드를 통해 Request 객체에 설정한 유효성 검사에 문제가 있는지 확인하고, 문제가 있으면 원하는 데이터 형식으로 가공해서 사용하면 된다.
/* 회원가입 */
@PostMapping("/auth/joinProc")
public String joinProc(@Valid UserRequestDto userDto, Errors errors, Model model) {
if (errors.hasErrors()) {
/* 회원가입 실패시 입력 데이터 값을 유지 */
model.addAttribute("userDto", userDto);
/* 유효성 통과 못한 필드와 메시지를 핸들링 */
Map<String, String> validatorResult = userService.validateHandling(errors);
for (String key : validatorResult.keySet()) {
model.addAttribute(key, validatorResult.get(key));
}
/* 회원가입 페이지로 다시 리턴 */
return "/user/user-join";
}
userService.userJoin(userDto);
return "redirect:/auth/login";
}
[Valid의 동작 원리]
모든 요청은 프론트 Controller인 디스패처 서블릿을 통해 Controller로 전달된다. 전달 과정에서는 Controller 메서드의 객체를 만들어주는 ArgumentResolver가 동작하는데, @Valid 역시 ArgumentResolver에 의해 처리가 된다.
대표적으로 @RequestBody는 Json 메세지를 객체로 변환해주는 작업이 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 처리하며, 이 내부에서 @Valid로 시작하는 어노테이션이 있을 경우 유효성 검사를 진행한다. (이러한 이유로 @Valid가 아니라 커스텀 어노테이션인 @Validddddd여도 동작한다.) 만약 @ModelAttribute를 사용중이라면 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다.
그리고 검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생하게 되고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러가 발생한다.
이러한 이유로 @Vaild는 기본적으로 Controller에서만 동작하며, 다른 계층에서는 검증되지 않는다.
다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합되어야 한다.
[Validated]
입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋다. 하지만 개발을 하다보면 불가피하게 다른 곳에서 파라미터를 검증해야 할 수 있다. Spring에서는 이를 위해 AOP 기반으로 메소드의 요청을 가로채서 유효성 검증을 진행해주는 @Validated를 제공하고 있다. @Validated는 JSR 표준 기술이 아니며 Spring 프레임워크에서 제공하는 어노테이션 및 기능이다.
다음과 같이 클래스에 @Validated를 붙여주고, 유효성을 검증할 메소드의 파라미터에 @Valid를 붙여주면 유효성 검증이 진행된다.
@Service
@Validated
public class UserService {
public void addUser(@Valid AddUserRequest addUserRequest) {
...
}
}
유효성 검증에 실패하면 에러가 발생하는데, 로그를 확인해보면 이전의 MethodArgumentNotValidException 예외가 아닌 ConstraintViolationException 예외가 발생했다.
특정 ArgumnetResolver에 의해 유효성 검사가 진행되었던 @Valid와 달리, @Validated는 AOP 기반으로 메소드 요청을 인터셉터하여 처리된다. @Validated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 AOP의 어드바이스 또는 인터셉터(MethodValidationInterceptor)가 등록된다. 그리고 해당 클래스의 메소드들이 호출될 때 AOP의 포인트 컷으로써 요청을 가로채서 유효성 검증을 진행한다.
이러한 이유로 @Validated를 사용하면 컨트롤러, 서비스, 레포지토리 등 계층에 무관하게 스프링 빈이라면 유효성 검증을 진행할 수 있다. 대신 클래스에는 유효성 검증 AOP가 적용되도록 @Validated를, 검증을 진행할 메소드에는 @Valid를 선언해주어야 한다.
이러한 이유로 @Valid에 의한 예외는 MethodArgumentNotValidException이며, @Validated에 의한 예외는 ConstraintViolationException이다.
출처 : https://mangkyu.tistory.com/174