[SpringBoot] Spring Validation을 이용한 유효성 검증

나르·2021년 11월 7일
11

Spring

목록 보기
4/25
post-thumbnail

✅ Validation

올바르지 않은 데이터를 걸러내고 보안을 유지하기 위해 데이터 검증(validation)은 여러 계층에 걸쳐서 적용됩니다.
Client의 데이터는 조작이 쉬울 뿐더러 모든 데이터가 정상적인 방식으로 들어오는 것도 아니기 때문에, Client Side뿐만 아니라 Server Side에서도 데이터 유효성을 검사해야 할 필요가 있습니다.
스프링부트 프로젝트에서는 @validated를 이용해 유효성을 검증할 수 있습니다.

👏 Bean Validation

스프링의 기본적인 validationBean validation은 클래스 "필드"에 특정 annotation을 적용하여 필드가 갖는 제약 조건을 정의하는 구조로 이루어진 검사입니다.
validator가 어떠한 비즈니스적 로직에 대한 검증이 아닌, 그 클래스로 생성된 객체 자체의 필드에 대한 유효성 여부를 검증합니다.

❗️ @Valid, @Validated 차이
@Valid는 Java 에서 지원해주는 어노테이션이고 @Validated는 Spring에서 지원해주는 어노테이션입니다. @Validated@Valid의 기능을 포함하고, 유효성을 검토할 그룹을 지정할 수 있는 기능을 추가로 가지고 있습니다.

🌱 Spring Boot Validation 적용하기

1. 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>

2. Contoller에서 유효성 검사를 적용할 API의 Request객체 앞에 @validated 어노테이션을 추가합니다.

userController.java

    @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()))
        );

    }

ErrorResponse.java

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;
    }
}

@Validated로 검증한 객체가 유효하지 않은 객체라면 Controller의 메서드의 파라미터로 있는 BindingResult 인터페이스를 확장한 객체로 들어옵니다.
때문에 bindingResult.hasError() 메서드는 유효성 검사에 실패했을 때 true를 반환합니다.

3. Request를 핸들링할 객체를 정의할 때 Validation 어노테이션을 통해 필요한 유효성 검사를 적용합니다.

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();
    }
}

유효성 검사에 적용할 수 있는 어노테이션은 다음과 같습니다.

@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은 체크하지 않습니다.

해당 기능들에 대한 더 세부적인 내용은 이 사이트를 참고.

🛠 Exception Handling

위에 적용한 것 처럼 에러를 처리하는 객체를 따로 생성해 가공하는 것 외에도,
@ExceptionHandler 어노테이션을 이용해 예외를 처리할 수 있습니다.

example1

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public Object exception(Exception e) {
	return e.getMessage(); 
}

example2

@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    log.error("MethodArgumentNotValidException : " + e.getMessage());
    final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
    return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}    

Test Code 작성

import com.springboot.server.controller.user.UserCreateRequestDto;
import org.junit.jupiter.api.*;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;


public class UserValidationTest {
    private static ValidatorFactory factory;
    private static Validator validator;

    @BeforeAll
    public static void init() {
        factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @AfterAll
    public static void close() {
        factory.close();
    }

    @DisplayName("빈문자열 전송 시 에러 발생")
    @Test
    void blank_validation_test() {
        // given
        UserCreateRequestDto userCreateRequestDto = new UserCreateRequestDto("", "test","email@naver.com");
        // when
        Set<ConstraintViolation<UserCreateRequestDto>> violations = validator.validate(userCreateRequestDto); // 유효하지 않은 경우 violations 값을 가지고 있다.
        // then
        assertThat(violations).isNotEmpty();
        violations
                .forEach(error -> {
                    assertThat(error.getMessage()).isEqualTo("NAME_IS_MANDATORY");
                });
    }

    @DisplayName("이메일 형식 아닌 경우 에러 발생")
    @Test
    void email_validation_test() {
        // given
        UserCreateRequestDto userCreateRequestDto = new UserCreateRequestDto("name", "test","email");
        // when
        Set<ConstraintViolation<UserCreateRequestDto>> violations = validator.validate(userCreateRequestDto);
        // then
        assertThat(violations).isNotEmpty();
        violations
                .forEach(error -> {
                    assertThat(error.getMessage()).isEqualTo("NOT_VALID_EMAIL");
                });
    }

    @DisplayName("유효성 검사 성공")
    @Test
    void validation_success_test() {
        // given
        UserCreateRequestDto userCreateRequestDto = new UserCreateRequestDto("name", "test","email@naver.com");
        // when
        Set<ConstraintViolation<UserCreateRequestDto>> violations = validator.validate(userCreateRequestDto);
        // then
        assertThat(violations).isEmpty(); // 유효한 경우
    }
}

hamcrest.assertThat을 사용하면 violations를 찾지 못해서 assertj.assertThat으로 처리하긴 했는데...이유는 더 찾아봐야겠다.
->hamcrestviolationMatchers로 따로 구현돼있는 것 같습니다.

📖 Reference

Valid
Valid2
Valid3
Valid4
bindingresult casting error
return validation error binding result with response entity
spring validation
hamcrest violation matchers

Todo

  • custom exception
  • binding result casting error
profile
💻 + ☕ = </>

3개의 댓글

comment-user-thumbnail
2022년 5월 13일

내 호기심을 공유하는 다른 사람들을 발견하게 되어 기쁩니다. 그리고 좋은 소식은 이 질문이 이미 이 포럼에서 다루어졌다는 것입니다. Paper Minecraft 을 플레이하고 더 연결합시다!

답글 달기
comment-user-thumbnail
2022년 5월 25일

정리가 깔끔해서 이해하기 쉬웠습니다. 감사해요 :)

답글 달기
comment-user-thumbnail
2022년 9월 14일

In order to write a compelling article or story, the author needs to have sharp senses and eyes, and they need to examine the topic in great detail. Your article has left a very favorable impression on me. crossword puzzles

답글 달기