Chapter 12 - MVC 연습(2) : 메시지, 커맨드 객체 검증

이현빈·2024년 3월 26일

1. 프로젝트 준비

이전 포스트에서 작성했던 코드를 이어서 작성했다. 여기에 유효성 검사를 수행하기 위한 다음의 의존 설정을 pom.xml에 추가했다.

	<!--jakarta.validation : Java 애플리케이션에서 객체의 유효성 검증 규칙 & 규격 정의 -->
	<dependency>
      <groupId>jakarta.validation</groupId>
      <artifactId>jakarta.validation-api</artifactId>
      <version>3.0.2</version>
    </dependency>

    <!-- hibernate : Java 환경에서 객체의 유효성 검증 기능을 실제로 제공하는 모듈, 
					 Bean Validation 적용 목적으로 사용 -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>8.0.1.Final</version>
    </dependency>

2. <spring:message> 태그로 메시지 출력하기

문자열이 View 코드에 하드코딩되어 있는 경우, 다음의 문제들이 발생한다.

1. View 코드 내 동일 문자열 변경의 번거로움
View 코드에 직접 문자열을 삽입할 경우, 해당 문자열이 여러 개의 View 코드에서 반복적으로 사용된다면 각각의 jsp 파일마다 해당 문자열이 사용된 부분을 찾아 일일이 변경해야 한다는 번거로움이 발생하여 코드의 유지보수를 어렵게 한다.
2. View 코드에서의 다국어 지원 문제
전 세계를 대상으로 서비스를 제공해야 하는 경우, 사용자의 언어 설정에 맞게 문자열을 표시해야 한다. 하지만 View 코드에 문자열이 하드코딩되어 있다면 동일한 View 코드를 언어별로 여러 개 작성해야 하는 상황이 발생한다.

위와 같은 문제를 해결하는 최선의 방법은 문자열을 언어별로 파일에 보관하고, View 코드에서 언어 설정에 맞는 파일로부터 문자열을 읽어오게 하는 것이다.

메시지 처리용 MessageSource와 <spring:message> 태그

jsp 파일에서의 문자열 하드코딩으로 인한 문제를 해결하는 최선의 방법은 문자열을 언어별로 파일에 보관하고, View 코드에서 언어 설정에 맞는 파일로부터 문자열을 읽어오게 하는 것이다.
Spring에서는 자체적으로 이러한 기능을 제공하므로, jsp 코드에 대해 다음의 작업을 수행하면 된다.

  1. 문자열을 담은 메시지 파일을 별도로 작성
    • src/main/resource/message 폴더 내부에 label.properties 파일을 생성하여 UTF-8 인코딩을 적용한 다음, 내용을 작성한다. [Intellij에서의 properties 인코딩 설정 방법]
    • 다국어 메시지를 지원할 경우 각 properties 파일 이름에 지원할 언어에 해당하는 Locale 문자를 추가한다.(ex: label_ko.properties(한글), label_en.properties(영어))
      별도의 다국어 메시지 지원이 없다면 언어 구분이 없는 label.properties 파일을 사용한다.
  2. 메시지 파일로부터 값을 읽어올 MessageSource Bean을 Spring 설정 클래스에 추가
  3. jsp 코드로 출력할 메시지에 <spring:message> 태그 적용하기
    • <spring:message> 사용을 위한 아래의 taglib 설정을 jsp 파일에 추가
      		  <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
    • <spring:message> 태그를 이용한 메시지를 출력하도록 jsp 코드를 수정한다.
      [<spring:message> 활용 jsp 코드 예시]

MessageSource 인터페이스

package org.springframework.context;

import java.util.Locale;
import org.springframework.lang.Nullable;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;

public interface MessageSource {
    @Nullable
    String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

