회원가입 시 사용자가 입력한 정보가 서버로 전송되기 전에 특정 규칙에 맞게 입력됐는지, 아이디와 닉네임이 중복됐는지 등 확인하는 검증 단계가 필요하다.
@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