Spring - Model Validation 방법!(파라미터 Validation),@Valid

2019. 9. 10. 17:06Web/Spring

 

우리는 컨트롤러에서 사용자가 넘겨준 파라미터를 전달 받을 때, @ModelAttribute를 붙여서 혹은 생략한 특정 객체로 파라미터를 받게된다. 우리는 이때 2가지 상황을 고려할 수 있다. 

 

  1. 파라미터 바인딩에 실패(데이터 타입 등이 맞지 않는 경우)
  2. 파라미터 바인딩은 문제없이 됬으나, 들어온 파라미터가 비즈니스 로직에 맞지 않는 혹은 유효하지 않은 파라미터 일경우

이러한 파라미터를 검증하는 방법은 무엇이 있을까? 크게 2가지 방법이 존재한다.

 

  1. 사용자 정의 Validator 구현
  2. JSR-303 애너테이션

간단히 두가지를 다루어본다.

 

사용자 정의 Validator

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Slf4j
@Component
public class UserParamValidator implements Validator {
 
    @Override
    public boolean supports(Class<?> aClass) {
//isAssignableFrom은 하위 타입도 적용되게 하는 메서드이다.
        return (User.class.isAssignableFrom(aClass));
    }
 
    @Override
    public void validate(Object o, Errors errors) {
        log.info("User Param Validator");
        User user = (User)o;
 
        if(StringUtils.isEmpty(user.getFirstName())){
            errors.rejectValue("firstName","param.required");
        }
 
    }
}
 
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
 
    @Autowired
    private UserParamValidator validator;
 
    @InitBinder
    public void initBinder(WebDataBinder webDataBinder){
 
        webDataBinder.setValidator(validator);
    }
 
    @GetMapping("/find")
    public Object findById(@ModelAttribute("findUser") @Valid User user, BindingResult bindingResult){
        List<String> resultMessages = new ArrayList<>();
        if(bindingResult.hasErrors()){
            List<FieldError> fieldsErrors = bindingResult.getFieldErrors();
            fieldsErrors.stream().forEach(fieldError->{
                log.info("bind error objectName = {}, fieldName = {} ,error code = {}",fieldError.getObjectName(),fieldError.getField(),fieldError.getCode());
            });
            return resultMessages;
        }
 
        if(user.getId() == 1){
            user.setLastName("yoon");
            user.setAge(28);
            user.setLevel(Level.BASIC);
 
            return user;
        }
 
        return null;
    }
 
}
 
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
class User{
 
    private int id;
    private String firstName;
    private String lastName;
    private int age;
    private Level level;
 
}
cs

 

위와 같이 사용자 정의 Validator 클래스를 정의하고 특정 컨트롤러에서 DI 받은 후, @InitBinder 메서드에 해당 Validator를 등록해준다. 그리고 검증이 실패한 값이 들어온다면 컨트롤러 BindingResult에 검증 실패한 정보가 저장된다. 이러한 검증 실패정보를 로그로 찍고 있는 예제이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
 
    @GetMapping("/find")
    public Object findById(@ModelAttribute("findUser") @Valid User user, BindingResult bindingResult, Locale locale){
        List<String> resultMessages = new ArrayList<>();
        if(bindingResult.hasErrors()){
            List<FieldError> fieldsErrors = bindingResult.getFieldErrors();
            fieldsErrors.stream().forEach(fieldError->{
                log.info("bind error objectName = {}, fieldName = {} ,error code = {}",fieldError.getObjectName(),fieldError.getField(),fieldError.getCode());
                resultMessages.add(messageSource.getMessage(fieldError.getField()+"."+fieldError.getCode(),null, locale));
            });
            return resultMessages;
        }
 
        if(user.getId() == 1){
            user.setLastName("yoon");
            user.setAge(28);
            user.setLevel(Level.BASIC);
 
            return user;
        }
 
        return null;
    }
 
}
 
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
class User{
 
