๋ ์ด์ด ๋ณ Validation ์ค์์ฑ(Jackson ์๊ฒฌ)์
๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆBean Validation 2.0 (JSR 380)Request Body ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ (@Valid)@RequestParam (์ฟผ๋ฆฌํ๋ผ๋ฏธํฐ) & @PathVariable (path param) ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ@Valid vs @Validated@BindingResultValidation ์ด๋
ธํ
์ด์
์ข
๋ฅValidation ์ปค์คํ
ํ๊ฒ ํ๊ธฐ์คํ๋ง์ด ์ ๊ณตํ๋ Validator ๋ฅผ ์ฐ๋, valid ํ exception์ ์ปค์คํ
ํ๊ฒ ๋์ง๊ธฐAssertTrue/False๋ฅผ ์ด์ฉํ ValidationConstraint Validator๋ฅผ ์ด์ฉํ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ Validation๋ฌธ์์ด ์ ํจ์ฑ ๊ฒ์ฆ ์ ํธ ๋ฉ์๋ StringUtils๋น์ฆ๋์ค ๊ท์น ๊ฒ์ฆ
- ๊ฒ์ฆ ๋ก์ง์ ํน์ ๊ณ์ธต(์ปจํธ๋กค๋ฌ, ์๋น์ค, ๋๋ฉ์ธ)์ ์ข ์๋๊ธฐ๋ณด๋ค๋ ๋๋ฉ์ธ ์ค๋ธ์ ํธ์ฒ๋ผ ๋ ๋ฆฝ์ ์ผ๋ก ๋ง๋๋ ๊ฒ์ด ์ข์
- Validator๋ฅผ ์จ์ ๊ฒ์ฆ ์์ ์ ์งํํ๋ ๊ณณ์ ์ด๋๋ก ํ ์ง๋ ๊ณ ๋ฏผํด๋ณด์์ผ ํจ. ๋ค๊ฐ์ง ๋ฐฉ๋ฒ์ด ์กด์ฌ โ by ํ ๋น์ ์คํ๋ง
- ์ปจํธ๋กค๋ฌ ๋ฉ์๋ ๋ด์ ์ฝ๋(Validator๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ๊ณ validate() ๋ฉ์๋๋ฅผ ํธ์ถํด์ ๊ฒ์ฆ์์ ์งํ)
- @Valid๋ฅผ ์ด์ฉํ ์๋๊ฒ์ฆ(์ปจํธ๋กค๋ฌ์์) โ JSR-303(Bean Validation) ์ด์ฉ
- ์๋น์ค ๊ณ์ธต ์ค๋ธ์ ํธ์์์ ๊ฒ์ฆ
- ์ฌ๋ฌ ๊ฐ์ ์๋น์ค ๊ณ์ธต ์ค๋ธ์ ํธ์์ ๋ฐ๋ณต์ ์ผ๋ก ๊ฐ์ ๊ฒ์ฆ ๊ธฐ๋ฅ์ด ์ฌ์ฉ๋๋ค๋ฉด Validator๋ก ๊ฒ์ฆ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ๊ณ ์ด๋ฅผ DI ๋ฐ์์ ์ฌ์ฉํ ์ ์์
- ์๋น์ค ๊ณ์ธต์ ํ์ฉํ๋ Validator
- Validator๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํด์ ์๋น์ค ๊ณ์ธต์ ๊ธฐ๋ฅ์ ์ฌ์ฉํด ๊ฒ์ฆ ์์ ์ ์ํํ ์ ์๋ค
๋ ์ด์ด ๋ณ Validation ์ค์์ฑ(Jackson ์๊ฒฌ)
- validation์ ๋๋ฉ์ธ > ์๋น์ค > ์ปจํธ๋กค๋ฌ ์์ผ๋ก ๋ ์ด์ด ๋ฎ์ ๊ณณ์์ ๋ ๋ง์ด, ์ค์ํ๊ฒ ์งํ๋์ด์ผ ํ๋ค!
- ์ค์ํ ๊ฒ์ ์ด๋ ํ validation ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋๊ฐ๊ฐ ์๋ validation ์ด ํ์ํ ๊ณณ์ ์ ์ ํ๊ฒ validation ์ ์ํํ๋๊ฐ ์ ๋๋ค. validation ์ด ์ด๋์ ์์นํด์ผ ํ๋๊ฐ? ์ ๋ํ ์ง๋ฌธ์ ์ฌ๋๋ง๋ค ์๊ฒฌ์ด ๋ค๋ฅผ ์ ์์ง๋ง ๊ฐ์ธ์ ์๊ฒฌ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ๋๋ฉ์ธ ๋ชจ๋ธ > ์๋น์ค ๋ ์ด์ด > ์ปจํธ๋กค๋ฌ ๋ ์ด์ด ์๋ฅผ ๋ค์ด Email ํด๋์ค์ String getAddress() ๋ฉ์๊ฐ null์ ๋ฆฌํดํ์ง ์์์ ๋ณด์ฅํ๋ ๊ฐ์ฅ ํ์คํ ๋ฐฉ๋ฒ์ ๋ฌด์์๊น์? Email ํด๋์ค๋ฅผ ๋ถ๋ณ๊ฐ์ฒด๋ก ๋ง๋ค๊ณ address ๋ฅผ ์์ฑ์์์ ์ฒดํฌํ๋ ๊ฒ ์ ๋๋ค. ๋ฐ๋ผ์, ๊ฐ์ฒด์ ์์ฑ์๋ ๊ฐ์ฒด๋ฅผ ๊ตฌ์ฑํ๋ ํ๋๋ค์ ๋ํ validation ์ ์ํํ๋๋ฐ ๊ฐ์ฅ ์ต์ ์ ์์น์ ๋๋ค. ์์ฑ์์์ ์ฌ๋ฐ๋ฅธ validation ์ํ์ ๋ณด์ฆํ ์ ์๋ค๋ฉด getter ๋ฉ์๋๋ฅผ ํธ์ถํ๊ณ , ๋ฐํ๊ฐ์ ๋ํด ํญ์ ๋ฐฉ์ด์ ์ธ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํฉ๋๋ค. ๋ํ UserService ํด๋์ค์ login ์ด๋ผ๋ ๋ฉ์๋๋ ์ปจํธ๋กค๋ฌ๊ฐ ์๋ ๋ค๋ฅธ ์์น์์๋ ํธ์ถ๋ ์ ์์ต๋๋ค. ์ด๋ฌํ ํน์ฑ์ ๊ณ ๋ คํ์ ๋, validation ๋ก์ง์ ์ฌํ์ฉ์ฑ์ ๋์ด๊ธฐ ์ํด์๋ ์ปจํธ๋กค๋ฌ๋ณด๋ค๋ ์๋น์ค ๋ ์ด์ด์์ validation ์ฒ๋ฆฌ๊ฐ ๋ ์ข์ต๋๋ค.
- ๋ง๋ค๋ฉด์ ๋ฐฐ์ฐ๋ ํด๋ฆฐ์ํคํ ์ฒ์ ์ด๋๊น์ง์ ๊ฒฝํ์ ์ข ํฉํด์ ์ ๋ฆฌ๋ฅผ ํด๋ณด๋ฉด
- ์ปจํธ๋กค๋ฌ - ์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ
- ์๋น์ค์ ์ ๋ ฅ๋ชจ๋ธ์ ์๋น์ค์ ๋งฅ๋ฝ์์ ์ ํจํ ์ ๋ ฅ๋ง ํ์ฉ. ์ปจํธ๋กค๋ฌ์์์ ์ ๋ ฅ ๋ชจ๋ธ์ ์๋น์ค์ ์ ๋ ฅ ๋ชจ๋ธ๊ณผ๋ ๊ตฌ์กฐ๋ ์๋ฏธ๊ฐ ์์ ํ ๋ค๋ฅผ ์ ์์
- ์๋น์ค ์ ๋ ฅ ๋ชจ๋ธ์์ ํ๋ ์ ํจ์ฑ ๊ฒ์ฆ์ ๋๊ฐ์ด ์ปจํธ๋กค๋ฌ์์๋ ๊ตฌํํ๋ ๊ฒ์ ์๋๊ณ , ์ปจํธ๋กค๋ฌ์ ์ ๋ ฅ ๋ชจ๋ธ์ ์๋น์ค์ ์ ๋ ฅ ๋ชจ๋ธ๋ก ๋ณํํ ์ ์๋ค๋ ๊ฒ์ ๊ฒ์ฆ
- (๊ทธ๋ผ, ์ปจํธ๋กค๋ฌ์ ์๋น์ค์์ ๊ฐ์ ๋ชจ๋ธ์ ์ฌ์ฉํ๋ค๋ฉด ์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ์ ํ๋ฒ๋ง ํ๋ฉด ๋๊ฒ ๊ตฌ๋)
- ์๋น์ค - ์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ, ๋น์ฆ๋์ค ๊ท์น ๊ฒ์ฆ
- ์ปจํธ๋กค๋ฌ์ ์น ๋ชจ๋ธ์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ฒ ๋๋ฉด ์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ปจํธ๋กค๋ฌ ์น ๋ชจ๋ธ์์ ์งํ๋๊ณ , ๋น์ฆ๋์ค ๊ท์น ๊ฒ์ฆ๋ง ํ๋ฉด ๋จ
- ๋๋ฉ์ธ - ๋น์ฆ๋์ค ๊ท์น ๊ฒ์ฆ & ์์ฑ์๋ก ์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ
- ์๋น์ค์์ ์งํํ๋ ๋น์ฆ๋์ค ๊ท์น ๊ฒ์ฆ์ ๋๋ฉ์ธ ๋ก์ง์ผ๋ก ์ฒ๋ฆฌํด๋ ๋จ
- ๋๋ฉ์ธ์ด ์ ์ผ ์์ชฝ์ ์์ผ๋ ์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ค์ํ๊ฒ ์งํ๋์ด์ผ ํจ!
์ ๋ ฅ ์ ํจ์ฑ ๊ฒ์ฆ
- ๊ฒ์ฆํด์ผ ํ ๊ฐ์ด ๋ง์ ๊ฒฝ์ฐ ์ฝ๋์ ๊ธธ์ด๊ฐ ๊ธธ์ด์ง๋ค.
- ๊ตฌํ์ ๋ฐ๋ผ์ ๋ฌ๋ผ์ง ์ ์์ง๋ง Service Logic๊ณผ์ ๋ถ๋ฆฌ๊ฐ ํ์ํจ
- ํฉ์ด์ ธ ์๋ ๊ฒฝ์ฐ ์ด๋์์ ๊ฒ์ฆ์ ํ๋์ง ์๊ธฐ ์ด๋ ค์ฐ๋ฉฐ, ์ฌ์ฌ์ฉ์ ํ๊ณ๊ฐ ์์
- ๊ตฌํ์ ๋ฐ๋ผ ๋ฌ๋ผ ์ง ์ ์์ง๋ง, ๊ฒ์ฆ Logic์ด ๋ณ๊ฒฝ๋๋ ๊ฒฝ์ฐ ํ ์คํธ ์ฝ๋ ๋ฑ ์ฐธ์กฐํ๋ ํด๋์ค์์ Logic์ด ๋ณ๊ฒฝ๋์ด์ผ ํ๋ ๋ถ๋ถ์ด ๋ฐ์ ํ ์ ์๋ค.
implementation โorg.springframework.boot:spring-boot-starter-validationโ

