회원가입 시 사용자가 입력한 정보가 서버로 전송되기 전에 특정 규칙에 맞게 입력됐는지, 아이디와 닉네임이 중복됐는지 등 확인하는 검증 단계가 필요하다.
@Validated
는 스프링 전용 검증 애노테이션이고, @Valid
는 자바 표준 검증 애노테이션이다. 둘 다 동일한 기능. 단 @Validated 는 groups 기능 포함Bean Validation
Bean Validation
을 잘 활용하면, 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.
- 검증 애노테이션과 여러 인터페이스의 모음
@ModelAttribute 각각의 필드에 타입 변환 시도
값을 넣으며 타입 변환 시도
시
1) 성공하면 다음으로
2) 실패하면 typeMismatch
로 FieldError
추가
Validator 적용
바인딩에 성공한 필드만 Bean Validation
적용
BeanValidator
는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다.
생각해보면 타입 변환에 성공해서 바인딩에 성공한 필드여야 BeanValidation 적용이 의미 있다. (일단 모델 객체에 바인딩 받는 값이 정상으로 들어와야 검증도 의미가 있다.)
@ModelAttribute
→ 각각의 필드 타입 변환 시도 → 변환에 성공한 필드만BeanValidation
적용
implementation 'org.springframework.boot:spring-boot-starter-validation'
유효성 검사에 필요한 RequestDto 객체에 Validation 어노테이션 사용
@NotBlank(message = "아이디는 필수 입력값입니다.")
@Pattern(regexp = "^[a-z0-9]{4,20}$", message = "아이디는 영어 소문자와 숫자만 사용하여 4~20자리여야 합니다.")
private String username;
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,16}$", message = "비밀번호는 8~16자리수여야 합니다. 영문 대소문자, 숫자, 특수문자를 1개 이상 포함해야 합니다.")
private String password;
@NotBlank(message = "닉네임은 필수 입력값입니다.")
@Pattern(regexp = "^[가-힣a-zA-Z0-9]{2,10}$" , message = "닉네임은 특수문자를 포함하지 않은 2~10자리여야 합니다.")
private String nickname;
@NotBlank(message = "이메일은 필수 입력값입니다.")
@Email
private String email;
RequestDto 객체 앞에 붙은 @Valid 어노테이션을 사용하고, Errors를 통해 유효성 검사 적합 여부 확인
@PostMapping("/auth/joinProc")
public String joinProc(@Valid UserDto.RequestUserDto dto, BindingResult bindingResult, Model model) {
/* 검증 */
if(bindingResult.hasErrors()) {
/* 회원가입 실패 시 입력 데이터 값 유지 */
model.addAttribute("userDto", dto);
/* 유효성 검사를 통과하지 못 한 필드와 메시지 핸들링 */
Map<String, String> errorMap = new HashMap<>();
for(FieldError error : bindingResult.getFieldErrors()) {
errorMap.put("valid_"+error.getField(), error.getDefaultMessage());
log.info("error message : "+error.getDefaultMessage());
}
/* 회원가입 페이지로 리턴 */
return "/user/user-join";
}
// 회원가입 성공 시
userService.userJoin(dto);
return "redirect:/auth/login";
}
/* 유효성 검사 - 중복 체크
* 중복 : true
* 중복이 아닌 경우 : false
*/
boolean existsByUsername(String username);
boolean existsByNickname(String nickname);
boolean existsByEmail(String email);
existsBy
키워드 사용/* 아이디, 닉네임, 이메일 중복 여부 확인 */
@Transactional(readOnly = true)
@Override
public boolean checkUsernameDuplication(String username) {
boolean usernameDuplicate = userRepository.existsByUsername(username);
return usernameDuplicate;
}
@Transactional(readOnly = true)
@Override
public boolean checkNicknameDuplication(String nickname) {
boolean nicknameDuplicate = userRepository.existsByNickname(nickname);
return nicknameDuplicate;
}
@Transactional(readOnly = true)
@Override
public boolean checkEmailDuplication(String email) {
boolean emailDuplicate = userRepository.existsByEmail(email);
return emailDuplicate;
}
/* 아이디, 닉네임, 이메일 중복 체크 */
@GetMapping("/auth/joinProc/{username}/exists")
public ResponseEntity<Boolean> checkUsernameDuplicate(@PathVariable String username){
return ResponseEntity.ok(userService.checkUsernameDuplication(username));
}
@GetMapping("/auth/joinProc/{nickname}/exists")
public ResponseEntity<Boolean> checkNicknameDuplicate(@PathVariable String nickname){
return ResponseEntity.ok(userService.checkUsernameDuplication(nickname));
}
@GetMapping("/auth/joinProc/{email}/exists")
public ResponseEntity<Boolean> checkEmailDuplicate(@PathVariable String email){
return ResponseEntity.ok(userService.checkUsernameDuplication(email));
}
원래는 jquery의 .blur 기능을 통해 마우스 포커스가 벗어나면 바로 중복체크 하게 하려고 했는데, 그러기 위해서는
1. 필드 조건에 맞지 않은 에러들도 포커스가 벗어나자마자 나타나도록 바꿔야 함
2. 유효성 검사를 모두 통과한 데이터만이 가입할 수 있도록 제한해야 함
이 두 가지를 해결해야 했다.
이 부분이 중점이 아니기 때문에 우선 회원가입
버튼 누를 때 두 유효성 검사를 다 실행할 수 있도록 (v1) 로직을 바꿨다.
(네이버 회원가입창 처럼 데이터를 입력하고 마우스 포커스가 벗어나면 바로 중복확인/가입조건에 맞는지 확인하도록 메시지가 아래 뜨도록 하고 싶었음)
package com.jy.web.validator;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import lombok.extern.slf4j.Slf4j;
/* 중복 검사를 위한 Validator 구현 추상 클래스 */
@Slf4j
public abstract class AbstractValidator<T> implements Validator{
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@SuppressWarnings("unchecked")
@Override
public void validate(Object target, Errors errors) {
try {
doValidate((T) target, errors); // 유효성 검증 로직
} catch (IllegalStateException e) {
log.error("중복 검증 에러", e);
throw e;
}
}
/* 유효성 검증 로직 */
protected abstract void doValidate(final T dto, final Errors errors);
}
supports()
@Validated 는 검증기를 실행하라는 애노테이션.
이 애노테이션이 붙으면 아래에 나오는 WebDataBinder
에 등록한 검증기를 찾아서 실행한다.
그런데 여러 검증기(Validator
)를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports() 가 사용된다.
검증 로직이 들어갈 부분은 doValidate 메서드로 따로 작성해둔다.
컴파일러에서 경고하지 않도록 @SuppressWarnings("unchecked")
설정
@SuppressWarnings("unchecked")?
AbstractValidator
추상 클래스를 상속받은 각각 필드의 중복 검사 클래스를 만들어보자. - CheckUsernameValidator, CheckNicknameValidator, CheckEmailValidator
- doValidate 메소드를 오버라이딩하여 중복 검사 로직을 구현한다.
package com.jy.web.validator;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import com.jy.domain.user.UserRepository;
import com.jy.web.dto.UserDto;
import com.jy.web.dto.UserDto.RequestUserDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Component
public class CheckUsernameValidator extends AbstractValidator<UserDto.RequestUserDto>{
private final UserRepository userRepository;
@Override
protected void doValidate(RequestUserDto dto, Errors errors) {
if (userRepository.existsByUsername(dto.toEntity().getUsername())) {
/* 중복인 경우 */
errors.rejectValue("username","아이디 중복 오류", "이미 사용 중인 아이디입니다.");
}
}
}
rejectValue
- FieldError, ObjectError을 직접 생성하지 않고 깔끔하게 검증 오류를 다룰 수 있다.
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
Validator 클래스 사용을 위해 @InitBinder
이 붙은 WebDataBinder
을 인자로 받는 메소드를 작성해 검증 Validator 추가
InitBinder
: 특정 컨트롤러에서 바인딩 또는 검증 설정을 변경하고 싶을 때 사용
WebDataBinder binder
: HTTP 요청 정보를 컨트롤러 메소드의 파라미터나 모델에 바인딩할 때 사용되는 바인딩 객체. 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다. WebDataBinder
에 addValidators
메소드를 이용해 검증기(Validator
)를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다. @InitBinder 해당 컨트롤러에만 영향을 준다.즉, 컨트롤러가 요청 올 때마다 WebDataBinder가 호출되면서 WebDataBinder 에 등록한 검증기가 매번 적용되게 할 수 있음
UserController
...
/* 중복 체크 유효성 검사 */
private final CheckUsernameValidator checkUsernameValidator;
private final CheckNicknameValidator checkNicknameValidator;
private final CheckEmailValidator checkEmailValidator;
/* 커스텀 유효성 검증 */
@InitBinder
public void validatorBinder(WebDataBinder binder) {
binder.addValidators(checkUsernameValidator);
binder.addValidators(checkNicknameValidator);
binder.addValidators(checkEmailValidator);
}
회원가입을 진행하는 joinProc 메소드에서 error에 함께 포함되어 view단에서 에러 메시지를 출력할 수 있다. (폼 입력 시 오류가 발생하면, 고객이 입력한 데이터를 유지한 상태로 어떤 오류가 발생했는지 친절하게 알려주어야 한다.)
Validator
을 구현한 커스텀Validation
으로 검증 로직을 작성한다.
해당 Validator을 사용하기 위해 Controller에 @InitBinder가 붙은 WebDataBinder을 파라미터로 받는 메서드를 반드시 추가해준다.
출처
김영한의 스프링 MVC 2편
https://blog.naver.com/lovewouldnever/222543144867