    String getMessage(java.lang.String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

    ... // 일부 메서드 생략
}
  • String code : 메시지 식별용
  • Object[] args : 인덱스 기반 변수값 전달을 위한 객체 배열
  • Locale locale : 지역 구분용

MessageSource 인터페이스의 구현체로는 ResourceBundleMessageSource 클래스를 사용한다. 이 클래스는 프로퍼티 파일 내 메시지 코드와 동일한 이름을 갖는 프로퍼티의 값을 메시지로 제공한다. ResourceBundleMessageSource는 java의 리소스번들(ResourceBundle)을 사용하므로 메시지를 읽어올 프로퍼티 파일은 src/main/resource 폴더나 그 하위 폴더에 저장되어야 한다.

src/main/resource/message/label.properties (프로퍼티 작성 예시)

member.register=회원가입

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

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

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

go.main=메인으로 이동

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

NotBlank=필수 항목입니다. 공백문자는 허용하지 않습니다.
NotEmpty=필수 항목입니다. 빈 칸으로 둘 수 없습니다.
Size.password=암호 길이는 6자 이상이어야 합니다.
Email=올바른 이메일 주소를 입력해야 합니다.

MessageSource 활용 예시

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

    ... 코드 생략

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        
        /* 1. message 패키지 내 label.properties 파일을 읽어오도록 설정 */
        ms.setBasenames("message.label");
        
        /* 2. 프로퍼티 파일에 UTF-8 인코딩 적용 */
        ms.setDefaultEncoding("UTF-8");
        return ms;
    }
}

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

유효성 검사 & 에러 메시지 처리를 수행하지 않을 시 발생하는 문제점

  1. 검출하지 못한 잘못된 입력값이 시스템의 비정상적인 동작을 유발
  2. 에러메시지 미출력으로 인해 사용자가 서비스를 제대로 이용하는 것이 불가능

따라서 Form 값의 검증과 에러 메시지 처리는 어플리케이션 개발 시 절대 놓치지 말아야 한다. Spring은 이러한 문제들을 처리하기 위해 다음과 같은 방법을 제공한다.

  • 커맨드 객체의 검증 수행 & 검증 결과를 에러 코드로 저장
  • jsp를 통해 에러 발생 코드로부터 에러 메시지를 출력

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

커맨드 객체 검증과 에러 코드 지정에 필요한 기능들을 제공하는 Validator, Errors, ValidationUtil은 모두 org.springframework.validation 패키지에 포함되어 있다.

Validator 인터페이스

Spring MVC에서 커맨드 객체를 검증할 때 사용되는 기능을 제공하는 인터페이스이다.

package org.springframework.validation;

import java.util.Objects;
import java.util.function.BiConsumer;

public interface Validator {
    boolean supports(Class<?> clazz);

    void validate(Object target, Errors errors);

    ... 코드 생략
}
  • boolean supports(Class<?> clazz)
    : Validator가 검증 가능한 타입인지 검사
  • void validate(Object target, Errors errors)
    : 첫번째 파라미터로 전달받은 객체의 특정 프로퍼티나 상태가 올바른지 검증하고, 에러 발생 시 두번째 파라미터인 Errors의 rejectValue() 메서드를 이용하여 커맨드 객체의 해당 프로퍼티에 관한 에러 코드를 저장한다.

Errors 인터페이스

에러 발생 여부를 검사하고, 에러가 발생했을 때 에러 메시지를 출력하기 위한 에러 코드를 지정하는 기능을 제공하는 인터페이스이다. 주요 메서드는 아래와 같다.

public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
public void rejectValue(@Nullable String field, String errorCode, 
					@Nullable Object[] errorArgs, @Nullable String defaultMessage);
  • reject() 메서드
    : 커맨드 객체 자체가 잘못되었을 때, 커맨드 객체에 에러를 추가 (글로벌 에러)
  • rejectValue() 메서드
    : 커맨드 객체 프로퍼티의 값이 잘못되었을 때, 해당 프로퍼티에 에러를 추가

ValidationUtils 클래스