Bean Validation 2.0 (JSR 380)
[Baeldung] Java Bean Validation Basics โ JSR(Java Specification Request) 380 (Bean Validation 2.0)
implementation('org.springframework.boot:spring-boot-starter-validation') // ํน์ hibernate-validator ๋ง ์ถ๊ฐํด๋ ๋จ. ๊ทผ๋ฐ ์ด ๋ spring boot ๋ฒ์ ์ด๋ ์๋ง์ผ๋ฉด ๋์์ ์ํ๋๋ผ implementation 'org.hibernate.validator:hibernate-validator:6.2.0.Final'
- Bean Validation 2.0 ์ JSR-380 ์ผ๋ก ๋ถ๋ฆฌ๋ api์ ๋ํ ์ ์
- Hibernate Validator ๋ ๊ทธ๊ฒ์ ๋ํ ๊ตฌํ์. 2018.4์ ๊ธฐ์ค์ผ๋ก ์ ์ผํ jSR-380 ์๋ํ ์ธ์ฆ๋ฐ์ ๊ตฌํ
Request Body ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ (@Valid)
@RestController public class ValidateRequestBodyController { @PostMapping("/validateBody") ResponseEntity<String> validateBody(@Valid @RequestBody InputRequest request) { return ResponseEntity.ok("valid"); } }
@Valid
์ด๋ ธํ ์ด์ ์ด ๋ถ์์ผ๋ก Spring์ด ๋ค๋ฅธ ์์ ์ ์ํํ๊ธฐ ์ ์ ๋จผ์ ๊ฐ์ฒด๋ฅผValidator
์ ์ ๋ฌํด์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํ๊ฒ ๋จ
- ๋ง์ฝ
InputRequest
์์ ์ ํจ์ฑ์ ๊ฒ์ฌํด์ผ ํ๋ ๋ค๋ฅธ ๊ฐ์ฒด๊ฐ ํ๋๋ก ํฌํจ๋๋ ๊ฒฝ์ฐ ๊ทธ ๊ฐ์ฒด์๋@Valid
๋ฅผ ๋ถ์ฌ์ค์ผ ํจ โ Complex Type
- @RequestBody์์ @Valid๋ฅผ ๋ถ์ด๊ณ InputRequest ํ๋๋ค์ ๋ํด @Min, @Max ์ด๋ฐ ์ ๋ค ๋ถ์ด๋ฉด ๋จ
- ์ ํจ์ฑ ๊ฒ์ฌ์ ์คํจํ ๊ฒฝ์ฐ
MethodArgumentNotValidException
์์ธ๊ฐ ๋ฐ์
- Kotlin์์๋ @field:NotBlank ์ ๊ฐ์ ์์ผ๋ก ์ ์ฉํด์ผํจ [ ์ฐธ๊ณ ]
@RequestParam (์ฟผ๋ฆฌํ๋ผ๋ฏธํฐ) & @PathVariable (path param) ์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ
@Validated @RestController public class ValidateParametersController { @GetMapping("/validatePathVariable/{id}") ResponseEntity<String> validatePathVariable(@PathVariable("id") @Min(5) int id) { return ResponseEntity.ok("valid"); } @GetMapping("/validateRequestParameter") ResponseEntity<String> validateRequestParameter(@RequestParam("param") @Min(5) int param) { return ResponseEntity.ok("valid"); } }
@Validated
์ด๋ ธํ ์ด์ ์ ํด๋์ค ๋ ๋ฒจ์ Controller์ ์ถ๊ฐํด Spring์ด ๋ฉ์๋ ๋งค๊ฐ ๋ณ์์ ๋ํ ์ ํ ์กฐ๊ฑด annotation์ ํ๊ฐํ๊ฒ ํด์ผํ๋ค.
- ์ ํจ์ฑ ๊ฒ์ฌ์ ์คํจํ ๊ฒฝ์ฐ
ConstraintViolationException
๋ฐ์
@Valid vs @Validated
- @Valid
- method level validation
- member attribute for validation ์์๋ ์ฌ์ฉํจ
- ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ์ โ
MethodArgumentNotValidException
- @Validated
- group level validation์์ ์ฌ์ฉ (ํด๋์ค ์์ค์์ ํ๊ฐ๋จ)
- ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ์ โ
ConstraintViolationException
@BindingResult
@PostMapping public ResponseEntity post(@Valid @RequestBody User user, BindingResult bindingResult){ if(bindingResult.hasErrors()){ StringBuilder sb = new StringBuilder(); bindingResult.getAllErrors().forEach(objectError->{ FieldError field = (FieldError) objectError; String message = objectError.getDefaultMessage(); sb.append("field : " + field.getField()); sb.append("message : " + message); }); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString()); } //logic return ResponseEntity.ok(user); } public class User{ private String name; private int age; @Email private String email; @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message="ํธ๋ํฐ ๋ฒํธ ์์๊ณผ ๋ง์ง ์์ต๋๋ค.") private String phoneNumber; ... }
- BindingResult๋ผ๋ ํ๋ผ๋ฏธํฐ๋ฅผ ํจ์์์ ๋ฐ์์ผ๋ก ๋ณ์์ ํํ์ ๋ง๊ฒ ๊ฐ์ด ๋ค์ด์ค์ง ์์์๋ ์์ ์๋ฌ๋ฅผ ๋ด๋ ๊ฒ์ด ์๋๋ผ ์ด๋ค ์์ผ๋ก ์๋ฌ๊ฐ ๋ฌ๋์ง๋ฅผ ๋ฐํ ๊ฐ๋ฅํจ โ ์ด๊ฒ๋ณด๋ค๋ exception ์ฒ๋ฆฌ๊ฐ ๋์๋ฏ
Validation ์ด๋ ธํ ์ด์ ์ข ๋ฅ
@Past
: annotated element must be an instant, date or time in the past.
@Future
: annotated element must be an instant, date or time in the future.
Validation ์ปค์คํ ํ๊ฒ ํ๊ธฐ
์คํ๋ง์ด ์ ๊ณตํ๋ Validator ๋ฅผ ์ฐ๋, valid ํ exception์ ์ปค์คํ ํ๊ฒ ๋์ง๊ธฐ
public static void validate(ReserveTownHallRequest request) { ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); Validator validator = validatorFactory.getValidator(); Set<ConstraintViolation<ReserveTownHallRequest>> violations = validator.validate(request); if (!violations.isEmpty()) { for (ConstraintViolation<ReserveTownHallRequest> violation: violations) { Path propertyPath = violation.getPropertyPath(); if (propertyPath.toString().equals("name")) { throw new TownHallCreateException(ErrorCode.TOWNHALL_TITLE_IS_EMPTY); } } throw new ConstraintViolationException(violations); } }
- ์คํ๋ง์์ ์ ๊ณตํ๋ Validator๋ฅผ ๊ฐ์ ธ์จ ๋ค, ์ง์ violations๋ฅผ ํ๋์ฉ ๋ณด๋ฉด์ ํด๋นํ๋ field ์ ๋ํด์ exception์ ์ปค์คํ ํ๊ฒ throw
AssertTrue/False๋ฅผ ์ด์ฉํ Validation
@AssertTrue(message= "yyyyMM์ ํ์์ ๋ง์ง ์์ต๋๋ค.") public boolean isreqYearMonth(){ try{ LocalDate localDate = LocalDate.parse(this.reqYearMonth + "01", DateTimeFormatter.ofPattern("yyyyMMdd")); } catch(Exception e){ return false; } return true; }
- @AssertTrue annotation์ด ๋ถ์ ๋ฉ์จ๋๋ is๋ก ์์ํด์ผ ๋์์ด ๋จ
- ๊ทผ๋ฐ ์์ ๊ฐ์ด ์์ฑํ ๋์ ๋ฌธ์ ์ ์ class ๋ณ๋ก ์ ๋ฉ์จ๋๋ฅผ ๋ค ๋ง๋ค์ด ์ฃผ์ด์ผ ํ๋ค๋ ๊ฒ โ Custom Annotation์ ๋ง๋ค์!
Constraint Validator๋ฅผ ์ด์ฉํ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ Validation
//YearMonth.java - annotation @Constraint(validatedBy = { YearMonthValidator.class}) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) public @interface YearMonth{ String message() default "yyyyMM ํ์์ ๋ง์ง ์์ต๋๋ค." Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; String pattern() default "yyyyMM"; } //YearMonthValidator.java public class YearMonthValidator implements ConstraintValidator<YearMonth, String>{ private String pattern; @Override public void initialize(YearMonth constraintAnnotation){ this.pattern = constraintAnnotation.pattern() + "dd"; } @Override public boolean isValid(String value, ConstraintValidatorContext context){ // yyyyMM try{ LocalDate localDate = LocalDate.parse(value + "01", DateTimeFormatter.ofPattern(this.pattern)); } catch(Exception e){ return false; } return true; } }
- ์์์ ๋ง๋ Annotation YearMonth๋ฅผ validation ํ๊ณ ์ถ์ ๋ณ์์์๋ค๊ฐ ๋ถ์ด๊ธฐ
๋ฌธ์์ด ์ ํจ์ฑ ๊ฒ์ฆ ์ ํธ ๋ฉ์๋ StringUtils
[์ฐธ๊ณ ] link
๋น์ฆ๋์ค ๊ท์น ๊ฒ์ฆ
- ๋น์ฆ๋์ค ๊ท์น์ ๋ํ ๊ฒ์ฆ์ ๋๋ฉ์ธ ์ํฐํฐ ์์ ์์นํ๊ฑฐ๋, ์๋น์ค ์ฝ๋์์ ๋๋ฉ์ธ ์ํฐํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ ์ ์งํํ๋ ๊ฒ์ด ์ข์
public class Account{ public boolean withdraw(Money money, AccountId targetAccountId) { if (!mayWithdraw(money)){ return false; } // ... } }
public class SendMoneyService implement SendMoneyUseCase{ // ... @Override public boolean sendMoney(SendMoneyCommand command) { requireAccountExists(command.getSourceAccountId()); requireAccountExists(command.getTargetAccountId());