여니의 프로그래밍 study/Spring & Spring Boot

[Spring] MVC 2 : 메시지, 커맨드 객체 검증

여니's 2023. 12. 8. 21:41

참고 자료 : 초보 개발자를 위한 스프링5 프로그래밍 입문

 


<Chapter 12. MVC 2 : 메시지, 커맨드 객체 검증 >

1. spring:message 태그로 메시지 출력하기

 

> 뷰 코드에 문자열이 하드코딩 되어 있으면

언어별로 뷰 코드를 따로 만들어야하는 문제점이 발생한다.

 

 

해결방법 

> 문자열을 별도 파일에 작성하고

JSP 코드에서 이를 사용하면 된다.

 

 

1.문자열을 담은 메시지 파일을 작성

: 자바의 프로퍼티 파일 형식으로 작성

 

 

2.메시지 파일에서 값을 읽어오는 MessageSource 빈을 설정

: MvcConfig 설정 클래스에 추가

 

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

 

 

basenames 프로퍼티의 값은 message.label

> message 패키지에 속한 label이라는 프로퍼티 파일로부터

메시지를 읽어온다는 의미

 

 

3. JSP 코드에서 <spring:message> 태그를 사용해 메시지 출력

 

 

<title>회원가입</title>을

<title><spring:message code="member.register"/></title>로

변경해준다. 

 

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<!DOCTYPE html>
<html>
<head>
    <title><spring:message code="member.register" /></title>
</head>
<body>
<h2><spring:message code="term" /></h2>
<p>약관 내용</p>
<form action="step2" method="post">
    <label>
        <input type="checkbox" name="agree" value="true">
        <spring:message code="term.agree" />
    </label>
    <input type="submit" value="<spring:message code="next.btn" />" />
</form>
</body>
</html>

 

 

<spring:message> 커스텀 태그를 사용하기 위해서는

태그 라이브러리 설정을 추가해줘야하고

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>

 

해당 태그를 이용해서 메세지를 출력해준다.

 

<spring:message> 태그는 MessageSource로부터

해당하는 메시지를 읽어온다.

 

MessageSource는 label.properties 파일로부터 

메시지를 읽어온다. 

 


 

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

스프링은 지역에 상관없이 일관된 방법으로

문자열(메시지)을 관리할 수 있는

MessageSource 인터페이스를 정의하고 있음.

 

특정 로케일(지역)에 해당하는 메시지가 필요한 경우

MessageSource 의 getMessage() 메서드를 이용하여

필요한 메시지를 가져와 사용한다.

 

getMessage() 메서드의 code 파라미터

> 메시지를 구분하기 위한 코드

 

locale 파라미터

> 지역을 구분하기 위한 locale

 

MessageSource의 구현체는

자바의 프로퍼티 파일로부터 메시지를 읽어오는

ResourceBundleMessageSource 클래스를 사용함

 

<spring:message> 태그는

스프링 설정에 등록된 'messageSource' 빈을 사용하여

메시지를 구한다.

 

즉, 내부적으로 MessageSource의 getMessage() 메서드를

실행해서 필요한 메시지를 구한다.

 

만약 <spring:message> 태그의 code 속성에

지정한 메시지가 존재하지 않으면

500 익셉션이 일어난다.

 


 

3. <spring:message> 태그의 메시지 인자 처리

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

 

{0}은 인덱스 기반 변수 중

0번 인덱스(첫 번째 인덱스)의 값으로

대치되는 부분을 표시한 것이다. 

 

Object[] args=new Object[1];
args[0]="자바";
messageSource.getMessage("register.done", args, Locale.KOREA);

 

<spring:message code="register.done" arguments="${registerRequest.name}"/>

 

 

arguments 속성을 사용해서

인덱스 기반 변수값을 전달한다. 

 

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

 

 

위 메시지를 사용하려면 2개의 인자를 전달해야 함

1.콤마로 구분한 문자열

2.객체 배열

3.<spring:argument> 태그 사용

 

<spring:message code="register.done" arguments="${registerRequest.name|, ${registerRequest.email|"/>

 


 

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

 

11장에서 작성한 회원가입 처리 코드는

입력한 값에 대한 검증 처리를 진행하지 않는다.

 

또한, 가입 실패에 대한 이유를 알려주지 않는다. 

 

스프링은 두  가지 문제를 처리하기 위해 다음 방법을 제공하고 있다

 

1. 커맨드 객체를 검증하고 결과를 에러 코드로 저장

2. JSP에서 에러 코드로부터 메시지 출력

 

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

 

커맨드 객체값이 올바른지 검사하기 위해

