유효성을 검사하는 것은 어떤 데이터의 값이 유효한지, 타당한 것인지 확인하는 것이다.
간단한 예를 들면 어떤 페이지의 회원가입을 하다보면 비밀번호를 8자리이상 입력하라거나 이메일 양식이 일치하지 않는다는 메세지등이 곧 유효성 검사를 하는 과정에서 나온것이다.
- 웹 브라우저 : 자바스크립트로 웹서버에 전송하기 전 검사한다.
- 웹 서버: 전달 받은 요청 파라미터를 검사한다.
자바스크립트에서 유효성 검사를 하지만 서버에서도 유효성 검사를 반드시 해줘야 한다.
> sample.xml
<bean id="memService" class="member.MemberService"/>
<bean class="member.RegistrationController">
<property name="memberService" ref="memService"></property>
</bean>
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>message.error</value>
</list>
</property>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
Spring에서 Message Properties를 사용하기 위한 방법은 2가지가 있는데, ResourceBundleMessageSource 와 ReloadableResourceBundleMessageSource 클래스다. (ReloadableResourceBundleMessageSource 클래스는 해당 Properties 파일이 변경되었을 경우에도 애플리케이션을 다시 시작할 필요하 없다는 장점이 있음)
하지만 이번 예제에서는 ResourceBundleMessageSource를 defaultEncoding통해 UTF-8로 설정하여 사용해보겠다.
> error.properties
required=필수 항목입니다.
required.email=이메일을 입력하세요.
minlength=최소 {1} 글자 이상 입력해야 합니다.
maxlength=최대 {1} 글자까지만 입력해야 합니다.
invalidIdOrPassword.loginCommand=아이디와 암호가 일치하지 않습니다.
invalidPassword=암호가 일치하지 않습니다.
shortPassword=암호 길이가 짧습니다.
notSame.confirmPassword=입력한 값이 암호와 같지 않습니다.
typeMismatch.birthday=날짜 형식이 올바르지 않습니다.
NotEmpty=필수 항목입니다.
NotEmpty.currentPassword=현재 암호를 입력하세요.
Email=올바른 이메일 주소를 입력해야 합니다.
badBeginDate=이벤트 시작일이 잘못되었습니다.
badEndDate=이벤트 종료일이 잘못되었습니다.
typeMismatch.beginDate=시작 날짜 형식이 올바르지 않습니다.
typeMismatch.endDate=종료 날짜 형식이 올바르지 않습니다.
실제 properties파일을 들어가보면 글자가 깨져있다. 메모장을 UTF-8로 저장하여 확인할 수 있다.
MemberRegistValidator.java
public class MemberRegistValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return MemberRegistRequest.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
MemberRegistRequest regReq = (MemberRegistRequest) target;
if(regReq.getEmail()==null || regReq.getEmail().trim().isEmpty()) {
errors.rejectValue("email", "required");
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmPassword", "required");
if(regReq.hasPassword()) {
if(regReq.getPassword().length() < 5) {
errors.rejectValue("password", "shortPassword");
}
else if(!regReq.isSamePasswordConfirmPassword()) {
errors.rejectValue("confirmPassword", "notSame");
}
}
Address address = regReq.getAddress();
if(address==null) {
errors.rejectValue("address", "required");
}
else {
errors.pushNestedPath("address");
try {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "address1", "required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "address2", "required");
} finally {
errors.popNestedPath();
}
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "birthday", "required");
}
}
Validator 인터페이스를 상속한 클래스는 두 개의 메서드를 구현해야 한다.
1. boolean supports(Class clazz) : 어떤 타입의 객체를 검증할 때 이 객체의 클래스가 Validator가 검증할 수 있는 클래스인 지를 판단하는 메서드
2. void validate(Object target, Errors error) : 실제 검증 로직이 이루어지는 메서드, 구현할 때 ValidationUtils를 사용하여 편리하게 작성 가능
supports 메서드에서 인자로넘어온 객체의 클래스가 지정한 형식의 인스턴스를 현재 형식의 변수에 할당할 수 있는지 여부를 결정.
validate 메서드에서는 password의 길이가 5자리 이상인지 검증하는 등 여러가지 조건문을 걸어주며 rejectValue()로 필드(에 대한 에러정보 추가(에러코드 및 메시지, 메시지 인자 전달), ValidationUtils의 rejectIfEmptyOrWhitespace() 를 사용하여 객체의 각 필드가 비어있거나 공백일 경우에는 errors에 에러 정보를 담는 로직을 구현하였다. 각각 error.properties에 들어있는 키 값을 넣어줬다.
registrationForm.jsp 의 일부분
<spring:hasBindErrors name="memberInfo"/>
<form method="post">
<label for="password">암호</label>:
<input type="password" name="password" id="password" value="${memberInfo.password}"/>
<form:errors path="memberInfo.password"/><br/>
<label for="password">암호확인</label>:
<input type="password" name="confirmPassword" id="confirmPassword" value="${memberInfo.confirmPassword}"/>
<form:errors path="memberInfo.confirmPassword"/><br/>
</form>
<spring:hasBindErrors> name 속성에 커맨드객체명을 넣고,
<form:errors> path 속성에 에러코드(커맨드객체명.이름)를 입력한다.
샘플로 가입시 유효성검사를 통해 조건으로 준 에러메세지들이 옆에 나오도록 jsp파일을 만들어 실행해보았다.
에러메세지들이 잘 나오는 것을 확인할 수 있다.
그 외 클래스 및 메서드들
MemberInfo.java
public class MemberInfo {
private String id;
private String name;
private String email;
private String password;
private boolean allowNoti;
private Address address;
public MemberInfo(String id, String name, String email, String password, boolean allowNoti, Address address) {
this.id = id;
this.name = name;
this.email = email;
this.password = password;
this.allowNoti = allowNoti;
this.address = address;
}
public boolean matchPassword(String inputPassword) {
return password.equals(inputPassword);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = 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 boolean isAllowNoti() {
return allowNoti;
}
public void setAllowNoti(boolean allowNoti) {
this.allowNoti = allowNoti;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
Address.java
public class Address {
private String address1;
private String address2;
private String zipcode;
public String getAddress1() {
return address1;
}
public void setAddress1(String address1) {
this.address1 = address1;
}
public String getAddress2() {
return address2;
}
public void setAddress2(String address2) {
this.address2 = address2;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
}
MemberRegistRequest.java
public class MemberRegistRequest {
private String email;
private String name;
private String password;
private String confirmPassword;
private boolean allowNoti;
private Address address;
private Date birthday;
public boolean isSamePasswordConfirmPassword() {
if(password == null || confirmPassword == null) {
return false;
}
return password.equals(confirmPassword);
}
public boolean hasPassword() {
return password != null && password.trim().length() > 0;
}
@DateTimeFormat(pattern="yyyyMMdd")
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
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 boolean isAllowNoti() {
return allowNoti;
}
public void setAllowNoti(boolean allowNoti) {
this.allowNoti = allowNoti;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public Date getBirthday() {
return birthday;
}
}
> RegistrationController.java
@Controller
@RequestMapping("/member/regist")
public class RegistrationController {
private static final String MEMBER_REGISTRATION_FROM = "member/registrationForm";
private MemberService memberService;
@RequestMapping(method = RequestMethod.GET)
public String form(@ModelAttribute("memberInfo") MemberRegistRequest memRegReq) {
return MEMBER_REGISTRATION_FROM;
}
@RequestMapping(method = RequestMethod.POST)
public String regist(@ModelAttribute("memberInfo") MemberRegistRequest memRegReq, BindingResult bindingResult) {
new MemberRegistValidator().validate(memRegReq, bindingResult);
if(bindingResult.hasErrors()) {
return MEMBER_REGISTRATION_FROM;
}
memberService.registNewMember(memRegReq);
return "member/registered";
}
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
BindingResult의 hasErrors() 메서드는 검증 결과 오류가 있다면 true를 리턴한다!
> MemberService.java
public class MemberService {
private int nextMemberId = 0;
private Map<String, MemberInfo> memberMap = new HashMap<>();
public MemberService() {
memberMap.put("m1", new MemberInfo("m1","이상화","sanghwa@sanghwa.com","sanghwa",
false,new Address()));
memberMap.put("m2", new MemberInfo("m2","김연아","yuna@yuna.com","yuna",
false,new Address()));
nextMemberId = 3;
}
public void registNewMember(MemberRegistRequest memRegReq) {
MemberInfo mi = new MemberInfo("m"+nextMemberId,
memRegReq.getName(), memRegReq.getEmail(), memRegReq.getPassword(),
memRegReq.isAllowNoti(), memRegReq.getAddress());
memberMap.put(mi.getId(),mi);
}
}
registrationForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html><html><head><title>Insert title here</title></head><body>
<spring:hasBindErrors name="memberInfo"/>
<form method="post">
<label for="email">이메일</label>:
<input type="text" name="email" id="email" value="${memberInfo.email}"/>
<form:errors path="memberInfo.email"/><br/>
<label for="name">이름</label>:
<input type="text" name="name" id="name" value="${memberInfo.name}"/>
<form:errors path="memberInfo.name"/><br/>
<label for="password">암호</label>:
<input type="password" name="password" id="password" value="${memberInfo.password}"/>
<form:errors path="memberInfo.password"/><br/>
<label for="password">암호확인</label>:
<input type="password" name="confirmPassword" id="confirmPassword" value="${memberInfo.confirmPassword}"/>
<form:errors path="memberInfo.confirmPassword"/><br/>
<label>주소</label>:
주소1
<input type="text" name="address.address1" value="${memberInfo.address.address1}"/>
<form:errors path="memberInfo.address.address1"/><br/>
주소2
<input type="text" name="address.address2" value="${memberInfo.address.address2}"/>
<form:errors path="memberInfo.address.address2"/><br/>
우편번호
<input type="text" name="address.zipcode" value="${memberInfo.address.zipcode}"/>
<form:errors path="memberInfo.address.zipcode"/><br/>
<label>
<input type="checkbox" name="allowNoti" value="true" ${memberInfo.allowNoti ? 'checked' : '' }/>
이메일을 수신합니다.
</label><br/>
<label for="birthday">생일</label> - 형식 : YYYYMMDD, ex) 19950731
<input type="text" name="birthday" id="birthday"
value='<fmt:formatDate value="${memberInfo.birthday}" pattern="yyyyMMdd" /> '/>
<form:errors path="memberInfo.birthday"/><br/>
<input type="submit" value="가입하기" />
</form></body></html>
registrationForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
${memberInfo.name} 님이 회원가입을 완료했습니다.
</body>
</html>