객체의 값을 검증하는 코드를 간결하게 작성하기 위한 기능을 제공하는 클래스로, 에러 코드 추가에 관한 주요 메서드는 아래와 같다.


public static void rejectIfEmpty(Errors errors, String field,
						String errorCode, @Nullable Object[] errorArgs);
public static void rejectIfEmptyOrWhitespace(Errors errors, String field, 
						String errorCode, @Nullable Object[] errorArgs);
  • rejectIfEmpty() 메서드
    : field에 해당하는 프로퍼티 값이 null이거나 빈 문자열("")인 경우 errorCode를 에러 코드로 추가한다. 만약 errorCode의 에러 메시지가 인덱스 기반 변수값을 포함한다면 객체 배열인 errorArgs를 이용하여 메시지에 포함된 각 변수값에 삽입할 값을 전달한다.
  • rejectIfEmptyOrWhitespace() 메서드
    : field에 해당하는 프로퍼티 값이 null값, 빈 문자열(""), 공백문자 중 하나인 경우 errorCode를 에러 코드로 추가한다. 만약 errorCode의 에러 메시지가 인덱스 기반 변수값을 포함하면 객체 배열인 errorArgs를 이용하여 메시지에 포함된 각 변수값에 삽입할 값을 전달한다.

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);
    }

    /* 파라미터로 전달받은 클래스를 RegisterRequest 클래스 타입으로 변환 가능한지 확인 */
    @Override
    public boolean supports(Class<?> clazz) {
        return RegisterRequest.class.isAssignableFrom(clazz);
    }

    /* 회원 가입 Form 입력값 검증 수행 */
    @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.rejectIfEmpty(errors, "password", "required");
        ValidationUtils.rejectIfEmpty(errors, "confirmPassword", "required");

        /* 비밀번호 확인 유효성 검사: 처음 입력한 비밀번호와 재입력한 비밀번호가 서로 일치해야 유효 */
        if (!regReq.getPassword().isEmpty()) {
            if (!regReq.isPasswordEqualToConfirmPassword()) {
                errors.rejectValue("confirmPassword", "nomatch");
            }
        }
    }
}

4. Validator 적용: 글로벌 범위 & 컨트롤러 범위

글로벌 범위 Validator 설정과 @Valid

글로벌 범위 Validator는 모든 컨트롤러에 대해 적용 가능한 Validator로, 다음의 2가지 작업을 모두 수행하면 적용할 수 있다.

  1. WebMvcConfigurer를 상속한 Spring 설정 클래스의 getValidator() 메서드가 Validator 구현 객체를 반환하도록 구현
  2. 글로벌 범위 Validator가 검증할 커맨드 객체에 @Valid 적용

주요 어노테이션

  • @Valid
    : 이 어노테이션은 요청 처리 메서드의 커맨드 객체에 해당하는 파라미터에 사용된다.
    이 어노테이션을 사용했을 시, 요청 처리 메서드가 실행되기 전에 해당 커맨드 객체의 타입을 Validator가 검증할 수 있는지 확인한다. 만약 해당 커맨드 객체를 검증 가능하다면 실제 검증을 수행한 후 그 결과를 Errors 타입 파라미터에 저장한다.

@Valid 어노테이션을 커맨드 객체에 사용했을 때, 해당 커맨드 객체를 파라미터로 사용하는 요청 처리 메서드에 Errors 타입 파라미터가 없으면 HTTP Status 400 에러가 발생한다.

@InitBinder를 이용한 컨트롤러 범위 Validator 설정

주요 어노테이션

  • @InitBinder
    이 어노테이션이 적용된 메서드는 파라미터로 WebDataBinder 객체를 사용함으로써 해당 메서드가 정의된 컨트롤러에 적용할 Validator를 결정할 수 있다. 그리고 이 어노테이션을 사용하는 메서드는 컨트롤러의 요청 처리 메서드를 실행하기 전에 매번 실행된다.

