이전 포스트에서 사용했던 코드에 이어서 작성하였다. 이번에도 pom.xml에 별도로 추가할 모듈은 없다.
MemberDao 클래스 : 날짜를 이용한 조회 기능 구현
package spring;
import java.time.LocalDateTime;
... 생략
public class MemberDao {
private final JdbcTemplate jdbcTemplate; // 조회 쿼리 수행
private final RowMapper<Member> memRowMapper
= new MemberRowMapper(); // 조회 결과 저장
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
... 코드 생략
/* 지정한 기간 동안 가입한 회원 조회 */
public List<Member> selectByRegdate(LocalDateTime from, LocalDateTime to) {
return jdbcTemplate.query(
"select * from MEMBER where REGDATE between ? and ?"
+ "order by REGDATE desc",
memRowMapper,
from, to);
}
... 코드 생략
}
MemberRowMapper 클래스 : 조회 쿼리에서 사용할 RowMapper 객체
package spring;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
public class MemberRowMapper implements RowMapper<Member> {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
/* 조건을 충족하는 각 tuple의 속성값을 Member 객체의 각 필드에 매핑하여 저장 */
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME"),
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
}
}
@DateTimeFormat("날짜 형식")
: 이 어노테이션은 날짜/시간 타입 프로퍼티에 사용할 수 있다. 문자열 타입인 날짜에 관한 입력값을 이 어노테이션에서 지정한 날짜 형식을 이용하여 날짜/시간 타입으로 변환한 다음, 이 어노테이션이 적용된 프로퍼티에 저장한다.
이 어노테이션이 지원하는 날짜/시간 타입으로는LocalDateTime,LocalDate,Date,Calendar가 있다.
ListCommand 클래스 : @DateTimeFormat 어노테이션 적용
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;
}
}
MemberListController 클래스 : 날짜 기준 조회 쿼리 & 에러 처리 수행
package controller;
import java.util.List;
... 코드 생략
@Controller
public class MemberListController {
private MemberDao memberDao;
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
@RequestMapping("/members")
public String list(
@ModelAttribute("cmd") ListCommand listCommand,
Errors errors, Model model) {
/* 1. 날짜값이 올바르지 않으면 에러 처리 */
if (errors.hasErrors()) {
return "member/memberList";
}
/* from & to 프로퍼티가 모두 입력되어야 조회 쿼리 수행 가능 */
if (listCommand.getFrom() != null && listCommand.getTo() != null) {
/* 2. 날짜 범위를 이용한 조회 쿼리 수행 */
List<Member> members = memberDao.selectByRegdate(
listCommand.getFrom(), listCommand.getTo());
model.addAttribute("members", members);
}
return "member/memberList";
}
}
ControllerConfig 클래스 : MemberListController Bean 추가
package config;
import controller.MemberListController;
import spring.MemberDao;
... 코드 생략
@Configuration
public class ControllerConfig {
... 코드 생략
@Autowired
private MemberDao memberDao;
... 코드 생략
@Bean
public MemberListController memberListController() {
MemberListController controller = new MemberListController();
controller.setMemberDao(memberDao);
return controller;
}
}
실행 결과
@DateTimeFormat 어노테이션은 지정한 형식으로 입력된 문자열을 날짜 타입으로 변환하는데, 이러한 값 변환에 관여하는 것이 WebDataBinder이다.
Spring MVC가 요청 매핑 어노테이션 적용 메서드와 DispatcherServlet 을 연결하기 위해 RequestMappingHandlerAdapter 객체를 사용할 때, WebDataBinder에서는 다음의 과정을 통해 이 핸들러 어댑터 객체에서 이루어지는 요청 파라미터와 커맨드 객체 간 변환 작업을 처리한다.
WebDataBinder의 값 변환 과정
WebDataBinder에서 커맨드 객체와 프로퍼티 생성
: 생성한 프로퍼티의 이름은 커맨드 객체의 프로퍼티와 동일WebDataBinder는ConversionService에게 변환 처리 역할 위임
:ConversionService로는DefaultFormattingConversionService사용
(@EnableWebMvc어노테이션 사용 시)DefaultFormattingConversionService에서 실제 변환 작업 수행 후,
그 결과를WebDataBinder에게 리턴
경로 변수란 매핑 경로에서 중괄호로 둘러싸인 부분을 말한다. 이러한 경로변수를 사용하여 경로의 일부가 가변적인 값을 가지는 경로를 가변 경로라고 한다. @PathVariable 어노테이션은 이러한 가변 경로를 처리할 때 활용된다.
@PathVariable("경로변수명")
: 요청 경로로부터 이 어노테이션의 값으로 지정한 이름의 경로 변수를 찾은 후, 그 값을 이 어노테이션이 적용된 파라미터로 전달한다. 이 때, 경로 변수의 값은 이 어노테이션이 붙은 파라미터의 타입과 일치시킨다. 경로변수의 값을 파라미터의 타입으로 변환할 수 없을 경우HTTP Status 400에러가 발생한다.
MemberDetailController 클래스 : @PathVariable 사용 예시
package controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import spring.Member;
import spring.MemberDao;
import spring.MemberNotFoundException;
@Controller
public class MemberDetailController {
private MemberDao memberDao;
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
/* 요청 경로에 경로변수 id 사용,
@PathVariable은 경로변수 id의 값을 memId에게 전달 */
@GetMapping("/members/{id}")
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";
}
}
DefaultFormattingConversionService는 날짜 타입뿐만 아니라 int, long과 같은 기본 데이터 타입에 대해서도 변환 기능을 제공한다. 따라서 @DateTimeFormat을 이용하여 문자열을 날짜 타입으로 변환할 때뿐만 아니라 @PathVariable을 통해 가변 경로를 처리하는 과정에서 추출한 경로 변수의 값을 파라미터에 저장할 때도 내부적으로 활용된다.
웹 어플리케이션에서 Exception이 발생한 경우, Exception 화면을 그대로 보이는 대신 사용자에게 적합한 안내를 하는 것이 더 낫다. 이처럼 컨트롤러에서 Exception이 발생했을 때, 이를 처리하기 위해 사용할 수 있는 것이 @ExceptionHandler이다.
@ExceptionHandler(예외처리할 Exception)
: 이 어노테이션은 컨트롤러에 포함된 메서드에 사용할 수 있다.
이 어노테이션을 포함하는 컨트롤러에서 이 어노테이션이 지정한 Exception이나 그 하위 Exception이 발생했을 때, 이 어노테이션이 붙은 메서드를 실행하여 해당 Exception이나 그 하위 Exception을 처리한다. 이 어노테이션의 적용 범위는 이 어노테이션이 사용된 컨트롤러 클래스로 한정된다.
MemberDetailController
package controller;
import org.springframework.beans.TypeMismatchException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
... 코드 생략
@Controller
public class MemberDetailController {
private MemberDao memberDao;
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
@GetMapping("/members/{id}")
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(TypeMismatchException.class)
public String handleTypeMismatchException() {
return "member/invalidId";
}
/* 경로변수값에 해당되는 회원 데이터가 없을 시 예외 처리 */
@ExceptionHandler(MemberNotFoundException.class)
public String handleNotFoundException() {
return "member/noMember";
}
}
@ControllerAdvice("적용할 패키지명")
: 이 어노테이션을 적용한 클래스는 지정한 패키지와 그 하위 패키지에 속한 컨트롤러 클래스에 공통으로 적용할 설정을 지정할 수 있다. 이 어노테이션을 적용한 클래스가 동작하려면 해당 클래스를@EnableWebMvc이 적용된 설정 클래스의 Bean으로 추가해야 한다.
@ControllerAdvice 주요 속성
CommonExceptionHandler 클래스
package controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice("spring")
public class CommonExceptionHandler {
/* handleRuntimeException() : spring 패키지와 그 하위 패키지 내 모든 컨트롤러에서 발생한
RuntimeException과 그 하위 Exception을 처리 */
@ExceptionHandler(RuntimeException.class)
public String handleRuntimeException() {
return "error/commonException";
}
}
컨트롤러에서 Exception이 발생했을 때, @ExceptionHandler 적용 메서드의 실행 우선순위는 아래와 같다.
- 같은 컨트롤러 클래스 내
@ExceptionHandler메서드 중 현재 Exception을 처리 가능한 메서드@ControllerAdvice클래스의@ExceptionHandler메서드 중 현재 Exception을 처리 가능한 메서드
: 컨트롤러 클래스 내@ExceptionHandler메서드로 현재 Exception을 처리할 수 없는 경우
- 사용 가능 파라미터 타입
HttpServletRequest,HttpServletResponse,HttpServletSessionModelException- 사용 가능 리턴 타입
ModelAndViewString: View 이름을 반환하는 경우 한정Object:@ResponseBody어노테이션을 사용한 경우 한정ResponseEntity