Spring MVC : 날짜 값 변환
1. 날짜를 이용한 회원 검색 기능
회원 가입 일자를 기준으로 검색하는 기능을 구현하면서 몇 가지 스프링 MVC의 특징을 설명할 것이다. 이를 위해 MemberDao 클래스에 selectByRegdate() 메서드를 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MemberDao {
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
...
public List<Member> selectByRegdate(
LocalDateTime from, LocalDateTime to) {
List<Member> results = jdbcTemplate.query(
"select * from MEMBER where REGDATE between ? and ? "
+ "order by REGDATE desc;",
new RowMapper<Member>(){
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME"),
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
}
}, from, to);
return results;
}
}
selectByRegdate() 메서드는 REGDATE 값이 파라미터로 전달받은 from과 to 사이에 있는 Member 목록을 구한다. 이 메서드를 이용해서 특정 기간 동안에 가입한 회원 목록을 보여준느 기능을 구현할 것이다.
2. 커맨드 객체 Date 타입 프로퍼티 변환 처리 : @DateTimeFormat
회원이 가입한 일시를 기준으로 회원을 검색하기 위해 시작 시간 기준과 끝 시간 기준을 파라미터로 전달받는다고 하자. 검색 기준 시간을 표현하기 위해 다음의 커맨드 클래스를 구현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.time.LocalDateTime;
public class ListCommand {
private LocalDateTime from;
private LocalDateTime to;
public LocalDateTime getFrom(){
return from;
}
public void setFrom(LocalDateTime from){
this.from = from;
}
public LocalDateTime getTo(){
return to;
}
public void setTo(LocalDateTime to){
this.to = to;
}
}
검색을 위한 입력 폼은 다음처럼 이름이 from과 to인
1
2
<input type="text" name="from"/>
<input type="text" name="to"/>
여기서 문제는 < input>에 입력한 문자열을 LocalDateTime 타입으로 변환해야 한다는 것이다, < input>에 2018년 3월 1일 오후 3시를 표현하기 위해 “2018030115”로 입력한다고 해 보자. “2018030115” 문자열을 알맞게 LocalDateTime 타입으로 변환해야 한다.
스프링은 Long이나 int와 같은 기본 데이터 타입으로의 변환은 기본적으로 처리해 주지만 LocalDateTime 타입으로의 변환은 추가 설정이 필요하다. 다행히 설정이 복잡하지 않다.
앞서 작성한 ListCommand 클래스의 두 필드에 @DateTimeFormat 애노테이션을 적용하면 된다.
1
2
3
4
5
6
7
8
9
10
11
import org.springframework.format.annotation.DateTimeFormat;
public class ListCommand {
@DateTimeFormat(pattern = "yyyyMMddHH")
private LocalDateTime from;
@DateTimeFormat(pattern = "yyyyMMddHH")
private LocalDateTime to;
...
}
커맨드 객체에 @DateTimeFormat 애노테이션이 적용되어 있으면 @DateTimeFormat에서 지정한 형식을 이용해서 문자열을 LocalDateTime 타입으로 변환한다. 예를 들어 pattern 속성값으로 “yyyyMMddHH”을 주었는데 이 경우 “2018030115”의 문자열을 “2018년 3월 1일 15시” 값을 갖는 LocalDateTime 객체로 변환해준다.
폼에 입력한 문자열이 커맨드 객체의 LocalDateTime 타입 프로퍼티로 잘 변환되는지 확인하기 위해 뷰 코드를 작성할 차례이다. 먼저 LcalDateTime 값을 원하는 형식으로 출력해주는 커스텀 태그 파일을 작성해야한다.
JSTL이 제공하는 날짜 형식 태그는 아쉽게도 자바 8의 LocalDateTime 타입은 지원하지 않는다. 그래서 다음과 같은 태그 파일을 사용해서 LocalDateTime 값을 지정한 형식으로 출력할 것이다,
1
2
3
4
5
6
7
8
9
10
<%@ tag body-content="empty" pageEncoding="utf-8"%>
<%@ tag import="java.time.format.DateTimeFormatter" %>
<%@ tag trimDirectiveWhitespaces="true" %>
<%@ attribute name="value" required="true"
type="java.time.temporal.TemporalAccessor"%>
<%@ attribute name="pattern" type="java.lang.String" %>
<%
if(pattern == null) pattern= "yyyy-MM-dd";
%>
<%= DateTimeFormatter.ofPattern(pattern).format(value) %>
뷰 코드는 이에 맞게 ListCommand 객체를 위한 폼을 제공하고 members 속성을 이용해서 회원 목록을 출력하도록 구현하면 된다. JSP코드는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<%@ page contentType = "test/html; charset=utf-8" %>
<%@ taglib prefix ="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="tf" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html>
<head>
<title>회원 조회</title>
</html>
<body>
<form:form modelAttribute="cmd">
<p>
<label>from: <form:input path="from" /></label>
~
<label>to: <form:input path="to" /></label>
<input type="submit" value="조회">
</p>
</form:form>
<c:if teset="${! empty members}">
<table>
<tr>
<th>아이디</th><th>이메일</th>
<th>이름</th><th>가입일</th>
</tr>
<c:forEach var="mem" items="${members}">
<tr>
<td>${mem.id}</td>
<td><a href="<c:url value="/members/${mem.id}"/>">
${mem.email}</a></td>
<td>${mem.name}</td>
<td><tf:formatDateTime value="${mem.registerDateTime }"
pattern="yyyy-MM-dd"/></td>
</tr>
</c:forEach>
</table>
</c:if>
</body>
</html>
- 13~15행
:< form:input> 태그를 이용해서 커맨드 객체의 from 프로퍼티와 to 프로퍼티를 위한 < input> 태그를 생성한다. 스프링 폼 태그는 커맨드 객체의 프로퍼티 값을 출력할 때 @DateTimeFormat 애노테이션에 설정한 패턴을 사용해서 값을 출력한다.
from, to 프로퍼티는 모두 @DateTimeFormat(pattern=”yyyyMMddHH”)애노테이션이 적용되어 있으므로 < input> 태그에 사용할 값을 생성할 때 “yyyyMMddHH 형식으로 값을 출력한다.
@DateTimeFormat은 java.tim.LocalDateTime, java.time.LocalDate와 같이 자바8에 추가된 시간 타입과 java.util.Date와 java.util.Calendar 타입을 지원한다.
3. 변환 에러 처리
폼에서 from이나 to에 “20180301”을 입력하면 원래 지정한 형식은 “yyyyMMddHH”이기 때문에 “yyyyMMdd” 부분만 입려가하면 지정한 형식과 일치하지 않게 된다. 형식에 맞지 않은 값을 폼에 입력한 뒤 ‘조회’를 실행하면 다음과 같이 400 에러가 발생한다.
잘 못 입력했을 때 위와 같은 에러 화면을 보고 싶은 사용자는 없다. 400 에러 대신 폼에 알맞은 에러 메시지를 보여주고 싶다면 Errors 타입 파라미터를 요청 매핑 애노테이션 적용 메서드에 추가하면 된다.(Errors 타입 파라미터를 listCommand 파라미터 바로 뒤에 위치시킨 것에 유의한다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class MemberListController{
...
@RequestMapping("/members")
public String list(
@ModelAttribute("Cmd") ListCommand listCommand,
Errors errors, Model mode){
if(errors.hasErrors()){
return "member/memberList"
}
if(listCommand.getFrom() != null && listCommand.getTo() != null){
List<Member> members = memberDao.selectByRegdate(
listCommand.getFrom(),listCommand.getTo());
model.addAttribute("members", members);
}
return "member/memberList";
}
}
요청 매핑 애노테이션 적용 메서드가 Errors 타입 파라미터를 가질 경우 @DateTimeFormat에 지정한 형식에 맞지 않으면 Errors 객체에 “typeMismatch” 에러 코들르 추가한다. 따라서 8행의 코드처럼 에러 코드가 존재하는지 확인해서 알맞은 처리를 할 수 있다.
에러 코드로 “typeMismatch”를 추가하므로 메시지 프로퍼티 파일에 해당 메시지를 추가하면 에러 메시지를 보여줄 수 있다. 이번에는 에러 코드에 해당하는 메시지 코드 중 “typeMismatch.java.tiem.LocalDateTime” 코드를 properties 파일에 추가해 봤다.
1
typeMismatch.java.time.LocalDateTime=잘못된 형식
4. 변환 처리에 대한 이해
@DateTimeFormat 애노테이션을 사용하면 지정한 형식의 문자열을 LocalDateTime 타입으로 변환해준다는 것을 예제를 통해 확인했다. 여기서 궁금증이 하나 생긴다.
누가 문자열을 LocalDateTime 타입으로 변환하는지에 대한 것이다. 답은 WebDateBinder에 있따. WebDateBinder는 로컬 범위 Validator를 설명할 때 언급을 했는데 값 변환에도 관여한다.
스프링 MVC는 요청 매핑 애노테이션 적용 메서드와 DispatcherServlet 사이를 연결하기 위해 RequestMappingHandlerAdapter 객체를 사용한다. 이 핸드러 어댑터 객체는 요청 파라미터와 커맨드 객체 사이의 변환 처리를 위해 WebDataBinder를 이용한다.
WebDataBinder는 커맨드 객체를 생성한다. 그리고 커맨드 객체의 프로퍼티와 같은 이름을 갖는 요청 파라미터를 이용해서 프로퍼티 값을 생성한다.
-
WebDataBinder는 직접 타입을 변환하지 않고 위에 그림과 같이 ConversionService에 그 역할을 위임한다. 스프링 MVC를 위한 설정인 @EnableWebMvc 애노테이션을 사용하면 DefaultFormmattingConversionService를 ConversionService로 사용한다.
-
DefaultFormmattingConversionService는 int, long과 같은 기본 데이터 타입뿐만 아니라 @DateTimeFormat 애노테이션을 사용한 시간 관련 타입 변환 기능을 제공한다. 이런 이유로 커맨드로 사용할 클래스에 @DateTimeFormat 애노테이션만 붙이면 지정한 형식의 문자열을 시간 타입 값으로 받을 수 있는 것이다.
-
WebDataBinder는 < form:input>에도 사용된다. < form:input>태그를 사용하면 아래 그림과 같이 path 속성에 지정한 프로퍼티 값을 String으로 변환해서 < input> 태그의 value속성값으로 생성한다. 이때 프로퍼티 값을 String으로 변환할 때 WebDataBinder의 ConversionService를 사용한다.
Ref.
- 최범균, 스프링프로그래밍입문5, 가메출판사.