컨트롤러에 Validator 추가하기
@InitBinder 애노테이션을 사용하면, 모든 요청 전에 Validator를 붙여준다.
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(loginValidator);
}
그렇다면, 단순히 addValidator을 계속 해주면 될까? Validator를 추가하는 것과 모델에 Validation을 하는 것은 다르다. 모델에 @Valitate가 붙어있으면, 해당 모델을 검증하는데, Validator의 supports를 통과하지 못하면 단순히 패스하는 것이 아니라, 예외를 발생시키기 때문에 문제가 발생한다.
Validator의 supports
스프링의 Validator를 구현한 Validator는 이런 식으로 구현했다.
@Override
public boolean supports(Class clazz) {
return LoginUserDto.class.isAssignableFrom(clazz);
}
해결 방법 1
InitBinder에 이름을 붙여주자
@InitBinder("joinUser")
public void initJoinBinder(WebDataBinder dataBinder) {
dataBinder.addValidators(joinValidator);
}
@InitBinder("loginUser")
public void initLoginBinder(WebDataBinder dataBinder) {
dataBinder.addValidators(loginValidator);
}
@PostMapping("/join")
public String join(@Validated @ModelAttribute("joinUser") JoinUserDto user, BindingResult bindingResult) {
log.debug("objectName={}", bindingResult.getObjectName());
log.debug("target={}", bindingResult.getTarget());
log.debug("Input User DTO: {}", user);
if (bindingResult.hasErrors()) {
log.debug("errors={}", bindingResult);
return "home/join";
}
// 회원가입 후에 홈으로 리다이렉트
return "redirect:/";
}
@PostMapping("/login")
public String login(@Validated @ModelAttribute("loginUser") LoginUserDto user, BindingResult bindingResult) {
log.debug("objectName={}", bindingResult.getObjectName());
log.debug("target={}", bindingResult.getTarget());
log.debug("Input User DTO: {}", user);
if (bindingResult.hasErrors()) {
log.debug("errors={}", bindingResult);
return "home/login";
}
// 로그인 후에 홈으로 리다이렉트
return "redirect:/";
}
특정 ModelAttribute가 사용하도록, 통일시켜주는 방법이 있다. 이렇게 하면 적절한 Validator가 부착되고, 실행된다. 이 때문에, 모델명을 모두 user로 통일했었지만, loginUser, joinUser로 분리했다.
메시지명에서 문제 발생
errors.properties 파일에서 에러 메시지를 정의해두었는데, 같은 에러를 쓰고 싶은데 모델명이 달라 문제가 발생했다.
loginGlobal=로그인에 실패했습니다. 아이디 및 패스워드를 확인하세요.
required.user.username=유저이름은 필수입니다.
required.user.password=패스워드는 필수입니다.
required=필수 값입니다.
duplicated.user.username=중복된 유저 이름입니다.
min.user.username=유저이름은 {0}자 이상으로 해주세요.
min.user.password=패스워드는 {0}자 이상으로 해주세요.
min=최소 {0}자 이상으로 해주세요.
duplicated=중복된 이름입니다.
public static final String REQUIRED_FIELD = "required";
public static final String LOGIN_ERROR = "loginGlobal";
...
@Override
public void validate(Object target, Errors errors) {
...
// 필수값 검증
if (!StringUtils.hasText(user.getUsername()) || !StringUtils.hasText(user.getPassword())) {
if (!StringUtils.hasText(user.getUsername())) {
errors.rejectValue("username", REQUIRED_FIELD);
}
if (!StringUtils.hasText(user.getPassword())) {
errors.rejectValue("password", REQUIRED_FIELD);
}
return; // 이후 검증 불필요
}
...
}
MessageCodesResolver 메시지 생성 규칙
객체오류와 필드오류의 생성 규칙은 이렇게 되어 있다. 따라서, 코드를 require이 아닌 require.user까지로 생각한다면, 함께 메시지를 공유하는 것이 가능해진다.
객체 오류
- code + “.” + object name
- code
필드 오류
- code + “.” + object name + “.” + field
- code + “.” + field
- code + “.” + field type
- code
상수 수정
이런 식으로, 코드명 자체를 required명까지 보던 것을 required.user까지로 보면 loginUser 모델, joinUser모델 둘 다 같은 메시지 필드를 공유해서 사용하는 것이 가능해진다. min, duplicated또한 min.user, duplicated.user로 보면 된다.
public static final String REQUIRED_FIELD = "required.user";
public static final String LOGIN_ERROR = "loginGlobal";
해결 방법 2
1의 방법으로는 우선적으로 메시지를 사용은 가능하지만, 범용성 있게 사용이 어렵다는 점이 아쉽다. @InitBinder을 씀으로써 이렇게 많은 것을 포기해야만 할까? 한 컨트롤러에 하나의 Validator만 사용한다면 유용하지만, 이런 경우 그냥 컨트롤 메서드 안에, 직접 validator.valdate()를 해주는 게 낫다고 보았다.
@PostMapping("/login")
public String login(@Validated @ModelAttribute("user") LoginUserDto user, BindingResult bindingResult) {
loginValidator.validate(user, bindingResult);
if (bindingResult.hasErrors()) {
log.debug("errors={}", bindingResult);
return "home/login";
}
// 로그인 후에 홈으로 리다이렉트
return "redirect:/";
}
이렇게 해주면 모델명을 바꿀 필요가 없어진다. 훨씬 간단하고 깔끔한 방법이다.