[스프링] 스프링5 프로그래밍 입문 - 12장 MVC 2: 메시지, 커맨드 객체 검증

June·2021년 6월 16일
0
post-thumbnail
<label>이메일</label>
<input type = "text" name = "email">

이렇게 문자열을 하드코딩하면 문자열을 변경할 때 문제가 있다. 만약 문자열이 바뀌면 하나씩 직접 바꿔야하기 때문이다

또 다른 문제점은 다국어 지원에서 문제가 있다.

해결 책은 뷰 코드에서 사용할 문자열을 언어별로 파일에 보관하고 뷰 코드는 언어에 따라 알맞은 파일에서 문자열을 읽어와 출려사는 것이다.

  1. 문자열을 담은 메시지 파일을 작성한다.
  2. 메시지 파일에서 값을 읽어오는 MessageSource 빈을 설정한다.
  3. JSP 코드에서 <spring:message> 태그를 사용해서 메시지를 출력하낟.

메시지 파일은 자바의 프로퍼티 파일 형식으로 작성한다.

src/main/resources/message/label.properties

member.register=회원가입

term=약관
term.agree=약관동의
next.btn=다음단계

member.info=회원정보
email=이메일
name=이름
password=비밀번호
password.confirm=비밀번호 확인
register.btn=가입 완료

register.done=<strong>{0}님 ({1})</strong>, 회원 가입을 완료했습니다.

go.main=메인으로 이동

required=필수항목입니다.
bad.email=이메일이 올바르지 않습니다.
duplicate.email=중복된 이메일입니다.
nomatch.confirmPassword=비밀번호와 확인이 일치하지 않습니다.

MvcConfig

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

    ...
    
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        ms.setBasenames("message.label");
        ms.setDefaultEncoding("UTF-8");
        return ms;
    }
}

메시지 처리를 위한 MessageSource와 <spring:message> 태그

스프링은 로케일(지역)에 상관없이 일관된 방법으로 문자열(메시지)을 관리할 수 있는 MessageSource 인터페이스를 정의하고 있다.

pulic interface MessageSource {
    String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
    
    String getMessage(String code, Object[] args, Locale locale);
    ...
}

locale 파이미터는 지역을 구분하기 위한 locale이다. 같은 코드라 하더라도 지역에 따라 다른 메시지를 제공할 수 있도록 MessageSource를 설계했다. 이 기능을 사용하면 국내에서 접근하면 한국어로, 해외에서 접근하면 영어로 보여준다.

커맨드 객체의 값 검증과 에러 메시지 처리

폼 값 검증과 에러 메시지 처리는 어플리케이션을 개발할 때 놓쳐서는 안된다.

커맨드 객체 검증과 에러 코드 지정하기

RegisterRequestValidator

public class RegisterRequestValidator implements Validator {

    private static final String emailRegExp =
            "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" +
                    "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
    private Pattern pattern;

    public RegisterRequestValidator() {
        pattern = Pattern.compile(emailRegExp);
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return RegisterRequest.class.isAssignableFrom(aClass);
    }

    @Override
    public void validate(Object target, Errors errors) {
        RegisterRequest regReq = (RegisterRequest) target;
        if (regReq.getEmail() == null || regReq.getEmail().trim().isEmpty()) {
            errors.rejectValue("email", "required");
        } else {
            Matcher matcher = pattern.matcher(regReq.getEmail());
            if (!matcher.matches()) {
                errors.rejectValue("email", "bad");
            }
        }
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmPassword", "required");
        if (!regReq.getPassword().isEmpty()) {
            if (!regReq.isPasswordEqualToConfirmPassword()) {
                errors.rejectValue("confirmPassword", "nomatch");
            }
        }
    }
}

supports() 메서드느 파라미터로 전달받은 객체가 RegisterRequest 클래스로 타입 변환이 가능한지 확인한다.

target 파라미터는 검사 대상 객체이고, errors 파라미터는 검사 결과 에러 코드를 설정하기 위한 객체이다. validate() 메서드는 보통 아래와 같이 구현한다

  • 검사 대상 객체의 특정 프로퍼티나 상태가 올바른지 검사
  • 올바르지 않다면 Errors의 rejectValue() 메서드를 이용해서 에러 코드 저장

검사 대상 객체인 taret을 파라미터로 전달하지 않았는데 어떻게 target 객체의 "name" 프로퍼티의 값을 검사할까? Errors 객체에 비밀이 있다. Errors 타입 파라미터를 아래처럼 받고, 이 객체를 Validator의 validate() 메서드에 전달한다.

RegisterController

@Controller
public class RegisterController {
	...

    @PostMapping("/register/step3")
    public String handleStep3(RegisterRequest regReq, Errors errors) {
        new RegisterRequestValidator().validate(regReq, errors);
        if (errors.hasErrors()) {
            return "register/step2";
        }
        try {
            memberRegisterService.regist(regReq);
            return "register/step3";
        } catch (DuplicateMemberException ex) {
            errors.rejectValue("email", "duplicate");
            return "register/step2";
        }
    }
}

