Chapter 14 - MVC 연습(4) : 날짜 값 변환, @PathVariable, Exception 처리

이현빈·2024년 3월 28일

1. 프로젝트 준비

이전 포스트에서 사용했던 코드에 이어서 작성하였다. 이번에도 pom.xml에 별도로 추가할 모듈은 없다.


2. 날짜를 이용한 회원 검색 기능

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;
    }
}

3. 커맨드 객체 Date 타입 프로퍼티 변환: @DateTimeFormat

  • @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;
    }
}

실행 결과


4. 변환 처리에 대한 이해

@DateTimeFormat 어노테이션은 지정한 형식으로 입력된 문자열을 날짜 타입으로 변환하는데, 이러한 값 변환에 관여하는 것이 WebDataBinder이다.
Spring MVC가 요청 매핑 어노테이션 적용 메서드와 DispatcherServlet 을 연결하기 위해 RequestMappingHandlerAdapter 객체를 사용할 때, WebDataBinder에서는 다음의 과정을 통해 이 핸들러 어댑터 객체에서 이루어지는 요청 파라미터와 커맨드 객체 간 변환 작업을 처리한다.

WebDataBinder의 값 변환 과정

  1. WebDataBinder에서 커맨드 객체와 프로퍼티 생성
    : 생성한 프로퍼티의 이름은 커맨드 객체의 프로퍼티와 동일
  2. WebDataBinderConversionService에게 변환 처리 역할 위임
    : ConversionService로는 DefaultFormattingConversionService 사용
    (@EnableWebMvc 어노테이션 사용 시)
  3. DefaultFormattingConversionService에서 실제 변환 작업 수행 후,
    그 결과를 WebDataBinder에게 리턴

5. @PathVariable을 이용한 경로 변수 처리

경로 변수란 매핑 경로에서 중괄호로 둘러싸인 부분을 말한다. 이러한 경로변수를 사용하여 경로의 일부가 가변적인 값을 가지는 경로를 가변 경로라고 한다. @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을 통해 가변 경로를 처리하는 과정에서 추출한 경로 변수의 값을 파라미터에 저장할 때도 내부적으로 활용된다.


6. @ExceptionHandler: 컨트롤러 범위Exception 처리하기

웹 어플리케이션에서 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를 이용한 공통 Exception 처리

  • @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";
    }
}

@ExceptionHandler 적용 메서드의 우선 순위

컨트롤러에서 Exception이 발생했을 때, @ExceptionHandler 적용 메서드의 실행 우선순위는 아래와 같다.

  1. 같은 컨트롤러 클래스 내 @ExceptionHandler 메서드 중 현재 Exception을 처리 가능한 메서드
  2. @ControllerAdvice 클래스의 @ExceptionHandler 메서드 중 현재 Exception을 처리 가능한 메서드
    : 컨트롤러 클래스 내 @ExceptionHandler 메서드로 현재 Exception을 처리할 수 없는 경우

@ExceptionHandler 적용 메서드의 파라미터 타입 & 리턴 타입

  • 사용 가능 파라미터 타입
    • HttpServletRequest, HttpServletResponse, HttpServletSession
    • Model
    • Exception
  • 사용 가능 리턴 타입
    • ModelAndView
    • String : View 이름을 반환하는 경우 한정
    • Object : @ResponseBody 어노테이션을 사용한 경우 한정
    • ResponseEntity

Reference

  • 초보 웹 개발자를 위한 스프링5 프로그래밍 입문(최범균)

0개의 댓글