한 컨트롤러에서 여러 개의 Validator 추가하기

컨트롤러에 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까지로 생각한다면, 함께 메시지를 공유하는 것이 가능해진다.

객체 오류

  1. code + “.” + object name
  2. code

필드 오류

  1. code + “.” + object name + “.” + field
  2. code + “.” + field
  3. code + “.” + field type
  4. 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:/";

}

이렇게 해주면 모델명을 바꿀 필요가 없어진다. 훨씬 간단하고 깔끔한 방법이다.

댓글

개발자  김철준

백엔드 개발자 김철준의 블로그입니다.

주요 프로젝트