다음의 두 인터페이스를 사용함

 

-org.springframework.validation.Validator

-org.springframework.validation.Errors

 

public interface Validator {
	boolean supports(Class<?> clazz);
    	void validate(Object target, Errors erros);
}

 

supports() 메서드는 Validator가 검증할 수 있는 타입인지 검사를 한다.

validate 메서드는 첫 번째 파라미터로 전달받은 객체를 검증하고

오류 결과를 Errors에 담는 기능을 정의함

 

package controller;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

import spring.RegisterRequest;

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);
		System.out.println("RegisterRequestValidator#new(): " + this);
	}

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

	@Override
	public void validate(Object target, Errors errors) {
		System.out.println("RegisterRequestValidator#validate(): " + this);
		RegisterRequest regReq = (RegisterRequest) target;
        	// 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");
			}
		}
	}

}

 

 

> RegisterRequest 객체를 검증하기 위한

Validator 구현 클래스의 작성 예시

 

package controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import spring.DuplicateMemberException;
import spring.MemberRegisterService;
import spring.RegisterRequest;

@Controller
public class RegisterController {

	private MemberRegisterService memberRegisterService;

	public void setMemberRegisterService(
			MemberRegisterService memberRegisterService) {
		this.memberRegisterService = memberRegisterService;
	}

	@RequestMapping("/register/step1")
	public String handleStep1() {
		return "register/step1";
	}

	@PostMapping("/register/step2")
	public String handleStep2(
			@RequestParam(value = "agree", defaultValue = "false") Boolean agree,
			Model model) {
		if (!agree) {
			return "register/step1";
		}
		model.addAttribute("registerRequest", new RegisterRequest());
		return "register/step2";
	}

	@GetMapping("/register/step2")
	public String handleStep2Get() {
		return "redirect:/register/step1";
	}

	@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";
		}
	}

}

 

위와 같이 코드를 수정한다.

 

@PostMapping("/register/step3")
	public String handleStep3(RegisterRequest regReq, Errors errors) {
    	// RegisterRequestValidator 객체 생성 후 validate 메서드 실행
        // RegisterRequest 커맨드 객체의 값이 올바른지 검사후 결과를 Errors 객체에 담는다.
		new RegisterRequestValidator().validate(regReq, errors);
      	
        // validate() 메서드를 실행하는 과정에서
        // 유효하지 않은 값이 존재하면 Errors의 rejectValue() 메서드를 실행
        
		if (errors.hasErrors())
            // 이 메서드가 한 번이라도 불리면 true를 리턴함 (hasErrors는)
        	return "register/step2";

		try {
			memberRegisterService.regist(regReq);
            // 이미 존재하는 회원이면 DuplicateMemberException 익셉션 발생
		} catch (DuplicateMemberException ex) {
			errors.rejectValue("email", "duplicate");
			return "register/step2";
		}
	}

 

 

요청 매핑 어노테이션 적용 메서드의 커맨드 객체 파라미터 뒤에

Errors 타입 파라미터가 위치하면

스프링 MVC는 handleStep3 메서드 호출 시

커맨드 객체와 연결된 Errors 객체를 생성해서 파라미터로 전달함

 

해당 Errors 객체는 커맨드 객체의

특정 프로퍼티 값을 구할 수 있는 getFieldValue 메서드를 제공함.

 

 

커맨드 객체의 특정 프로퍼티가 아닌

커맨드 객체 자체가 잘못되었을 경우

rejectValue() 대신에 reject()를 사용함.

 

위 메서드는 개별 프로퍼티가 아닌 객체 자체에 에러 코드를 추가해서

글로벌 에러라고 부르기도 한다. 

 

단, 주의해야할 점은

Errors 타입 파라미터가 커맨드 객체 앞에 위치하면

익셉션이 발생한다

 

 

(2) Errors 와 ValidationUtils 클래스의 주요 메서드

 

에러 코드에 해당하는 메시지가

{0}이나 {1}과 같이 인덱스 기반 변수를 포함하고 있다면

Object 배열 타입의 errorArgs 파라미터를 이용하여

변수에 삽입될 값을 전달한다.

 

 

(3) 커맨드 객체의 에러 메시지 출력하기

 

<form:erros> 태그를 사용하여

에러에 해당하는 메시지를 출력할 수 있다.

 

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<!DOCTYPE html>
<html>
<head>
    <title><spring:message code="member.register" /></title>
