※ 본문은 혼자 공부한 내용을 기록한 글입니다. 오개념이 있다면 댓글로 알려주세요!
2023.01.29 - [나에 대한 기록] - [독서 기록] 스프링5 프로그래밍 입문 - 최범균 저
[독서 기록] 스프링5 프로그래밍 입문 - 최범균 저
https://search.shopping.naver.com/book/catalog/32458958626 스프링5 프로그래밍 입문 : 네이버 도서 네이버 도서 상세정보를 제공합니다. search.shopping.naver.com 지난 한 주 동안 최범균님의 스프링 5 프로그래밍
krchoish.tistory.com
※ 해당 책을 참고하여 프로젝트를 진행하였습니다.
[ 1 ] 커맨드 객체 검증을 위한 Validator 인터페이스
이전 게시글에서 작성한 회원가입 로직은 정상적으로 작동하지만, 비정상적인 값을 입력해도 작동한다는 문제가 있다. 예를 들어, 올바르지 않은 이메일 또는 생년월일을 입력하거나 이름이나 비밀번호를 입력하지 않아도 가입 처리가 된다. 이를 방지하기 위해 form에서 입력한 값에 대한 검증 처리를 해야 한다.
현재 form에서 입력한 값을 커맨드 객체로 받고 있는데, Spring MVC는 커맨드 객체의 값을 검증할 수 있는 Validator와 Errors 인터페이스를 제공한다.
이전 게시글에서 작성한 RegisterCommand 객체를 검증하는 클래스를 통해 Validator와 Errors 인터페이스를 알아보도록 하자.
package validator;
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 command.RegisterCommand;
public class RegisterCommandValidator 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 static final String birthdateRegExp =
"^(19[0-9][0-9]|20\\d{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$";
private Pattern email_pattern;
private Pattern birthdate_pattern;
public RegisterCommandValidator() {
email_pattern = Pattern.compile(emailRegExp);
birthdate_pattern = Pattern.compile(birthdateRegExp);
}
// clazz 객체가 RegisterCommand로 변환 가능한 지 확인 - Spring MVC가 자동 검증
@Override
public boolean supports(Class<?> clazz) {
return RegisterCommand.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
RegisterCommand regCommand = (RegisterCommand)target;
if(regCommand.getEmail() == null || regCommand.getEmail().trim().isEmpty()) {
errors.rejectValue("email", "required");
} else {
Matcher matcher = email_pattern.matcher(regCommand.getEmail());
if(!matcher.matches()) {
errors.rejectValue("email", "wrong");
}
}
if(regCommand.getBirthdate() == null || regCommand.getBirthdate().trim().isEmpty()) {
errors.rejectValue("birthdate", "required");
} else {
Matcher matcher = birthdate_pattern.matcher(regCommand.getBirthdate());
if(!matcher.matches()) {
errors.rejectValue("birthdate", "wrong");
}
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmPassword", "required");
if(!regCommand.getPassword().isEmpty()) {
if(!regCommand.isPasswordEqualToConfirmPassword()) {
errors.rejectValue("confirmPassword", "nomatch");
}
}
}
}
- supports(Class<?> clazz) 메서드
- clazz 객체가 RegisterCommand로 변환 가능한 지 확인한다.
- 메서드를 직접 실행하지 않아도 Spring MVC가 자동으로 검증 기능을 수행한다.
- validate(Object target, Errors errors) 메서드
- target 객체를 검증하고 오류 결과를 Errors에 담는다.
- 우선, 검사 대상의 값을 구하기 위해 target을 실제 타입(RegisterCommand)으로 변환한다.
- 커맨드 객체의 각 프로퍼티 값이 유효한지 검사하고, 유효하지 않다면 Errors의 rejectValue() 메서드로 해당 프로퍼티의 에러 코드를 설정한다. 첫 번째 매개변수로는 프로퍼티의 이름을, 두 번째 매개변수로는 에러 코드를 전달 받는다.
- 예를 들어, 아래 코드는 "birthdate" 프로퍼티 값이 null이거나 빈문자열인 경우 errors.rejectValue("birthdate", "required")를 통해 "birthdate" 프로퍼티의 에러 코드로 "required"를 추가한다. 아래의 else {} 문은 정규 표현식을 이용하여 생년월일이 올바른지 확인(yyyymmdd)하고, 정규 표현식이 일치하지 않으면 errors.rejectValue("birthdate", "wrong")를 통해 "birthdate" 프로퍼티의 에러 코드로 "wrong"을 추가한다.
if(regCommand.getBirthdate() == null || regCommand.getBirthdate().trim().isEmpty()) {
errors.rejectValue("birthdate", "required");
} else {
Matcher matcher = birthdate_pattern.matcher(regCommand.getBirthdate());
if(!matcher.matches()) {
errors.rejectValue("birthdate", "wrong");
}
}
- ValidationUtils 클래스로도 에러 코드를 추가할 수 있다. ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required"); 는 "name" 프로퍼티가 null이거나 공백문자로만 되어 있을 경우 "name" 프로퍼티의 에러 코드로 "required"를 추가한다.
[ 2 ] 커맨드 객체 검증하기
커맨드 객체를 검증하기 위해서 handlerSuccess() 요청 매핑 애노테이션 적용 메서드를 다음과 같이 수정한다.
package controller;
import javax.servlet.http.HttpServletRequest;
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.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import command.RegisterCommand;
import exception.DuplicateMemberException;
import service.RegisterService;
import validator.RegisterCommandValidator;
@Controller
public class RegisterController {
private RegisterService registerService;
public RegisterController(RegisterService registerService) {
this.registerService = registerService;
}
@GetMapping("/register/terms")
public String handlerterms() {
...
}
@PostMapping("/register/form")
public String handlerForm(HttpServletRequest request, Model model) {
...
model.addAttribute("registerCommand", new RegisterCommand());
return "register/form";
}
@GetMapping("/register/form")
public String handlerFormGet() {
...
}
@GetMapping("/register/success")
public String handlerSuccessGet() {
...
}
@PostMapping("/register/success")
public String handlerSuccess(@ModelAttribute("registerCommand") RegisterCommand registerCommand, Errors errors) {
new RegisterCommandValidator().validate(registerCommand, errors);
if(errors.hasErrors()) {
return "register/form";
}
try {
registerService.regist(registerCommand);
return "register/success";
} catch(DuplicateMemberException e) {
errors.rejectValue("email", "duplicate");
return "register/form";
}
}
}
(1) handlerSuccess() 메서드의 커맨드 객체 매개변수 '바로' 뒤에 Errors 타입 매개변수를 추가한다. Errors 타입 파라미터가 커맨드 객체 앞에 위치하면 Exception이 발생한다.
(2) 앞서 작성한 RegisterCommandValidator 객체의 validate() 메서드를 실행한다. 이를 통해 커맨드 객체의 값을 검사하고 그 결과를 Errors 객체에 담는다.
(3) Errors의 hasErrors() 메서드를 통해 에러가 존재하는지 검사하고, 에러가 존재한다면 다시 form.jsp로 forward 한다.
try {
registerService.regist(registerCommand);
return "register/success";
} catch(DuplicateMemberException e) {
errors.rejectValue("email", "duplicate");
return "register/form";
}
해당 코드처럼 Controller에서도 에러 코드를 추가할 수 있다. 만약 이미 가입된 이메일이라면 "email" 프로퍼티에 "duplicate"라는 에러 코드를 추가한다.
[ 3 ] 커맨드 객체의 에러 메시지 출력
에러 메시지는 앞서 지정했던 에러 코드와 Spring이 제공하는 <form:errors> 태그를 활용하여 출력할 수 있다.
에러 코드에 해당하는 에러 메시지를 properties 파일에 추가하여 MessageSource Bean으로 메시지를 찾아 출력하자.
우선, 다음과 같은 label.properties 파일을 src/main/resources 아래 message 폴더에 추가하자.
required=필수 항목입니다.
wrong.email=이메일이 올바르지 않습니다.
duplicate.email=중복된 이메일입니다.
wrong.birthdate=생년월일이 올바르지 않습니다.
nomatch.confirmPassword=비밀번호와 비밀번호 확인이 일치하지 않습니다.
메시지를 찾아주는 MessageSource를 Bean으로 등록하자.
package config;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
...
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
...
}
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
ms.setBasename("message.label");
ms.setDefaultEncoding("UTF-8");
return ms;
}
}
setBasename("message.label")은 클래스 패스의 message 패키지에 속한 label.properties로부터 메시지를 읽어온다고 설정한다. Encoding 방식은 UTF-8로 설정하는데, 이것이 정상 작동하기 위해서는 label.properties도 UTF-8 인코딩 방식으로 작성되어야 한다.
Spring MVC가 제공하는 <form> 태그를 사용하면 더 간단하게 커맨드 객체의 값을 출력할 수 있기에 form.jsp를 아래와 같이 수정했다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="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>
<meta charset="UTF-8">
<title><spring:message code="member.register"/></title>
</head>
<body>
<form:form action="success" method="post" modelAttribute="registerCommand">
<p>
이메일: <br>
<label>
<form:input path="email"/>
<form:errors path="email"/>
</label>
</p>
<p>
이름: <br>
<label>
<form:input path="name"/>
<form:errors path="name"/>
</label>
</p>
<p>
성별: <br>
<label><input type="radio" name="sex" value="남" checked />남자</label>
<label><input type="radio" name="sex" value="여"/>여자</label>
</p>
<p>
생년월일: <br>
<label>
<form:input path="birthdate"/>
<form:errors path="birthdate"/>
</label>
</p>
<p>
비밀번호: <br>
<label>
<form:password path="password"/>
<form:errors path="password"/>
</label>
</p>
<p>
비밀번호 확인: <br>
<label>
<form:password path="confirmPassword"/>
<form:errors path="confirmPassword"/>
</label>
</p>
<input type="submit" value="가입">
</form:form>
</body>
</html>
<form:errors path="email"/>, <form:errors path="name"/>, <form:errors path="birthdate"/> ... 를 통해 각 프로퍼티에 저장된 에러 코드에 맞는 에러 메시지를 출력할 수 있다.
단, Spring MVC가 제공하는 <form:form> 태그를 사용하기 위해서는 커맨드 객체가 존재해야 한다. 즉, form.jsp에서 <form:form> 태그를 사용하기 때문에 terms.jsp에서 form.jsp로 넘어올 때 이름이 "registerCommand" 인 객체를 Model에 추가해야 <form:form> 태그가 정상적으로 작동한다.
이를 위해 Controller에서 /form을 요청받는 handlerForm() 요청 매핑 애노테이션 메서드를 다음과 같이 수정한다.
@PostMapping("/register/form")
public String handlerForm(HttpServletRequest request, Model model) {
String agree = request.getParameter("agree");
if(agree == null || !agree.equals("true")) {
return "register/terms";
}
model.addAttribute("registerCommand", new RegisterCommand());
return "register/form";
}
[ 4 ] 검증 결과 및 에러 메시지 출력 확인
커맨드 객체 검증이 제대로 이루어지는지 확인하자.
위 예시뿐만 아니라 Duplicate, nomatch.confirmPassword 등의 에러 메시지도 정상적으로 출력됨을 확인할 수 있었다!
다음에는 로그인 로직을 구현해 보도록 하자!
❗️전체 코드는 github에서 확인할 수 있습니다❗️
'Project > [Spring] 게시판' 카테고리의 다른 글
[Spring] 게시판 만들기 - 로그인 및 로그아웃 구현 (2) | 2023.02.13 |
---|---|
[Spring] 게시판 만들기 - 회원가입 구현 (1) | 2023.02.10 |
[Spring] 게시판 만들기 - gradle 설정, Spring MVC 설정, DispatcherServlet 설정 (1) | 2023.02.07 |