주요 메서드
@InitBinder 메서드의 파라미터로 사용되는 WebDataBinder 객체에서는 아래와 같은 메서드들을 통해 컨트롤러 범위에 적용할 Validator를 설정할 수 있다. 사용하는 메서드에 따라 적용되는 Validator의 가짓수가 달라질 수 있다.

  • void setValidator(Validator validator)
    WebDataBinder 객체에 저장된 기존의 Validator를 해당 메서드의 인자로 전달받은 Validator로 대체한다. 따라서 글로벌 범위 Validator를 대신하여 컨트롤러 범위 Validator만 적용한다.
  • void addValidator(Validator validator)
    WebDataBinder 객체에 저장된 기존의 Validator에 해당 메서드의 파라미터로 사용된 Validator를 추가한다. 글로벌 범위 Validator를 저장한 후에 컨트롤러 범위 Validator를 새로 추가하게 되므로 각 범위의 Validator를 모두 적용할 수 있다. 이 경우, 글로벌 범위 Validator를 먼저 적용한 다음 컨트롤러 범위 Validator를 적용하게 된다.

5. Bean Validation을 이용한 값 검증

Bean Validation의 어노테이션을 사용하면 커맨드 객체에 관한 별도의 Validator를 작성할 필요 없이 어노테이션만으로도 그 값을 검증하는 것이 가능하다.

Bean Validator 제공 어노테이션 사용법

1. Bean Validator 관련 의존을 pom.xml에 추가

	<!-- jakarta.validation : Java 애플리케이션에서 객체의 유효성 검증 규칙 & 규격 정의  -->
	<dependency>
      <groupId>jakarta.validation</groupId>
      <artifactId>jakarta.validation-api</artifactId>
      <version>3.0.2</version>
    </dependency>
    <!-- hibernate : Java 환경에서 Bean Validation을 실제로 사용하기 위한 모듈 -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>8.0.1.Final</version>
    </dependency>

2. 커맨드 객체에 @NotNull, @Digits 등의 어노테이션을 사용하여 검증 규칙 설정

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;

public class RegisterRequest {

    @NotBlank
    @Email
    private String email;

    @Size(min = 6)
    private String password;
    
    @NotEmpty
    private String confirmPassword;
    
    @NotEmpty
    private String name;
    
    ... 코드 생략
}

3. OptionalValidatorFactoryBean 클래스를 Bean으로 등록

해당 클래스는 Spring MVC에서 자체적으로 제공하는 글로벌 범위 Validator로, Bean Validation 어노테이션을 적용한 커맨드 객체를 검증할 때 사용된다. 만약 @EnableWebMvc 어노테이션을 사용했다면 해당 클래스 타입의 Validator를 글로벌 범위 Validator로 자동 등록하므로 별도의 설정을 수행하지 않아도 된다.
만약 별도로 지정한 글로벌 범위 Validator가 존재한다면 해당 글로벌 범위 Validator에 관한 설정을 삭제해야 OptionalValidatorFactoryBean 타입 Validator를 글로벌 범위 Validator로 사용할 수 있다.

Bean Validation의 주요 어노테이션 (~2.0 버전)

cf) @NotEmpty, @NotBlank, @Email 사용 관련 부연 설명
실습에 사용했던 Hibernate 8.0.1 버전 기준 org.hibernate.validator.constraints 패키지에 포함된 @Email, @NotBlank, @NotEmpty는 deprecated된 상태로, Hibernate 공식 가이드에서는 jakarta.validation.constraints 패키지에 포함된 어노테이션을 사용할 것을 권고하고 있다.
따라서 실제로 실습할 때는 jakarta.validation.constraints 패키지에 정의된 어노테이션들을 대신 사용하였다.
[Hibernate 8.0 버전 기준 deprecated 목록]
[Hibernate에서 @NotEmpty, @NotBlank 등의 어노테이션이 deprecated된 배경에 관한 참고글]


Reference

  • 초보 웹 개발자를 위한 스프링5 프로그래밍 입문(최범균)

0개의 댓글