Errors 객체는 커맨드 객체의 특정 프로퍼티 값을 구할 수 있는 getFieldValue() 메서드를 제공한다.

// errors 객체의 getFeildValue("name") 메서드를 실행해서
// 커맨드 객체의 name 프로퍼티 값을 구함
// 따라서 커맨드 객체를 직접 전달하지 않아도 값 검증을 할 수 있음
ValidationUtils.rejectIfEmptyorWhitespace(errors, "name", "required");

new RegisterRequestValidator().validate(regReq, errors)에서 validate()를 실행하는 과정에서 유효하지 않은 값이 존재하면 Errors의 rejectValue() 메서드를 실행한다. 이 메서드가 한 번이라도 불리면 Errors의 hasErrors() 메서드는 true를 실행한다.

글로벌 범위 Validator와 컨트롤러 범위 Validator

스프링 MVC는 모든 컨트롤러에 적용할 수 있는 글로벌 Validator와 단일 컨트롤러에 적용할 수 있는 Validator를 설정하는 방법을 제공한다.

글로벌 범위 Validator 설정과 @Valid 애노테이션

글로벌 범위 Validator는 모든 컨트롤러에 적용할 수 있는 Validator이다.

  • 설정 클래스에서 WebMvcConfigurer의 getValidator() 메서드가 Validator 구현 객체를 리턴하도록 구현
  • 글로벌 범위 Validator가 검증할 커맨드 객체에 @Valid 애노테이션 적용

MvcConfig

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
    
    @Override
    public Validator getValidator() {
        return new RegisterRequestValidator();
    }
	...
}

스프링 MVC는 WebMvcConfigurer 인터페이스의 getValidator() 메서드가 리턴한 객체를 글로벌 범위 Validator로 사용한다.

RegisterController

@Controller
public class RegisterController {

    ...

    @PostMapping("/register/step3")
    public String handleStep3(@Valid RegisterRequest regReq, Errors errors) {
        new RegisterRequestValidator().validate(regReq, errors);
        if (errors.hasErrors()) {
            return "register/step2";
        }
        try {
            memberRegisterService.regist(regReq);
            return "register/step3";
        } catch (DuplicateMemberException ex) {
            errors.rejectValue("email", "duplicate");
            return "register/step2";
        }
    }
}

커맨드 객체에 해당하는 파라미터에 @Valid 애노테이션을 붙이면 글로벌 범위가 Validator가 해당 타입을 검증할 수 있는지 확인한다. 검증 가능하면 실제 검증을 수행하고 그 결과를 Errors에 저장한다. 이는 요청 처리 메서드 실행 전에 적용된다.

검증 수행 결과는 Errors 타입 파라미터로 받고, 이 과정은 handleStep3() 메서드가 실행되기 전에 이뤄지므로, 파라미터로 전달받은 Errors를 이용해서 검증에러가 존재하는지만 확인하면 된다.

@InitBinder 애노테이션을 이용한 컨트롤러 범위 Validator

@InitBinder 애노테이션을 이용하면 컨트롤러 범위 Validator를 설정할 수 있다.

RegisterController

@Controller
public class RegisterController {

    ...

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.setValidator(new RegisterRequestValidator());
    }
}

어떤 Validator가 커맨드 객체를 검증할지는 initBinder() 메서드가 결정한다. @InitBinder 애노테이션을 적용한 메서드는 WebDataBinder 타입 파라미터를 갖는데 setValidator() 메서드를 이용해서 컨트롤러 범위에 적용할 Validator를 설정할 수 있다.

참고로 @InitBinder가 붙은 메서드는 컨트롤러의 요청 처리 메서드를 실행하기 전에 매번 실행된다. 예를들어 위의 코드에서 컨트롤러 요청 처리 메서드인 handleStep1(), handleStep2(), handleStep3()을 실행하기 전에 initBinder() 메서드를 매번 호출해서 WebDataBinder를 초기화한다.

Bean Validation을 이용한 값 검증 처리

Bean Validation이 제공하는 애노테이션을 이용해서 커맨드 객체의 값을 검증하는 방법은 다음과 같다.

  • Bean Validation과 관련된 의존을 설정에 추가한다.
  • 커맨드 객체에 @NotNull, @Digits 등의 애노테이션을 이용해서 검증 규칙을 설정한다.
public class RegisterRequest {
    @NotBlank
    @Email
    private String email;
    @Size(min = 6)
    private String password;
    @NotEmpty
    private String confirmPassword;
    @NotEmpty
    private String name;
}

Bean Validation 애노테이션을 사용했다면 그 다음으로 할 작업은 Bean Validation 애노테이션을 적용한 커맨드 객체를 검증할 수 있는 OptionalValidatorFactoryBean 클래스를 빈으로 등록하는 것이다.

@EnableWebMvc 애노테이션을 사용하면 OptionalValidatorFactoryBean을 글로벌 범위 Validator로 등록하므로 추가로 설정할 것은 없다.

MvcConfig

@Configuration
@EnableWebMvc // OptionalValidatorFactoryBean을 글로벌 범위 Validator로 등록
public class MvcConfig implements WebMvcConfigurer {

	...
}

0개의 댓글