    private int id;
    @NotNull
    private String firstName;
    private String lastName;
    private int age;
    private Level level;
 
}
cs

 

두번째 방법은 직접 엔티티 클래스에 제약조건 애너테이션을 달아주는 방법이다. 그러면 별도의 Validator DI 없이도 BindingResult에 엔티티 클래스에 있는 애너테이션을 이용한 검증을 한 후 검증에 실패하면 정보를 넣어준다.

 

만약 검증이 아니고 데이터 타입이 잘못넘어와서 바인딩 자체가 실패하면 어떻게 될까?

 

그렇다면 아래와 같은 errorCode가 발생한다.

 

FieldName.typeMismatch

 

좋다 여기까지, 바인딩 오류 혹은 검증이 실패한 결과가 BindResult에 담겼다. 그렇다면 그 이후에는 어떻게 처리하면 좋을까? 보통 이럴때는 MessageSource를 이용해 errorCode에 해당되는 미리 준비된 문자열을 사용자에게 보여준다. 예를 들면 아래와 같은 상황이다.

 

회원가입을 하는 과정에서 사용자가 userId값을 넘기지 않으면 "ID를 입력해주세요."라는 문구가 뿌려지는데, 이럴 경우 미리 준비된 errorCode에 해당되는 메시지를 준비하여 userId 파라미터가 빈값일때 해당 메시지를 전달해줄 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
 
    @Autowired
    private LocaleChangeInterceptor localeChangeInterceptor;
 
    @Value("${spring.messages.basename}")
    String baseName;
    @Value("${spring.messages.encoding}")
    String defaultEncoding;
    @Value("${cookie.locale.param}")
    String cookieName;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
 
        registry.addInterceptor(localeChangeInterceptor);
 
    }
 
    /*
     * MessageSource Bean register
     */
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = new ReloadableResourceBundleMessageSource();
        reloadableResourceBundleMessageSource.setBasename(baseName);
        reloadableResourceBundleMessageSource.setDefaultEncoding(defaultEncoding);
 
        return reloadableResourceBundleMessageSource;
    }
 
    /*
     * Locale Resolver Bean register
     */
    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
        cookieLocaleResolver.setCookieName(cookieName);
        cookieLocaleResolver.setDefaultLocale(Locale.KOREA);
 
        return cookieLocaleResolver;
    }
 
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName(cookieName);
 
        return  localeChangeInterceptor;
    }
}
cs

 

위와 같이 필요한 빈을 등록해준다.

 

 

message.properties 파일들을 생성해준다.

 

1
2
firstName.param.required=이름 입력은 필수입니다.
id.typeMismatch=유효하지 않은 유저 아이디 값입니다.
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
 
    @Autowired
    private UserParamValidator validator;
    @Autowired
    private MessageSource messageSource;
 
    @InitBinder
    public void initBinder(WebDataBinder webDataBinder){
        webDataBinder.setValidator(validator);
    }
 
    @GetMapping("/find")
    public Object findById(@ModelAttribute("findUser") @Valid User user, BindingResult bindingResult, Locale locale){
        List<String> resultMessages = new ArrayList<>();
        if(bindingResult.hasErrors()){
            List<FieldError> fieldsErrors = bindingResult.getFieldErrors();
            fieldsErrors.stream().forEach(fieldError->{
                log.info("bind error objectName = {}, fieldName = {} ,error code = {}",fieldError.getObjectName(),fieldError.getField(),fieldError.getCode());
                resultMessages.add(messageSource.getMessage(fieldError.getField()+"."+fieldError.getCode(),null, locale));
            });
            return resultMessages;
        }
 
        if(user.getId() == 1){
            user.setLastName("yoon");
            user.setAge(28);
            user.setLevel(Level.BASIC);
 
            return user;
        }
 
        return null;
    }
}
cs

 

바인딩 errorCode에 해당 되는 Message가 생성되는 것을 볼 수 있다.