ํ‹ฐ์Šคํ† ๋ฆฌ ๋ทฐ

Spring

Spring Validation

mokhs 2021. 7. 25. 04:15

๊ฐœ์š”

Spring-validation์— ๋Œ€ํ•ด์„œ ํ•™์Šตํ•˜๊ณ  ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค.

ํ•ด๋‹น ๊ธ€์€ spring-boot 2.5.2 ์—์„œ ์ง„ํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Spring Validation์€ ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๊ฐ„ํŽธํ•˜๊ฒŒ ํŠน์ • ๊ฐ’์„ validationํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

Gradle

spring validation ์‚ฌ์šฉ์„ ์œ„ํ•ด์„œ ๊ทธ๋ž˜์ด๋“ค ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

dependencies {
        ...
    implementation 'org.springframework.boot:spring-boot-starter-validation'
        ...
}

์˜ˆ์‹œ

spring-validation์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐ๊ฐ์— ๋งž๋Š” Annotation์„ ๋ถ™์—ฌ์คŒ์œผ๋กœ์จ ๊ฐ’์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋จผ์ € ์•„๋ž˜ ์˜ˆ์‹œ ์ฝ”๋“œ๋ฅผ ๋ณด๊ณ  ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๊ฐ์„ ์žก์•„๋ณด์„ธ์š”.
์ž‘๋ช…์ด ์ž˜ ๋˜์–ด์žˆ์–ด์„œ ์–ด๋Š์ •๋„ ์œ ์ถ”ํ•  ์ˆ˜ ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค!

์ดํ›„ ๋‚ด์šฉ์—์„œ ๊ด€๋ จ Annotation์— ๋Œ€ํ•ด ๋‹ค๋ฃน๋‹ˆ๋‹ค.

// Api.java

@GetMapping
public User get(@RequestParam @NotBlank String name,
                @RequestParam @Min(value = 1, message = "age๋Š” 1์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") Integer age) {
    User user = new User();
    user.setName(name);
    user.setAge(age);

    return user;
}

@PostMapping
public User post(@RequestBody @Valid UserDto user) {
    System.out.println(user);

    return user;
}

// UserDto.java

public class UserDto {

    @Size(min = 1, max = 10) // ๊ธธ์ด
    private String name;

    @NotNull // null ๋ถˆ๊ฐ€๋Šฅ
    @Min(1) // 1 ์ด์ƒ
    private Integer age;

        public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

}

๊ด€๋ จ Annotation

  • @Valid : ํ•ด๋‹น object์— validation์„ ํ•˜๊ฒ ๋‹ค๋Š” ํ‘œ์‹œ
  • @NotNull : null ๋ถˆ๊ฐ€๋Šฅ
  • @NotEmpty : null, ""(๊ณต๋ฐฑ) ๋ถˆ๊ฐ€๋Šฅ
  • @NotBlank : null, "", " " ๋ถˆ๊ฐ€๋Šฅ
  • @Pattern : ์ •๊ทœ์‹์„ ์ด์šฉํ•ด์„œ ๊ฒ€์ฆ ๊ฐ€๋Šฅ
  • @Size : ๋ฌธ์ž ๊ธธ์ด ๊ฒ€์ฆ, ์ˆซ์ž type์€ ๋ถˆ๊ฐ€๋Šฅ ex) @Size(min = 0, max = 11)
  • @Max : ์ตœ๋Œ€๊ฐ’ ๊ฒ€์ฆ
  • @Min : ์ตœ์†Œ๊ฐ’ ๊ฒ€์ฆ
  • @AssertTrue / @AssertFalse : ๋ณ„๋„์˜ ๋กœ์ง์œผ๋กœ ๊ฒ€์ฆ ๊ฐ€๋Šฅ → ์กฐ๊ธˆ ์•„๋ž˜์—์„œ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.
  • @Past : ๊ณผ๊ฑฐ ๋‚ ์งœ (์˜ค๋Š˜ ์ด์ „)
  • @PastOrPresent : ์˜ค๋Š˜์ด๊ฑฐ๋‚˜ ๊ณผ๊ฑฐ
  • @Future : ๋ฏธ๋ž˜ ๋‚ ์งœ (์˜ค๋Š˜ ์ดํ›„)
  • @FutureOfPresent : ์˜ค๋Š˜์ด๊ฑฐ๋‚˜ ๋ฏธ๋ž˜

๊ฒ€์ฆ ์‹คํŒจ ์‹œ Message

Validation์— ์‹คํŒจํ•˜๋ฉด message๋ฅผ ํ†ตํ•ด์„œ ๊ฒ€์ฆ ์‹คํŒจ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์•„๋ž˜์™€ ๊ฐ™์ด server error๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  message๊ฐ€ ์ถœ๋ ฅ๋˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Message ๋ณ€๊ฒฝํ•˜๊ธฐ

Validation ๊ด€๋ จ Annotation์„ ์‚ดํŽด๋ณด๋ฉด

