https://hahagogo.tistory.com/154
데이터 바인딩 - PropertyEditor, Converter, Formatter
📝 데이터 바인딩 : 사용자가 입력한 문자열 값을 프로퍼티 타입에 맞춰 변환하여 할당하는 것 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/propertyeditors/p..
hahagogo.tistory.com
Validator 인터페이스를 지원하여 어플리케이션에서 사용하는 객체를 검증할 수 있는 기능을 제공
boolean supports(Class clazz) : 이 객체의 클래스가 이 Validator가 검증할 수 있는 클래스인 지를 판단하는 매서드
void validate(Object target, Errors error) : 실제 검증 로직이 이루어지는 메서드
수동 검증
컨트롤러 메서드 내에서 검증하던것을
if( id == null || "".equals(id.trim() ){
model.addAttribute("msg" , "id는 필수 입력 입니다.");
return "redirect:/register/add;
}
컨트롤러 메서드 와 별도로 분리 하여 검증
@PostMapping("/register/add")
public String save(Model m , @Valid User user, BindingResult result ){
UserValidator userValidator = new UserValidator();
userValidator.validate(user , result); //검증
if( result.hasErrors() ){
return "registForm";
}
}
Validator
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return User.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
User user = (User)target;
String id = user.getId();
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "required" , "id는 필수값입니다.");// 하드코딩
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
if(id == null || id.length() < 5 || id.length() > 12){
error.rejectValue("id","invalidLength");
}
}
}
Bean Validation API > pom.xml 추가
https://mvnrepository.com/artifact/javax.validation/validation-api
자동 검증( Controller 안에서만 동작 ) ( binder.setValidator )
@InitBinder
public void toDate( WebDataBinder binder ){
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
binder.registerCustomEditor(Date.class , new CustomDateEditor(df, false) );
// =============================================================================
binder.registerCustomEditor(String[].class , new StringArrayPropertyEditor("#") );
binder.registerCustomEditor(String[].class , "hobby" , new StringArrayPropertyEditor("#") ); //hobby에만
// =============================================================================
binder.setValidator( new UserValidator() );
}
@PostMapping("/register/add")
public String save(Model m , @Valid User user, BindingResult result ){
if( result.hasErrors() ){
return "registForm";
}
}
자동 검증( Global )
servlet-context.xml
<annotation-driven validator = "globalValidator" />
<beans:bean id="globalValidator" class="com.fastkk.ch2.GlobalValidator" />
GlobalValidator
public class GlobalValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
// return User.class.equals(clazz); // 검증하려는 객체가 User타입인지 확인
return User.class.isAssignableFrom(clazz); // clazz가 User 또는 그 자손인지 확인
}
@Override
public void validate(Object target, Errors errors) {
System.out.println("GlobalValidator.validate() is called");
User user = (User)target;
String id = user.getId();
// if(id==null || "".equals(id.trim())) {
// errors.rejectValue("id", "required");
// }
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
if(id==null || id.length() < 5 || id.length() > 12) {
errors.rejectValue("id", "invalidLength", new String[]{"5", "12"}, null);
}
}
}
Global + Controller 안에서만 동작( 주의 binder.addValidators )
@InitBinder
public void toDate( WebDataBinder binder ){
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
binder.registerCustomEditor(Date.class , new CustomDateEditor(df, false) );
// =============================================================================
binder.registerCustomEditor(String[].class , new StringArrayPropertyEditor("#") );
binder.registerCustomEditor(String[].class , "hobby" , new StringArrayPropertyEditor("#") ); //hobby에만
// =============================================================================
binder.addValidators( new UserValidator() );
}
@PostMapping("/register/add")
public String save(Model m , @Valid User user, BindingResult result ){
if( result.hasErrors() ){
return "registForm";
}
}
MessageResource
다양한 리소스 에서 메시지를 읽기위한 MessageSource 인터페이스 보기..
public interface MessageSource {
String getMessage ( Strong code , Object[] args , String defaultMessage, Locale locale );
String getMessage ( String code , Object[] args , Locale locale ) throws NoSuchMessageException;
String getMessage ( MessageSourceResolvable, Locale locale ) throws NoSuchMessageException;
}
프로퍼티 파일을 메시지로 하는 ResourceBundleMessageSource를 servlet-context.xml 에 등록해야함.
<beans:bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<beans:property name="basenames">
<beans:list>
<beans:value>error_message</beans:value> <!-- /src/main/resources/error_message.properties -->
</beans:list>
</beans:property>
<beans:property name="defaultEncoding" value="UTF-8"/>
</beans:bean>
/src/main/resources > error_message.properties 에는
required = 필수 항목입니다.
required.user.pw = 비밀번호 필수항목임
invalidLength.id = 아이디 길이는 {0} ~ {1} 입니다.
찾는 순서 ex) id
required.user.id > required.id > required.java.lang.String > required > defaultMessage
검증 메시지의 출력
<%@ taglib uri="http://www.springframework.org/gags/form" prefix="form" %>
기존 <form action="<c:url value="/register/save"/>" method="post" onsubmit="return formCheck(this)"></form>
변경 <form:form modelAttribute="user" ></form:form>
기존
<div id="msg" class="msg">
<c:if test="${not empty param.msg}">
<i class="fa fa-exclamation-circle"> ${URLDecoder.decode(param.msg)}</i>
</c:if>
</div>
변경
<div id="msg" class="msg">
<c:if test="${not empty param.msg}">
<i class="fa fa-exclamation-circle">
<form:errors path="id"/>
</i>
</c:if>
</div>
사용 예시
UserDao.java
public interface UserDao {
int deleteUser(String id);
User selectUser(String id);
// 사용자 정보를 user_info테이블에 저장하는 메서드
int insertUser(User user);
// 매개변수로 받은 사용자 정보로 user_info테이블을 update하는 메서드
int updateUser(User user);
void deleteAll() throws Exception;
}
UserDaoImpl.java
@Repository
public class UserDaoImpl implements UserDao {
@Autowired DataSource ds;
final int FAIL = 0;
@Override
public int deleteUser(String id) {
int rowCnt = FAIL; // insert, delete, update
Connection conn = null;
PreparedStatement pstmt = null;
String sql = "delete from user_info where id= ? ";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, id);
// int rowCnt = pstmt.executeUpdate(); // insert, delete, update
// return rowCnt;
return pstmt.executeUpdate(); // insert, delete, update
} catch (SQLException e) {
e.printStackTrace();
return FAIL;
} finally {
// close()를 호출하다가 예외가 발생할 수 있으므로, try-catch로 감싸야함.
// try { if(pstmt!=null) pstmt.close(); } catch (SQLException e) { e.printStackTrace();}
// try { if(conn!=null) conn.close(); } catch (SQLException e) { e.printStackTrace();}
close(pstmt, conn); // private void close(AutoCloseable... acs) {
}
}
@Override
public User selectUser(String id) {
User user = null;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = "select * from user_info where id= ? ";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
pstmt.setString(1, id);
rs = pstmt.executeQuery(); // select
if (rs.next()) {
user = new User();
user.setId(rs.getString(1));
user.setPwd(rs.getString(2));
user.setName(rs.getString(3));
user.setEmail(rs.getString(4));
user.setBirth(new Date(rs.getDate(5).getTime()));
user.setSns(rs.getString(6));
user.setReq_date(new Date(rs.getTimestamp(7).getTime()));
}
} catch (SQLException e) {
return null;
} finally {
// close()를 호출하다가 예외가 발생할 수 있으므로, try-catch로 감싸야함.
// close()의 호출순서는 생성된 순서의 역순
// try { if(rs!=null) rs.close(); } catch (SQLException e) { e.printStackTrace();}
// try { if(pstmt!=null) pstmt.close(); } catch (SQLException e) { e.printStackTrace();}
// try { if(conn!=null) conn.close(); } catch (SQLException e) { e.printStackTrace();}
close(rs, pstmt, conn); // private void close(AutoCloseable... acs) {
}
return user;
}
// 사용자 정보를 user_info테이블에 저장하는 메서드
@Override
public int insertUser(User user) {
int rowCnt = FAIL;
Connection conn = null;
PreparedStatement pstmt = null;
// insert into user_info (id, pwd, name, email, birth, sns, reg_date)
// values ('asdf22', '1234', 'smith', 'aaa@aaa.com', '2022-01-01', 'facebook', now());
String sql = "insert into user_info values (?, ?, ?, ?,?,?, now()) ";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
pstmt.setString(1, user.getId());
pstmt.setString(2, user.getPwd());
pstmt.setString(3, user.getName());
pstmt.setString(4, user.getEmail());
pstmt.setDate(5, new java.sql.Date(user.getBirth().getTime()));
pstmt.setString(6, user.getSns());
return pstmt.executeUpdate(); // insert, delete, update;
} catch (SQLException e) {
e.printStackTrace();
return FAIL;
} finally {
close(pstmt, conn); // private void close(AutoCloseable... acs) {
}
}
// 매개변수로 받은 사용자 정보로 user_info테이블을 update하는 메서드
@Override
public int updateUser(User user) {
int rowCnt = FAIL; // insert, delete, update
// Connection conn = null;
// PreparedStatement pstmt = null;
String sql = "update user_info " +
"set pwd = ?, name=?, email=?, birth =?, sns=?, reg_date=? " +
"where id = ? ";
// try-with-resources - since jdk7
try (
Connection conn = ds.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
){
pstmt.setString(1, user.getPwd());
pstmt.setString(2, user.getName());
pstmt.setString(3, user.getEmail());
pstmt.setDate(4, new java.sql.Date(user.getBirth().getTime()));
pstmt.setString(5, user.getSns());
pstmt.setTimestamp(6, new java.sql.Timestamp(user.getReq_date().getTime()));
pstmt.setString(7, user.getId());
rowCnt = pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
return FAIL;
}
return rowCnt;
}
public void deleteAll() throws Exception {
Connection conn = ds.getConnection();
String sql = "delete from user_info ";
PreparedStatement pstmt = conn.prepareStatement(sql); // SQL Injection공격, 성능향상
pstmt.executeUpdate(); // insert, delete, update
}
private void close(AutoCloseable... acs) {
for(AutoCloseable ac :acs)
try { if(ac!=null) ac.close(); } catch(Exception e) { e.printStackTrace(); }
}
}
Pom.xml
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
web.xml
<!-- 한글 변환 필터 시작 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 한글 변환 필터 끝 -->
servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<!-- com ( component-scan ) -->
<!-- 패키지 안에 있는 클래스 중에서 @Component 붙은것을 찾아서 Bean 으로 등록 -->
<context:component-scan base-package="com.test.ch3" >
<context:exclude-filter type="regex" expression="com.test.ch3.dpCopy*.*"/>
</context:component-scan>
<view-controller path="/" view-name="index"/>
<beans:bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<beans:property name="basenames">
<beans:list>
<beans:value>error_message</beans:value> <!-- /src/main/resources/error_message.properties -->
</beans:list>
</beans:property>
<beans:property name="defaultEncoding" value="UTF-8"/>
</beans:bean>
</beans:beans>
UserValidator.java
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
// return User.class.equals(clazz); // 검증하려는 객체가 User타입인지 확인
return User.class.isAssignableFrom(clazz); // clazz가 User 또는 그 자손인지 확인
}
@Override
public void validate(Object target, Errors errors) {
System.out.println("UserValidator.validate() is called");
User user = (User)target;
String id = user.getId();
// if(id==null || "".equals(id.trim())) {
// errors.rejectValue("id", "required");
// }
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
if(id==null || id.length() < 5 || id.length() > 12) {
errors.rejectValue("id", "invalidLength", new String[]{"5","12"}, null);
}
}
}
LoginController.java
@Controller
@RequestMapping("/login")
public class LoginController {
@Autowired UserDao userDao;
@GetMapping("/login")
public String loginForm() {
return "loginForm";
}
@GetMapping("/logout")
public String logout(HttpSession session) {
// 1. 세션을 종료
session.invalidate();
// 2. 홈으로 이동
return "redirect:/";
}
@PostMapping("/login")
public String login(String id, String pwd, String toURL, boolean rememberId,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1. id와 pwd를 확인
if(!loginCheck(id, pwd)) {
// 2-1 일치하지 않으면, loginForm으로 이동
String msg = URLEncoder.encode("id 또는 pwd가 일치하지 않습니다.", "utf-8");
return "redirect:/login/login?msg="+msg;
}
// 2-2. id와 pwd가 일치하면,
// 세션 객체를 얻어오기
HttpSession session = request.getSession();
// 세션 객체에 id를 저장
session.setAttribute("id", id);
if(rememberId) {
// 1. 쿠키를 생성
Cookie cookie = new Cookie("id", id); // ctrl+shift+o 자동 import
// 2. 응답에 저장
response.addCookie(cookie);
} else {
// 1. 쿠키를 삭제
Cookie cookie = new Cookie("id", id); // ctrl+shift+o 자동 import
cookie.setMaxAge(0); // 쿠키를 삭제
// 2. 응답에 저장
response.addCookie(cookie);
}
// 3. 홈으로 이동
toURL = toURL==null || toURL.equals("") ? "/" : toURL;
return "redirect:"+toURL;
}
private boolean loginCheck(String id, String pwd) {
User user = userDao.selectUser(id);
if(user == null) return false;
return user.getPwd().equals(pwd);
//return "asdf".equals(id) && "1234".equals(pwd);
}
}
RegisterController.java ( @Valid User user, BindingResult result )
@Controller // ctrl+shift+o 자동 import
@RequestMapping("/register")
public class RegistController {
@Autowired UserDao userDao;
final int FAIL = 0;
@InitBinder
public void toDate(WebDataBinder binder) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
binder.setValidator(new UserValidator()); // UserValidator를 WebDataBinder의 로컬 validator로 등록
// List<Validator> validatorList = binder.getValidators();
// System.out.println("validatorList="+validatorList);
}
@GetMapping("/add")
public String register() {
return "registerForm"; // WEB-INF/views/registerForm.jsp
}
@PostMapping("/add")
public String save(@Valid User user, BindingResult result, Model m) throws Exception {
System.out.println("result="+result);
System.out.println("user="+user);
// User객체를 검증한 결과 에러가 있으면, registerForm을 이용해서 에러를 보여줘야 함.
if(result.hasErrors()) {
// 2. DB에 신규회원 정보를 저장
int rowCount = userDao.insertUser(user);
if(rowCount != FAIL){ return "registerForm"; }
}
return "registerForm";
}
private boolean isValid(User user) {
return true;
}
}
error_message.properties
required = 필수 항목입니다.
required.user.pw = 비밀번호 필수항목임
invalidLength.id = 아이디 길이는 {0} ~ {1} 입니다.