상세 컨텐츠

본문 제목

[스프링1] 14장.MVC4 : 날짜 값 변환, @PathVariable, 익셉션 처리

22-23/22-23 Spring 1

by YUZ 유즈 2022. 12. 8. 10:09

본문

728x90

해당 포스트는 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 [최범균 저] 책 내용을 참고하였습니다.


13장에서 작성한 예제를 이어서 작성합니다.

❗ 회원가입 날짜로 회원 검색

📌selectByRegdate

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

 

📌@DateTimeFormat

검색을 위한 폼은 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를 사용한다.


❗ @PathVariable

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를 적용한 메서드가 존재하면 그 메서드가 익셉션 처리한다.
한 컨트롤러 안에서 발생한 익셉션을 처리한다.

@ExceptionHandler(TypeMismatchException.class)
public String handleTypeMismatchException() {
    return "member/invalidId";
}

@ExceptionHandler(MemberNotFoundException.class)
public String handleNotFoundException() {
    return "member/noMember";
}

 

📌@ControllerAdvice

다수의 컨트롤러에서 동일 타입의 익셉션을 처리하고 싶다면 @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 적용 클래스가 동작하려면 해당 클래스를 스프링에 빈으로 등록해야한다.

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

  • 같은 컨트롤러에 위치한 @ExceptionHandler 메서드 중 해당 익셉션을 처리할 수 있는 메서드를 검색
  • 같은 클래스에 위치한 메서드가 익셉션을 처리할 수 없을 경우 @ControllerAdvice 클래스에 위치한 @ExceptionHandler 메서드를 검색

@ControllerAdvice 애노테이션은 공통 설정을 적용할 컨트롤러 대상을 지정하기 위해 아래와 같은 속성을 제공

속성 타입 설명
valuebasePackages String[] 공통 설정을 적용할 컨트롤러가 속하는 기준 패키지
annotations Class<? extends Annotation>[] 특정 애노테이션이 적용된 컨트롤러 대상
assignableTypes Class<?>[] 특정 타입 또는 그 하위 타입인 컨트롤러 대상

 


Spring 1
EDITOR: OJO

728x90

관련글 더보기