</head>
<body>
    <h2><spring:message code="member.info" /></h2>
    <form:form action="step3" modelAttribute="registerRequest">
    <p>
        <label><spring:message code="email" />:<br>
        <form:input path="email" />
        <form:errors path="email"/>
        </label>
    </p>
    <p>
        <label><spring:message code="name" />:<br>
        <form:input path="name" />
        <form:errors path="name"/>
        </label>
    </p>
    <p>
        <label><spring:message code="password" />:<br>
        <form:password path="password" />
        <form:errors path="password"/>
        </label>
    </p>
    <p>
        <label><spring:message code="password.confirm" />:<br>
        <form:password path="confirmPassword" />
        <form:errors path="confirmPassword"/>
        </label>
    </p>
    <input type="submit" value="<spring:message code="register.btn" />">
    </form:form>
</body>
</html>

 

 

<form:errors> 태그의 path 속성은

에러 메시지를 출력할 프로퍼티 이름을 지정함

 

에러 코드에 해당하는 메시지를

메시지 프로퍼티 파일에도 추가해줘야한다

 

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=비밀번호와 확인이 일치하지 않습니다.

 

 

(4) <form:errors> 태그의 주요 속성

 

> 프로퍼티에 추가한 에러 코드 개수만큼 

에러 메시지를 출력한다. 

 

element : 각 에러 메시지를 출력할 때 사용하는 태그

delimiter : 각 에러 메시지를 구분할 때 사용할 태그

path 속성을 지정하지 않으면, 글로벌 에러에 대한 메시지를 출력함

 

<form:errors path="userId" element="div" delimeter""/>

 


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

 

스프링 MVC는 모든 컨트롤러에 적용할 수 있는 글로벌 Validator와

단일 컨트롤러에 적용할 수 있는 Validator를 설정하는 방법을

제공한다.

 

@Vadli 어노테이션을 사용하여

커맨드 객체에 검증 기능을 적용할 수 있음

 

(1) 글로벌 범위 Validator 설정과 @Valid 어노테이션

 

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

 

글로벌 범위 Validator 설정법

1. WebMvcConfigurer 인터페이스의 getValidator() 메서드 구현

: getValidator 메서드가 리턴한 객체를

글로벌 범위 Validator로 사용함

 

RegisterRequestValidator는 

RegisterRequest 타입에 대한 검증을 지원함

 

@Valid 어노테이션은 Bean Validation API 에 포함되어 있어서

의존 설정에 추가해줘야함

 

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>

 

커맨드 객체에 해당하는 파라미터에 @Valid 어노테이션을 붙이면

글로벌 범위 Validator가 해당 타입을 검증할 수 있는지 확인한다

 

검증이 가능하면 실제 검증을 수행하고

그 결과를 Errors에 저장한다. 

 

handleStep3() 메서드가 실행되기 전에 이루어지므로

해당 메서드는 RegisterRequest 객체를 검증하는 코드를 작성할 필요가 없다

전달받은 Errors를 이용하여

검증 에러가 존재하는지 확인하면 된다.

 

단, @Valid 어노테이션 사용 시

Errors 타입 파라미터가 없으면 검증 실패 시 400 에러를 응답한다. 

 

 

(2) @initBinder 어노테이션을 이용한 컨트롤러 범위 Validator

 

> 스프링 MVC는 에러 코드에 해당하는 메시지가 존재하지 않을 때

Bean Validation 프로바이더가 제공하는 

기본 에러 메시지를 출력함.

 


5. Bean Validation을 이용한 값 검증 처리

package spring;

import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;

public class RegisterRequest {
	@NotBlank
	@Email
	private String email;
	@Size(min = 6)
	private String password;
	@NotEmpty
	private String confirmPassword;
	@NotEmpty
	private String name;

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getConfirmPassword() {
		return confirmPassword;
	}

	public void setConfirmPassword(String confirmPassword) {
		this.confirmPassword = confirmPassword;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public boolean isPasswordEqualToConfirmPassword() {
		return password.equals(confirmPassword);
	}
}

 

 

@NotBlank 어노테이션으로 지정한 검사를 통과하지 못할 때

사용하는 메시지 코드는 아래와 같다

(커맨트 객체의 모델 이름을 registerRequest라고 가정)

 

NotBlank.registerRequest.name

NotBlank.name

NotBlank

 

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

 

 

(1) Bean Validation 의 주요 어노테이션

 

모든 어노테이션은 javax.validation.constraints 패키지에 정의되어 있음

 

@NotNull을 제외한 나머지 어노테이션은

검사 대상 값이 널인 경우 유효한 것으로 판단함

 

필수 입력값을 검사할 시에는

@NotNull , @Size를 함께 사용해야 함

 

@NotNull

@Size(min=1)

private String title;