์•„๋ž˜์™€ ๊ฐ™์ด message๋ผ๋Š” ๋ฉค๋ฒ„๋ฅผ ๊ฐ€์ง„ ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ํ•œ ๋ฒˆ ์ปค์Šคํ…€ ํ•ด๋ณผ๊ฒŒ์š”

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface Min {

    String message() default "{javax.validation.constraints.Min.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    /**
     * @return value the element must be higher or equal to
     */
    long value();

์•„๋ž˜์™€ ๊ฐ™์ด ๊ฐ„๋‹จํ•œ api๋ฅผ ๋งŒ๋“ค๊ณ  @Min ์„ ์ด์šฉํ•ด์„œ age๊ฐ€ 19 ๋ฏธ๋งŒ์ผ ๋•Œ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  error ๋ฐœ์ƒ ์‹œ message ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

@GetMapping
public User get(@RequestParam @NotBlank String name,
                @RequestParam @Min(value = 19, message = "ํ•ด๋‹น ์ปจํ„ด์ธ ๋Š” 19์„ธ ์ด์ƒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
                                                Integer age) {
    User user = new User();
    user.setName(name);
    user.setAge(age);

    return user;
}

์ด์ œ ํ•ด๋‹น uri๋กœ ์ ‘๊ทผํ•˜๋ฉด?

๋‹ค์Œ๊ณผ ๊ฐ™์ด message๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

๊ฒ€์ฆ ๋กœ์ง ์ปค์Šคํ…€ํ•˜๊ธฐ

์›ํ•˜๋Š” ๊ฒ€์ฆ ๋กœ์ง์„ ์„ค์ •ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • @AssertTrue์™€ @AssertFalse ์ด์šฉํ•˜๊ธฐ
  • @AsserFalse๋Š” ์—ฌ๊ธฐ์„  ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
    ๊ฐ„๋‹จํ•˜๊ฒŒ @AssertTrue์™€ ๋ฐ˜๋Œ€๋กœ return๊ฐ’์„ ์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
    return๊ฐ’์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์•„๋ž˜ @AssertTrue๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”
  • Annotation + Validator ์ •์˜ํ•˜๊ธฐ

@AssertTrue

@AssertTrue๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฉ”์„œ๋“œ๋ฅผ ์ƒˆ๋กœ ์ •์˜ํ•ด์„œ ๊ฒ€์ฆ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์˜์  : ์•ž์— boolean๊ฐ’์„ return ํ•ด์•ผํ•˜๋ฉฐ, ๋ฉ”์„œ๋“œ ๋ช…์— ์ ‘๋‘์‚ฌ๋กœ is๋ฅผ ๋ถ™์—ฌ์•ผํ•ฉ๋‹ˆ๋‹ค.
์•ˆ ๋ถ™์ด๋ฉด ์ธ์‹์„ ๋ชป ํ•˜๋”๋ผ๊ตฌ์š”..

Day๋ผ๋Š” dto๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ด๋ฅผ ํ…Œ์ŠคํŠธํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

public class Day {

    String localDate;

    @AssertTrue(message = "yyyy-MM-dd ํ˜•์‹์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
    public boolean isValidLocalDate() {

        try {
            LocalDate.parse(getLocalDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        } catch (Exception e) {
            return false;
        }

        return true;
    }

    public String getLocalDate() {
        return localDate;
    }

    public void setLocalDate(String localDate) {
        this.localDate = localDate;
    }
}

BindingResult๋ฅผ Controller ๋งค๊ฐœ๋ณ€์ˆ˜์— ์ถ”๊ฐ€ํ•˜๋ฉด validation ์‹คํŒจ ์‹œ์— error ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋Š” ์ด๋ฅผ ์ด์šฉํ•ด์„œ error ๋ฉ”์‹œ์ง€๋ฅผ response๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

@RestController
@RequestMapping("/v1")
public class Api {

    @PostMapping("/day")
    public ResponseEntity day(@RequestBody @Valid Day dto, BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            StringBuilder sb = new StringBuilder();

            List<ObjectError> errors = bindingResult.getAllErrors();

            errors.forEach(objectError -> {
                FieldError fieldError = (FieldError) objectError;
                String field = fieldError.getField();
                String message = fieldError.getDefaultMessage();
                sb.append("field : " + field + "\n");
                sb.append("message : " + message + "\n");
            });

            return ResponseEntity.badRequest()
                    .body(sb.toString());
        }

        return ResponseEntity.ok(dto);
    }
}

postman์„ ์ด์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณด๋ฉด validation์ด ์ž˜ ์ ์šฉ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„ค์š”!

Annotation + Validator ์ •์˜ํ•˜๊ธฐ

์‚ฌ์‹ค ์œ„ @AssertTrue ๋ฐฉ์‹์€ ๋งค๋ฒˆ DTO๋งˆ๋‹ค ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•ด์ค˜์•ผ ํ•˜๊ธฐ์— ์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ Annotation๊ณผ Validator๋ฅผ ์ •์˜ํ•ด์„œ ํ”„๋กœ์ ํŠธ ์ „์—ญ์—์„œ Annotation๋งŒ์œผ๋กœ Validation์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

๋จผ์ € DateTime ์ด๋ผ๋Š” Annotation์„ ์ •์˜ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ yyyy-MM-dd ํ˜•์‹์œผ๋กœ ๊ฒ€์ฆ์„ ์ง„ํ–‰ํ•˜๊ณ , pattern์„ ์ปค์Šคํ…€ํ•  ์ˆ˜๋„ ์žˆ๊ฒŒ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ ๋กœ validation message์—๋Š” EL(ํ‘œํ˜„์–ธ์–ด)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

message์˜ ${validatedValue}๋Š” Request๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’์ด๊ณ , {pattern}์€ DateTime ํ•„๋“œ์ธ pattern๊ฐ’ ์ž…๋‹ˆ๋‹ค.

// DateTime.java

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {DateTimeValidator.class}) // (1)
public @interface DateTime {
        // (2)
    String message() default "${validatedValue}๋Š” {pattern}ํ˜•์‹๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

        // (3)
    String pattern() default "yyyy-MM-dd";

}

์ด์ œ Validator๋ฅผ ์ •์˜ํ•ด๋ด…์‹œ๋‹ค. ConstraintValidator interface๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜ ๋ณด์ด๋Š” DateTimeValidator๋Š” @DateTime์ด ์žˆ๋Š” ํ•„๋“œ ํ˜น์€ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

public class DateTimeValidator implements ConstraintValidator<DateTime, String> {

    private String pattern;

    @Override
    public void initialize(DateTime constraintAnnotation) {
        this.pattern = constraintAnnotation.pattern(); // (1)
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {

                // (2)
        try {
            LocalDate.parse(value, DateTimeFormatter.ofPattern(this.pattern));
        } catch (Exception e) {
            return false;
        }

        return true;

    }
}
  • (1) @DateTime์˜ parttern์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  • (2) LocalDate๋กœ parsing์ด ๊ฐ€๋Šฅํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. parsing ๋ถˆ๊ฐ€๋Šฅ ์‹œ error๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ try catch ๋ฌธ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ด์ œ Validation์„ ์œ„ํ•œ ์ค€๋น„๋ฅผ ๋งˆ์ณค์Šต๋‹ˆ๋‹ค

์ด๋ฒˆ์—๋Š” QueryString์„ Validation ํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๊ธฐ ์œ„ํ•ด์„  ๋ช‡๊ฐ€์ง€ ์ž‘์—…์ด ๋” ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

QueryString์„ Validationํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค์— @Valdated๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉํ•  api๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

@Validated๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด service๊ฐ™์ด controller๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ class์—์„œ๋„ validation์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Validated // (1)
@RestController
@RequestMapping("/v1")
public class Api {

    @GetMapping("/items") // (2)
    public ResponseEntity items(@RequestParam("createdDate") @DateTime String createdDate) {
        return ResponseEntity.ok(createdDate);
    }

}
  • (1) @Validated๋ฅผ ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.
  • (2) QueryString์œผ๋กœ createdDate ๊ฐ’์„ ๋ฐ›์•„์„œ ๊ฒ€์ฆํ•  api์ž…๋‹ˆ๋‹ค. @DateTime๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”

์ดํ›„์— postman์œผ๋กœ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฒ€์ฆ ๋กœ์ง์ด ์ž˜ ์ ์šฉ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

์ฃผ์˜! ๋ˆˆ์น˜์ฑ„์‹  ๋ถ„๋„ ์žˆ๊ฒ ์ง€๋งŒ ์ด๋ฒˆ items() GET API์—์„œ BindingResult๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์‚ฌ์šฉํ•˜์ง€ ๋ชป ํ–ˆ์Šต๋‹ˆ๋‹ค. BindingResult๋Š” @RequestBody ํ˜น์€ @RequestPart์‚ฌ์šฉ ์‹œ์—๋งŒ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ด ์  ์ฃผ์˜ํ•ด์ฃผ์„ธ์š”!

๋งˆ๋ฌด๋ฆฌ

์—ฌ๊ธฐ๊นŒ์ง€ Spring Validation์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ดค์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์—๋Š” Spring Validation๊ณผ exceptionHandler๋ฅผ ์กฐํ•ฉํ•ด์„œ API ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ค ๊ฐ’์„ ๋ณด๋ƒˆ๋Š”์ง€์— ๋Œ€ํ•ด์„œ ์•Œ๋งž๋Š” Response๋ฅผ ๋ณด๋‚ด์ค„ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ณผ์ •์— ๋Œ€ํ•ด์„œ ๊ธ€์„ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!

์ฐธ๊ณ 

Validation์— ๋Œ€ํ•ด์„œ ๋” ๋งŽ์€ ์ •๋ณด๋ฅผ ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜ ๊ธ€๋“ค์„ ์ฐธ๊ณ ํ•˜๋Š” ๊ฑธ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค!

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/

https://meetup.toast.com/posts/223

๋ฐ˜์‘ํ˜•

'Spring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

JPA UUID varchar๋กœ ์ €์žฅํ•˜๊ธฐ  (0) 2021.11.29