[SpringBoot] ์๋ฒ์์ ์ฌ์ฉ์ ์ ๋ ฅ ๊ฐ๊ณผ Request Body๋ฅผ Validationํ๋ 2๊ฐ์ง ๋ฐฉ๋ฒ
๐ Validation์ด ํ์ํ ์ด์
์๋์ ์ผ๋ก ์ด์ํ ๊ฐ์ผ๋ก ์๋ฒ์ ์์ฒญ์ ๋ณด๋ผ ์ ์๊ธฐ ๋๋ฌธ์ ํ๋ก ํธ์ ์๋ฒ์์ ๋๋ค ํด์ค์ผ ํ๋ค.
ํ์ฌ ์ฝ๋๋ฅผ ๋ณด๋ ์ด๋ฉ์ผ์ด๋ ์ ํ๋ฒํธ ์ ๋ ฅ ๊ฐ์ ๋ํ ํ์์ ๋ชจ๋ ์๋ฒ์์๋ ์ฒดํฌํด์ฃผ์๋ค.
(๊ธฐ์กด์ ์ฌ์ด๋ ํ๋ก์ ํธ ํ ๋๋ '์ ๋ ฅ๊ฐ์ ํ๋ก ํธ์์ ์ ํจ์ฑ ๊ฒ์ฌ ํด์ฃผ๋๊น ๊ด์ฐฎ๊ฒ ์ง' ํ๊ณ ๋์ด๊ฐ์๋๋ฐ ๋ฐ์ฑํ๊ฒ ๋์๋ค. ๋ค์ ์ฌ์ด๋ ํ๋ก์ ํธ์์๋ ์ ๋ ฅ๊ฐ์ ๋ํ ์๋ฒ ์ ํจ์ฑ ๊ฒ์ฌ๋ ์ถ๊ฐํด์ผ๊ฒ ๋ค)
๊ทธ๋ ๋ค๋ฉด, ์ด์ฐจํผ ์๋ฒ์์๋ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํด์ฃผ๋๋ฐ ํ๋ก ํธ์์๋ ํ๋ ์ด์ ๋?
- ์๋ฒ ๋ถํ๋ฅผ ์ค์ฌ์ฃผ๊ณ , ์ฌ์ฉ์์ ํธ์์ฑ์ ์ํด์์ด๋ค.
- ์๋ฒ์์์ ์ ํจ์ฑ ๊ฒ์ฌ๋ ํ๋ก ํธ์์์ ์ ํจ์ฑ ๊ฒ์ฌ๋ณด๋ค๋ ๋๋ฆด ์ ๋ฐ์ ์๋ค.
๐ ์ค๋น
- ์์กด์ฑ ์ถ๊ฐ
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
์์ ์์กด์ฑ์ ์ถ๊ฐํ๋ฉด, Bean Validation ๊ตฌํ์ฒด๋ก Hibernate Validator๋ฅผ ์ฌ์ฉํ์ฌ Spring Boot์์ Validation์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
๐ ๋ฐฉ๋ฒ1 - Hibernate์์ ์ ๊ณตํ๋ javax.validation.constraints(@NotEmpty, @NotNull ๋ฑ)์ ๊ธฐ์กด ๊ตฌํ๋ ์ด๋ ธํ ์ด์ ์ฌ์ฉ
@Getter
@Setter
@NoArgsConstructor
publicclass UserSignUpReqDto {
...
@NotEmpty(message = "๋๋ค์์ ํ์์
๋๋ค.")
private String nickName; //๋๋ค์
@NotEmpty(message = "์ด๋ฉ์ผ์ ํ์์
๋๋ค.")
@Email
private String email; //์ด๋ฉ์ผ
...
}
์์ ๊ฐ์ด ๊ตฌํ๋์ด ์๋ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด ์ ์ฉ์ด ๊ฐ๋ฅํ๋ค.
@NotEmpty, @NotNull, @NotBlank์ ์ฐจ์ด๋?
- @NotEmpty - null, ""์ ํ์ฉํ์ง ์๋๋ค. " "๋ ํ์ฉํ๋ค.
- @NotNull - null๋ง ํ์ฉํ์ง ์๋๋ค. "", " "๋ ํ์ฉํ๋ค.
- @NotBlank - null, "", " "์ ๋ชจ๋ ํ์ฉํ์ง ์๋๋ค.
๐ ๋ฐฉ๋ฒ2 - ConstraintValidator ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํด Custom Validation ๊ตฌํ
Custom Validation ์ฌ์ฉ ์ด์
- ์ฌ์ฉ์ ์ ๋ ฅ ๊ฐ์ ์ ํจ์ฑ์ ์ธ๋ถ์ ์ผ๋ก ๊ฒ์ฌํด์ผํ ๋ ์ฌ์ฉํ๋ค.
- ์์ธ ๊ฒ์ฆ ๋ก์ง์ ์์ฑํด๋์ด ๋ฐ๋ณต์ฝ๋๋ฅผ ์ค์ด๊ณ , ์ด๋ ธํ ์ด์ ์ผ๋ก ๊ฐ๊ฒฐํ๊ฒ ์ ์ฉ์ด ๊ฐ๋ฅํ๋ค.
์ฃผ์ํ ์
- ์ด๋ ธํ ์ด์ ์ ๋ด๋ถ ๋ก์ง์ด ์ด๋ค ๋์์ ํ๋์ง ์ ํํ ๋ชจ๋ฅธ๋ค๋ฉด, ํ๋ก์ฐ๋ฅผ ์ดํดํ๊ธฐ ์ด๋ ต๋ค.
- ๋ฐ๋ณต์ ์ผ๋ก ์ฌ์ฉํ์ง ์๊ณ , ํน์ ์์ฒญ์๋ง ์ฌ์ฉํ๋ ์ ํจ์ฑ ๊ฒ์ฌ์ ๊ฒฝ์ฐ์๋ ์ด๊ฒ ๋์ ๋จ์ ๋ฉ์๋๋ก ์ฒ๋ฆฌํ๋๊ฒ ์ข๋ค.
๊ตฌํ ๋ฐฉ๋ฒ
- ๊ธฐ์กด์ ๊ตฌํ๋์ด์๋ @NotEmpty๋ @NotNull๊ณผ ๋์ผํ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ฉด ๋๋ค.
- ์ถ๊ฐํด์ผํ ํ์ผ์ ์ปค์คํ ์ด๋ ธํ ์ด์ interface์ ConstraintValidator ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ์ปค์คํ Validator ํด๋์ค ์ด 2๊ฐ์ง ์ด๋ค.
์ค์ต
ํ์๊ฐ์ ์ ๋ณด ์ ๋ ฅ ์ ์ ํ๋ฒํธ์ ๋น๋ฐ๋ฒํธ ํ์์ ์ฒดํฌํด์ฃผ๋ custom validator๋ฅผ ๊ตฌํํด๋ณด์๋ค.
๐ฉ๐ป๐ป ์ค์ต ์ฝ๋ ํ์ธ : spring-validation-practice
A. ์ปค์คํ ์ด๋ ธํ ์ด์ interface
// SignUpDtoCheck interface
@Constraint(validatedBy = SignUpDtoCheckValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignUpDtoCheck {
String message() default "SignUpDtoCheck is invalid";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
- @Constraint
- validatedBy์ ๊ฐ์ ConstraintValidator ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ์ปค์คํ Validator ํด๋์ค๋ฅผ ์ง์ ํด์ค๋ค. ์ค์ง์ ์ธ ๊ฒ์ฆ์ด ์ด๋ค์ง๋ ํด๋์ค์ด๋ค.
- @Target
- ํด๋น ์ด๋ ธํ ์ด์ ์ด ์ ์ฉ๋ ์ ์๋ ํ์ ์ ์ง์ ํ๋ ๊ฒ์ด๋ค.
- ElementType.TYPE์ ๊ฒฝ์ฐ ํด๋์ค, ์ธํฐํ์ด์ค, Enum์ ์ ์ฉ ๊ฐ๋ฅํ๋ค.
- @Retention
- ํด๋น ์ด๋ ธํ ์ด์ ์ ๋ผ์ดํ ์ฌ์ดํด์ ์ง์ ํ๋ ๊ฒ์ด๋ค.
- @interface
- ์ด๋ ธํ ์ด์ ์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅํ ์ธํฐํ์ด์ค๋ฅผ ์๋ฏธํ๋ค.
B. ConstraintValidator ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ์ปค์คํ Validator ํด๋์ค
public class SignUpDtoCheckValidator implements ConstraintValidator<SignUpDtoCheck, UserDto.UserSignUpReqDto> {
@Override
public void initialize(SignUpDtoCheck constraintAnnotation) {
}
@Override
public boolean isValid(UserDto.UserSignUpReqDto value, ConstraintValidatorContext context) {
int invalidCount = 0;
PatternUtils patternUtils = new PatternUtils();
if(!patternUtils.phoneNumberMatch(value.getPhoneNumber())) {
addConstraintViolation(context, "์ ํ๋ฒํธ๋ ์ซ์ 8~11์๋ง ๊ฐ๋ฅํฉ๋๋ค.", "phoneNumber");
invalidCount++;
}
if (!patternUtils.passwordMatch(value.getPassword())) {
addConstraintViolation(context, "๋น๋ฐ๋ฒํธ๋ ์๋ฌธ์ 4~6์๋ง ๊ฐ๋ฅํฉ๋๋ค.", "password");
invalidCount++;
}
return invalidCount == 0;
}
private void addConstraintViolation(ConstraintValidatorContext context, String errorMessage, String firstNode) {
context.disableDefaultConstraintViolation()
context.buildConstraintViolationWithTemplate(errorMessage)
.addPropertyNode(firstNode)
.addConstraintViolation();
}
}
- ConstraintValidator ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๊ธฐ ์ํด์๋ initialize()์ isValid() ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉ ํด์ค์ผ ํ๋ค.
- isValid() ๋ด๋ถ์ ๊ฒ์ฆ ๋ก์ง์ ๊ตฌํํ์ฌ boolean์ผ๋ก ๊ฐ์ ๋ฐํํด์ค๋ค.
- addConstraintViolation()
- disableDefaultConstraintViolation() - SignUpDtoCheck ์ธํฐํ์ด์ค์ ์ค์ ํด๋ default message ๋์ ๋ค๋ฅธ ๋ด์ฉ์ ์ฌ์ฉํ๋๋ก ํ๋ค.
- ๊ตฌ์ฒด์ ์ธ ์๋ฌ ๋ฉ์์ง์ ๊ฒ์ฆํ Node Key๊ฐ์ ๋๊ฒจ์ค๋ค. ํด๋น Node๋ Errors ๊ฐ์ฒด์ errors ๋ฆฌ์คํธ์ ์ ์ฅ๋ ์์์ field ์์ฑ์ ๋ฐ์ธ๋ฉ๋๋ค.
C. Validation ์ ์ฉ
@Getter @Setter
@NoArgsConstructor
@SignUpDtoCheck //์ด๋
ธํ
์ด์
์ ์ฌ์ฉํด ์ ์ฉ
public static class UserSignUpReqDto {
@NotEmpty(message = "๋๋ค์์ ํ์์
๋๋ค.")
private String nickName; //๋๋ค์
private String phoneNumber; //์ ํ๋ฒํธ
@NotEmpty(message = "์ด๋ฉ์ผ์ ํ์์
๋๋ค.")
@Email
private String email; //์ด๋ฉ์ผ
private String password; //๋น๋ฐ๋ฒํธ
@Builder
public UserSignUpReqDto(String nickName, String phoneNumber, String email, String password) {
this.nickName = nickName;
this.phoneNumber = phoneNumber;
this.email = email;
this.password = password;
}
}
//Controller
@PostMapping("/user/sign-up")
public String userSignUp(@Valid UserDto.UserSignUpReqDto userSignUpReqDto, Errors errors, Model model) {
//ํ์๊ฐ์
์คํจ
if (errors.hasErrors()) {
return "user/sign-up";
}
//ํ์๊ฐ์
์ฑ๊ณต (๋ก๊ทธ์ธ ์๋ฃ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ์
)
return "user/login";
}
๐ ์ฐธ๊ณ
- https://tecoble.techcourse.co.kr/post/2021-06-21-custom-annotation/
- https://cheese10yun.github.io/ConstraintValidator/
๐ ์์ ๋ฐฉ๋ฒ1๊ณผ ๋ฐฉ๋ฒ2์ ๋ชจ๋ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์ ๋ํ ์ฃผ์์ฌํญ
๋ง์ฝ ๋์ผํ ํ๋์ @NotNull๊ณผ Validator ํด๋์ค๋ฅผ ์ง์ ๊ตฌํํ ๋ฐฉ์์ ๋ชจ๋ ์ ์ฉํ์๋ค๋ฉด, @NotNull๊ณผ ์ง์ ๊ตฌํํ Validator๋ฅผ ๋ชจ๋ ๊ฑฐ์น๊ฒ ๋๋ค.
@NotNull์์ ์ฒดํฌ๊ฐ ๋์๋ค๊ณ ๊ตฌํํ Validator ํด๋์ค์ isValid()๋ฅผ ์๊ฑฐ์น๋ ๊ฒ์ด ์๋๋ผ ๋ฌด์กฐ๊ฑด ๋๋ค ๊ฑฐ์น๊ฒ ๋๋ค.
์ด๋ ์ ์ ํ if๋ฌธ ์กฐ๊ฑด ์ฒ๋ฆฌ๋ฅผ ํ์ง ์๋๋ค๋ฉด, null ๊ฐ์ด ๋ค์ด์์ ๋ ๋์ผํ ํ๋์ ๋ํด 2๊ฐ์ error๊ฐ ๋ฐํ๋๋ค.
๋ฐ๋ผ์ ์์ Validator ํด๋์ค์์๋ง ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํ๊ฒ ํ๋ ์ง, ํน์ Validator ํด๋์ค ๋ด๋ถ์ if๋ฌธ์ ์กฐ๊ฑด์ ์ ์ ์ํด์ค์ผ ํ๋ค. (์ด๊ฒ์ ๊ฐ๊ณผํ๊ณ ์ค๋ฅ๊ฐ 2๊ฐ ๋์จ๋ค๋ ๊ฒ์ ์ข ์ง๋์์ผ ์๊ฒ ๋์๋ค..^^)
โถ๏ธ ์์ ์ฝ๋
//@NotBlank๋ฅผ ์ค์ ํด์คฌ์ ๋
public class sampleDto {
...
@NotBlank
private String email;
...
}
//์ ๋ฌ๋ ๊ฐ์ด blank๋ null์ด ์๋ ๊ฒฝ์ฐ์๋ง ์ ํจ์ฑ ๊ฒ์ฌ ์ํ
public class sampleValidator implements ConstraintValidator<...> {
@Override
public boolean isValid(...) {
...
if (!StringUtils.isBlank(value.getEmail()) && !validationUtils.emailMatch(value.getEmail())) {
addConstraintViolation(context, "์ด๋ฉ์ผ ํ์์ผ๋ก ์
๋ ฅํด์ฃผ์ธ์.", "email");
invalidCount++;
}
}
}
์์ ๊ฐ์ ์์ ์ํฉ์์ ๋ง์ฝ @NotBlank๋ก ์ค์ ํด์คฌ๋ค๋ฉด, ์ปค์คํ Validator ํด๋์ค์์๋ blank๋ null์ด ์๋ ๊ฒฝ์ฐ์๋ง ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ถ๊ฐ๋ก ์ํํ๋๋ก ํ๋ ๊ฒ์ด๋ค.