Validation의 필요성?
일반적인 애플리케이션에서 데이터 유효성 검사 로직은 다음과 같은 문제를 가지고 있다.
- 애플리케이션 전체에 분산되어 있다.
- 코드 중복이 심하다.
- 비즈니스 로직에 섞여있어 검사 로직 추척이 어렵고 애플리케이션이 복잡해진다.
이러한 문제 때문에 데이터 유효성 검사 로직에 기능을 추가, 수정하기 어렵고, 오류가 발생할 가능성도 크다.
Bean Validation
Bean Validation은 위에서 말한 문제들을 해결하기 위해 다양한 제약(Contraint)을 도메인 모델(Domain Model)에 어노테이션(Annotation)로 정의할 수 있게한다. 이 제약을 유효성 검사가 필요한 객체에 직접 정의하는 방법으로 기존 유효성 검사 로직의 문제를 해결한다.
Validation
올바르지 않은 데이터를 걸러내고 보안을 유지하기 위해 데이터 검증(validation)은 여러 계층에 걸쳐서 적용된다.
Client의 데이터는 조작이 쉬울 뿐더러 모든 데이터가 정상적인 방식으로 들어오는 것도 아니기 때문에, Client Side뿐만 아니라 Server Side에서도 데이터 유효성을 검사해야 할 필요가 있다.스프링부트 프로젝트에서는 @validated를 이용해 유효성을 검증할 수 있다
Dependency
라이브러리 추가
implementation("org.springframework.boot:spring-boot-starter-validation")
@Valid, @Validated 차이
@Valid는 Java 에서 지원해주는 어노테이션이고 @Validated는 Spring에서 지원해주는 어노테이션
// groups를 설정 불가
@Valid
private final ProductRequest request
@Validated는 @Valid의 기능을 포함하고, 유효성을 검토할 그룹을 지정할 수 있는 기능을 추가로 가지고 있다
// groups를 설정 가능
@Validated(value = { Create.class, Update.class })
private final ProductRequest request
Validation 적용
1. Contoller에서 유효성 검사
- @RequestBody로 전달받은 값에 대해서도 검증이 가능
@RequestBody 애노테이션을 설정하면 RequestResponseBodyMethodProcessor를 통해서 메서드 파라미터가 바인딩 됩니다. 그리고 @Validated이나 @Valid 애노테이션을 같이 설정하면 Valiation도 함께 처리
@PostMapping
public ResponseEntity<?> createUSer(@Validated @RequestBody final UserCreateRequestDto userCreateRequestDto, BindingResult bindingResult){
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).collect(Collectors.toList());
// 200 response with 404 status code
return ResponseEntity.ok(new ErrorResponse("404", "Validation failure", errors));
// or 404 request
// return ResponseEntity.badRequest().body(new ErrorResponse("404", "Validation failure", errors));
}
try {
final User user = userService.searchUser(userCreateRequestDto.toEntity().getId());
}catch (Exception e){
return ResponseEntity.ok(
new UserResponseDto(userService.createUser(userCreateRequestDto.toEntity()))
);
}
// user already exist
return ResponseEntity.ok(
new UserResponseDto(userService.searchUser(userCreateRequestDto.toEntity().getId()))
);
}
- @Validated로 검증한 객체가 유효하지 않은 객체라면 Controller의 메서드의 파라미터로 있는 BindingResult 인터페이스를 확장한 객체로 들어온다
그렇기 때문에 bindingResult.hasError() 메서드는 유효성 검사에 실패했을 때 true를 반환한다.
package com.springboot.server.common;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class ErrorResponse {
private String statusCode;
private String errorContent;
private List<String> messages;
public ErrorResponse(String statusCode, String errorContent, String messages) {
this.statusCode = statusCode;
this.errorContent = errorContent;
this.messages = new ArrayList<>();
this.messages.add(messages);
}
public ErrorResponse(String statusCode, String errorContent, List<String> messages) {
this.statusCode = statusCode;
this.errorContent = errorContent;
this.messages = messages;
}
}
- Spring 내부에서 생성된 프록시 객체를 통해서 @RquestParam과 @PathVariable에 바로 Validation 애노테이션을 적용할 수 있다
메서드 파라미터나 리턴 값을 검증하기 위해서는 클래스에 @Validated을 적용하면
MethodValidationPostProcessor에 의해서 Validation이 가능하도록 프록시 객체가 생성
@Validated
@RestController
public class ProductRestController {
@GetMapping(value = "/products")
public ResponseEntity<Void> search(
@Min(1) @RequestParam(value = "page") int page,
@Min(1) @Max(100) @RequestParam(value = "size") int size,
@Range(min = 1, max = 10) @RequestParam(value = "keyword") String keyword) {
// page는 1보다 크고 size는 1~100 사이
// keyword는 글자수 1~10 사이
return ResponseEntity.noContent().build();
}
@GetMapping(value = "/products/{productNo}")
public ResponseEntity<Void> getProduct(
@Min(1) @PathVariable("productNo") int productNo) {
// productNo는 최소 1이상
return ResponseEntity.noContent().build();
}
}
Validation 어노테이션 종류(유효성검사)
@Null // null만 혀용합니다.
@NotNull // null을 허용하지 않습니다. "", " "는 허용합니다.
@NotEmpty // null, ""을 허용하지 않습니다. " "는 허용합니다.
@NotBlank // null, "", " " 모두 허용하지 않습니다.
@Email // 이메일 형식을 검사합니다. 다만 ""의 경우를 통과 시킵니다
@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은 체크하지 않습니다.
2. Entity 객체, Dto 객체같은 핸들링이 필요한 곳에서 유효성 검사를 적용한다.
import lombok.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Getter
@Builder
@NoArgsConstructor
public class UserCreateRequestDto {
@NotBlank(message="NAME_IS_MANDATORY")
private String name;
@NotBlank(message="PASSWORD_IS_MANDATORY")
private String password;
@Email(message = "NOT_VALID_EMAIL")
private String email;
public User toEntity(){
return User.builder()
.user_name(name)
.email(email)
.password(password)
.build();
}
}
위에 적용한 것 처럼 에러를 처리하는 객체를 따로 생성해 가공하는 것 외에도 @ExceptionHandler 어노테이션을 이용해 예외를 처리할 수 있습니다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public Object exception(Exception e) {
return e.getMessage();
}
이외에 BindingResult, MessageCodesResolver 등의 개념이 있고 위에 언급한 @ExceptionHandler 도 클래스를 지정해주거나 먼저 순환되는 순서도 정할 수 있는데 그 방법들은 실제로 많이 사용해봐야 감이 올 것 같다.
검증과 예외처리를 어떻게 할 수 있냐에 따라서 사용자에게 보여주어야하는 정보나 오류 메시지를 정확하게 전달하거나 숨길 수도 있는 것 같다. 공부하자!!
References:
https://meetup.toast.com/posts/223
'프로그래밍 > Spring' 카테고리의 다른 글
첫 미니프로젝트 종료 (0) | 2022.08.18 |
---|---|
ORM, Hibernate, JPA (0) | 2022.08.07 |
AOP & Logging (slf4j) (0) | 2022.08.05 |
[SPRING BOOT]JWT, Thymeleaf, form (2/2) (0) | 2022.08.04 |
[SPRING BOOT]JWT, Thymleaf, form (1/2) (2) | 2022.08.04 |