해당 포스트는 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 [최범균 저] 책 내용을 참고하였습니다.
13장에서 작성한 예제를 이어서 작성합니다.
MemberDao에 회원 가입 일자를 기준으로 검색하는 기능, selectByRegdate 추가
public List<Member> selectByRegdate(LocalDateTime from, LocalDateTime to) {
// from 부터 to 사이에 있는 REGDATE 값을 갖는 Member 목록 반환
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;
}
}
검색을 위한 폼은 name이 from과 to인 <input> 태그를 정의
<input type="text" name="from" />
<input type="text" name="to" />
<input>으로 입력한 문자열을 그대로 쓸 수는 없고 LocalDateTime 타입으로 변환해야한다.
2022년 12월 04일 오후 3시를 표현하기 위해 "2022120415"를 입력해야한다고 할 때, 스프링에서 @DateTimeFormat 애노테이션이 이 역할을 한다.
from 과 to 를 전달하고 검색 기준 시간을 표현하기 위한 커맨드 클래스 작성
package controller;
import java.time.LocalDateTime;
import org.springframework.format.annotation.DateTimeFormat;
public class ListCommand {
@DateTimeFormat(pattern = "yyyyMMddHH")
private LocalDateTime from;
@DateTimeFormat(pattern = "yyyyMMddHH")
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;
}
}
이렇게 하면 컨트롤러 클래스에서는 별도 설정없이 커맨드 객체를 사용하면 된다.
@RequestMapping("/members")
public String list(
@ModelAttribute("cmd") ListCommand listCommand,
Model model) {
if (listCommand.getFrom() != null && listCommand.getTo() != null) {
List<Member> members = memberDao.selectByRegdate(
listCommand.getFrom(), listCommand.getTo());
model.addAttribute("members", members);
}
return "member/memberList";
}
JSTL은 자바8의 LocalDateTime 타입을 지원하지 않으므로, 커스텀 태그 파일을 작성해서 출력
<%@ 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) %>
위 컨트롤러 클래스에서 작성했던 list() 메서드는 from 부터 to 기간 까지 가입한 Member 목록을 구하고 members 속성으로 전달한다.
뷰 코드는 커맨드 객체를 위한 폼을 제공하고, members 속성으로 회원 목록을 출력하도록 구현
<%@ page contentType="text/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>
</head>
<body>
<form:form modelAttribute="cmd">
<p>
<!--from과 to를 입력받기 위한 form:input 태그-->
<!--yyyyMMddHH 형식으로 입력-->
<label>from: <form:input path="from" /></label>
~
<label>to:<form:input path="to" /></label>
<input type="submit" value="조회">
</p>
</form:form>
<!--검색 결과를 하나씩 출력-->
<c:if test="${! 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>
yyyyDDmmHH 형식이 아닌 입력은 400 에러를 발생시킨다.
에러 대신 폼에 알맞은 에러 메시지를 보여주기 위해, Errors 타입 파라미터를 요청 매핑 애노테이션 적용 메서드에 추가
@RequestMapping("/members")
public String list(
@ModelAttribute("cmd") ListCommand listCommand,
Errors errors, Model model) {
// 에러가 존재하는지 확인하고 알맞은 처리를 할 수 있음
if (errors.hasErrors()) {
return "member/memberList";
}
이제 @DateTimeFormat에 지정한 형식에 맞지 않으면 Errors 객체에 typeMismatch 에러 코드를 추가한다.
메세지 프로퍼티 파일(label.properties)에 메세지를 추가하면 원하는 에러 메세지를 보여줄 수 있다.
typeMismatch.java.time.LocalDateTime=잘못된 형식
뷰 코드에 <form:errors> 태그로 에러 메세지 출력 코드 추가
<form:form modelAttribute="cmd">
<p>
<label>from: <form:input path="from" /></label>
<form:errors path="from" />
~
<label>to:<form:input path="to" /></label>
<form:errors path="to" />
<input type="submit" value="조회">
</p>
</form:form>
@DateTimeFormat 이 문자열을 LocalDateTime 타입으로 변환해주는데, 누가 하는 일 일까?
바로 WebDataBinder이다.
스프링 MVC는 요청 매핑 애노테이션 적용 메서드와 DispatcherServlet 사이를 연결하기 위해 RequestMappingHandlerAdapter 객체를 사용하고, 이 객체는 요청 파라미터와 커맨드 객체 사이의 변환 처리를 위해 WebDataBinder를 이용한다.
WebDataBinder는 커맨드 객체의 프로퍼티와 같은 이름을 갖는 요청 파라미터를 이용해 커맨드 객체를 생성한다.
WebDataBinder는 직접 타입을 변환하지 않고 Conversion Service에 그 역할을 위임한다.
<form:input> 태그에도 WebDataBinder가 사용된다.
위 그림과 같이 path 속성 프로퍼티 값을 String으로 변환해서 <input> 태그의 value 속성값으로 생성한다.
이때 WebDataBinder의 ConversionService를 사용한다.
http://localhost:8080/sp5-chap14/members/10
ID가 10인 회원의 정보를 조회하는 URL이다.
이 형식의 URL은 회원마다 경로의 마지막 부분이 달라지는데 어떻게 처리할까?
컨트롤러에 아래와 같이 설정하면 경로 변수를 지정할 수 있다.
// 경로 변수 id 지정
@GetMapping("/members/{id}")
// 경로 변수가 @PathVariable("id")이 적용된 memId로 전달
public String detail(@PathVariable("id") Long memId, Model model) {
Member member = memberDao.selectById(memId);
if (member == null) {
throw new MemberNotFoundException();
}
model.addAttribute("member", member);
return "member/memberDetail";
}
현재 익셉션이 발생하면 익셉션 화면이 그대로 보인다.
익셉션을 처리해서 사용자에게 더 적합한 안내를 해주는 것이 좋다.
이럴 때 쓰이는 것이 @ExceptionHandler 애노테이션이다.
같은 컨트롤러에 @ExceptionHandler를 적용한 메서드가 존재하면 그 메서드가 익셉션을 처리한다.
한 컨트롤러 안에서 발생한 익셉션을 처리한다.
@ExceptionHandler(TypeMismatchException.class)
public String handleTypeMismatchException() {
return "member/invalidId";
}
@ExceptionHandler(MemberNotFoundException.class)
public String handleNotFoundException() {
return "member/noMember";
}
다수의 컨트롤러에서 동일 타입의 익셉션을 처리하고 싶다면 @ControllerAdvice 애노테이션을 사용한다.
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
// spring 패키지 아래에서 발생하는 RuntimeException을 처리하는 예시
@ControllerAdvice("spring")
public class CommonExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public String handleRuntimeException() {
return "error/commonExcpetion";
}
}
@ControllerAdvice 적용 클래스가 동작하려면 해당 클래스를 스프링에 빈으로 등록해야한다.
@ControllerAdvice 애노테이션은 공통 설정을 적용할 컨트롤러 대상을 지정하기 위해 아래와 같은 속성을 제공
속성 | 타입 | 설명 |
valuebasePackages | String[] | 공통 설정을 적용할 컨트롤러가 속하는 기준 패키지 |
annotations | Class<? extends Annotation>[] | 특정 애노테이션이 적용된 컨트롤러 대상 |
assignableTypes | Class<?>[] | 특정 타입 또는 그 하위 타입인 컨트롤러 대상 |
Spring 1
EDITOR: OJO
[스프링1] 16장.JSON 응답과 요청 처리 (0) | 2022.12.08 |
---|---|
[스프링1] 15장.간단한 웹 어플리케이션의 구조 (0) | 2022.12.08 |
[스프링1] 13장. MVC3: 세션, 인터셉터, 쿠키 (0) | 2022.12.01 |
[스프링1] 12장. MVC2: 메시지, 커맨드 객체 검증 (0) | 2022.12.01 |
[스프링 1] 11장. MVC1: 요청 매핑, 커맨드 객체, 리다이렉트, 폼 태그, 모델 (0) | 2022.11.24 |