Bean Validation
๊ธฐ๋ณธ์ ์ ๊ฒ์ฆ ๋ก์ง์ ๊ณตํตํํ๊ณ , ํ์คํ ํ ๊ฒ์ด Bean Validation์ด๋ค.
Bean Validation์ ํน์ ํ ๊ตฌํ์ฒด๊ฐ ์๋๋ผ Bean Validation 2.0์ด๋ผ๋ ๊ธฐ์ ํ์ค์ด๋ค.
๊ฐ๋จํ๊ฒ ๋งํ์๋ฉด, ๊ฒ์ฆ ์ ๋ ธํ ์ด์ ๊ณผ ์ฌ๋ฌ ์ธํฐํ์ด์ค์ ๋ชจ์์ด๋ค. ์ด๋ฌํ Bean Validation์ ๊ตฌํํ ๊ธฐ์ ๋ค ์ค ์ผ๋ฐ์ ์ผ๋ก ์ฐ๋ฆฌ๋ ํ์ด๋ฒ๋ค์ดํธ Validatior๋ฅผ ์ฌ์ฉํ๋ค. ์ด๋ฆ์ ํ์ด๋ฒ๋ค์ดํธ๊ฐ ๋ถ์ด์์ง๋ง ORM๊ณผ๋ ๊ด๋ จ์ด ์๋ค.
์๋๋ ์ฐธ๊ณ ์ฌ์ดํธ๋ค
์ฌ์ฉ
Bean Validation์ ์ฌ์ฉํ๋ ค๋ฉด ๋ค์ ์์กด๊ด๊ณ๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค.
build.gradle์ ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-validation'
์๋์ ๊ฐ์ด ์ฌ์ฉํ ์ ์๋ค.
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
public record MemberSignUpDto(@NotBlank(message = "์์ด๋๋ฅผ ์
๋ ฅํด์ฃผ์ธ์") @Size(min = 7, max = 25, message = "์์ด๋๋ 7~25์ ๋ด์ธ๋ก ์
๋ ฅํด์ฃผ์ธ์")
String username,
@NotBlank(message = "๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,30}$",
message = "๋น๋ฐ๋ฒํธ๋ 8~30 ์๋ฆฌ์ด๋ฉด์ 1๊ฐ ์ด์์ ์ํ๋ฒณ, ์ซ์, ํน์๋ฌธ์๋ฅผ ํฌํจํด์ผํฉ๋๋ค.")
String password,
@NotBlank(message = "์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์") @Size(min=2, message = "์ฌ์ฉ์ ์ด๋ฆ์ด ๋๋ฌด ์งง์ต๋๋ค.")
@Pattern(regexp = "^[A-Za-z๊ฐ-ํฃ]+$", message = "์ฌ์ฉ์ ์ด๋ฆ์ ํ๊ธ ๋๋ ์ํ๋ฒณ๋ง ์
๋ ฅํด์ฃผ์ธ์.")
String name,
@NotBlank(message = "๋๋ค์์ ์
๋ ฅํด์ฃผ์ธ์.")
@Size(min=2, message = "๋๋ค์์ด ๋๋ฌด ์งง์ต๋๋ค.")
@NotBlank String nickName,
@NotNull(message = "๋์ด๋ฅผ ์
๋ ฅํด์ฃผ์ธ์")
@Range(min = 0, max = 150)
Integer age) {
}
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
/**
* ํ์๊ฐ์
*/
@PostMapping("/signUp")
@ResponseStatus(HttpStatus.OK)
public void signUp(@Valid @RequestBody MemberSignUpDto memberSignUpDto) throws Exception {
memberService.signUp(memberSignUpDto);
}
}
์ฐธ๊ณ ๋ก javax.validation.constrains.NotNull์ฒ๋ผ javax.validation์ผ๋ก ์์ํ๋ค๋ฉด, ํน์ ๊ตฌํ์ฒด์ ๊ด๊ณ์์ด ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต๋๋ ํ์ค ์ธํฐํ์ด์ค์ด๊ณ , Range์ฒ๋ผ org.hibernate.validator๋ก ์์ํ๋ค๋ฉด ํ์ด๋ฒ๋ค์ดํธ validator ๊ตฌํ์ฒด๋ฅผ ์ฌ์ฉํ ๋๋ง ์ ๊ณต๋๋ ๊ฒ์ฆ ๊ธฐ๋ฅ์ด๋ค.
์คํ๋ง MVC์์ ์ฌ์ฉ
์คํ๋ง ๋ถํธ๋ 'spring-boot-starter-validation'๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ฃ์ผ๋ฉด ์๋์ผ๋ก Bean Validator๋ฅผ ์ธ์งํ๊ณ ์คํ๋ง์ ํตํฉํ๋ค.
์คํ๋ง ๋ถํธ๋ LocalValidatorFactoryBean์ ๊ธ๋ก๋ฒ Validator๋ก ๋ฑ๋กํ๋๋ฐ, ์ด๋ @NotNull๋ฑ๊ณผ ๊ฐ์ ์ ๋ ธํ ์ด์ ์ ๋ณด๊ณ ๊ฒ์ฆ์ ์ํํ๋ค. ์ด๋ ๊ฒ ๊ธ๋ก๋ฒ Validator๊ฐ ์ ์ฉ๋์ด ์๊ธฐ ๋๋ฌธ์ @Valid ํน์ @Validated๋ง ์ ์ฉํ๋ฉด ๋๋ค.
๊ฒ์ฆ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค๋ฉด FieldError ํน์ ObjectError๋ฅผ ์์ฑํ์ฌ BindingResult์ ๋ฑ๋กํด์ค๋ค.
์ฐธ๊ณ ๋ก @Valid๋ ์๋ฐ ํ์ค ๊ฒ์ฆ ์ ๋ ธํ ์ด์ ์ด๊ณ @Validated๋ ์คํ๋ง ์ ์ฉ ๊ฒ์ฆ ์ ๋ ธํ ์ด์ ์ด๋ค. ๋ ๋ค ๋ชจ๋ ๋์ผํ๊ฒ ์๋ํ๋ฏ๋ก ์๋ฌด๊ฑฐ๋ ์ฌ์ฉํด๋ ๋์ง๋ง @Validated๋ ๋ด๋ถ์ groups๋ผ๋ ๊ธฐ๋ฅ์ ํฌํจํ๊ณ ์๋ค. ๊ทธ๋ฌ๋ groups๋ ์ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์, groups ๋๋ฌธ์ @Validated๋ฅผ ์ฌ์ฉํ๋ ์ผ์ ๋๋ฌผ ๊ฒ์ด๋ค.
BindException, MethodArgumentNotValidException
@Valid๋ฅผ ํตํด ๊ฒ์ฆ์ ํ ํ, ๋ง์ฝ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ์ง ๋ชปํ์ฌ ์คํจํ ๊ฒฝ์ฐ ์์ ๋ ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค.
@ModelAttribute ์ด๋ ธํ ์ด์ ์ผ๋ก ๋ฐ์ ํ๋ผ๋ฏธํฐ์์ ๊ฒ์ฆ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ BindException์ด ๋ฐ์ํ๊ณ
@RequestBody ์ด๋ ธํ ์ด์ ์ผ๋ก ๋ฐ์ ํ๋ผ๋ฏธํฐ์์ ๊ฒ์ฆ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ MethodArgumentNotValidException๋ฅผ ๋ฐ์์ํจ๋ค.
์ถ๊ฐ๋ก MethodArgumentNotValidException ๋ BindException์ ์์๋ฐ์ ๊ตฌํ๋์๋ค.
์๋ฌ ์ฝ๋ ๋ด์ฉ ์ค์
Bean Validation์ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฌ ๋ฉ์์ง๋ฅผ ์ ๊ณตํ์ง๋ง, ์ด๋ฅผ ๋ฐ๊พธ๊ณ ์ถ์ ์ ์์๊ฒ์ด๋ค.
Bean Validation์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ๋ฉ์์ง ์ฝ๋๋ฅผ ์์ฑํด์ฃผ๋๋ฐ @NotBlank๋ฅผ ์์๋ก ํ์ธํด๋ณด์.
์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค
@NotBlack
- NotBlank.memberSignUpDto.nickName (๊ฒ์ฆ ์ ๋ ธํ ์ด์ , ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฐ์ฒด ์ด๋ฆ, ํ๋)
- NotBlank.nickName (๊ฒ์ฆ ์ ๋ ธํ ์ด์ , ํ๋)
- NotBlank.java.lang.String (๊ฒ์ฆ ์ ๋ ธํ ์ด์ , ์ ์ฉ๋ ํ์ )
- NotBlank (๊ฒ์ฆ ์ ๋ ธํ ์ด์ )
๋ค๋ฅธ ๊ฒ์ฆ ์ ๋ ธํ ์ด์ ๋ ์ด์ ๋น์ทํ๊ฒ ์์ฑ๋๋ค.
์ฐธ๊ณ ๋ก ์ด์๊ฐ์ ์๋ฌ ์ฝ๋๋ MessageCodesResolver๊ฐ ๋ง๋ค์ด์ค๋ค.
์ด๋ฅผ ์คํ๋ง์ด ์ ๊ณตํ๋ ๋ฉ์์ง ๊ธฐ๋ฅ์ ์ฌ์ฉํ์ฌ ๋ฐ๊ฟ ์ ์๋ ๊ฒ์ด๋ค.
์ฐธ๊ณ
์์ NotBlanck์ ๋ฉ์ธ์ง๋ฅผ ํ๋ฒ ๋ฐ๊พธ์ด๋ณด์.
NotBlank.memberSignUpDto.nickName=ํ์๊ฐ์
์ ๋ณ๋ช
์ ๊ผญ ์
๋ ฅํด์ผ ํฉ๋๋ค.
NotBlank.nickName=๋ณ๋ช
์ ๋น์นธ์ผ ์ ์์ต๋๋ค.
NotBlank.java.lang.String=ํด๋น ๋ฌธ์์ด์ ๋น์นธ์ผ ์ ์์ต๋๋ค.
NotBlank=๋น์นธ์ผ ์ ์์ต๋๋ค.
(๋ ์์ธํ ๊ฒ์ผ์๋ก ์ฐ์ ์์๊ฐ ๋๊ฒ ์ ์ฉ๋๋ค.)
BeanValidation ๋ฉ์์ง ์ฐพ๋ ์์
- ์์ฑ๋ ๋ฉ์์ง ์ฝ๋ ์์๋๋ก messageSource์์ ๋ฉ์์ง ์ฐพ๊ธฐ
- ์ ๋ ธํ ์ด์ ์ message ์์ฑ ์ฌ์ฉ (์ : @NotBlank(message = "๊ณต๋ฐฑ์ ์๋ผ์ฉ"))
- ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ๊ฐ ์ฌ์ฉ -> "๊ณต๋ฐฑ์ผ ์ ์์ต๋๋ค."
์คํ๋ง์ด ์ง์ ๋ง๋ค์ด์ค ์ค๋ฅ ๋ฉ์์ง ์ฒ๋ฆฌํ๊ธฐ
๊ฒ์ฆ ์ค๋ฅ๋ ๊ฐ๋ฐ์๊ฐ ์ง์ ์ค์ ํ ์๋ ์์ง๋ง, Integer ํ์ ์ String์ ๋ฃ๋ ๋ฑ์ ์ค๋ฅ๋ ์คํ๋ง์ด ์ง์ ๊ฒ์ฆ ์ค๋ฅ์ ์ถ๊ฐํ๋ค.
(์ฃผ๋ก ํ์ ์ ๋ณด๊ฐ ๋ง์ง ์์ ๊ฒฝ์ฐ ๋ง์ํ๋ ์ค๋ฅ)
ํ์ ์ด ๋ง์ง ์๋ ๊ฒฝ์ฐ์๋ typeMismatch๋ผ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
๋ค์๊ณผ ๊ฐ์ ํ์์ด๋ค.
- typeMismatch.item.price
- typeMismatch.price
- typeMismatch.java.lang.Integer
- typeMismatch
์ ๋ํ ๋ฉ์์ง ์์ค์ ๋ฑ๋กํ์ฌ ์ฒ๋ฆฌํ ์ ์๋ค.
์ฐธ๊ณ
@RequestBody๋ฅผ ํตํด Json์ ํ์ฑํ๋ ๊ฒฝ์ฐ์๋ ํ์ ์ด ๋ง์ง ์์๊ฒฝ์ฐ HttpMessageNotReadableException๊ฐ ๋ฐ์ํ๋ฉฐ, ์ด ๊ฒฝ์ฐ์๋ MessageCodesResolver๊ฐ ์๋ํ์ง ์์ ์๋ฌ์ฝ๋๊ฐ ์์ฑ๋์ง ์๋๋ค.
์ฌ๋ฌ ํ๋์ ๊ฐ์ ์กฐํฉํ์ฌ ๊ฒ์ฆํ๋ ๊ฒฝ์ฐ
์๋ฅผ ๋ค์ด ์ด๋ค ์ํ์ ์ฃผ๋ฌธํ ๋, ์ํ์ ๊ฐ๊ฒฉ๊ณผ ์๋์ด X์ ์ด์์ ๋๊ฒจ์ผ ํ๋ ๊ฒฝ์ฐ ์ด๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ๊น?
์์ ๊ฐ์ด ํน์ ํ๋๊ฐ ์๋, ์ค๋ธ์ ํธ์ ํฌํจ๋ ์ฌ๋ฌ ํ๋์ ๊ฐ์ ๊ฒ์ฆํ์ฌ ๋ฐ์์ํค๋ ์ค๋ธ์ ํธ ์ค๋ฅ์ ๊ฒฝ์ฐ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ ์์๊น?
@ScriptAssert๋ฅผ ์ฌ์ฉํ๋ฉด ๋์ง๋ง, ์ฌ์ค ์ด๊ฒ๋ณด๋ค๋ ๊ทธ๋ฅ ์๋ฐ ์ฝ๋๋ฅผ ์ง์ ์์ฑํ์ฌ ์ฒ๋ฆฌํ๋ ๊ฒ์ ๊ถ์ฅํ๋ฉฐ, ๊ทธ๋ผ์๋ ์์๋ณด๊ณ ์ถ๋ค๋ฉด ์ง์ ๊ฒ์ํด์ ์ฐพ์๋ณด๋๋ก ํ์.
๐ Reference