상세 컨텐츠

본문 제목

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

22-23/22-23 Spring 2

by YUZ 유즈 2023. 1. 12. 10:00

본문

728x90

14장의 키워드

@DateTimeFormat 애노테이션

@PathVariable 애노테이션

@ExceptionHeader 애노테이션

@ControllerAdvice 애노테이션

 


💡이번 장에서 구현할 기능

1. 날짜를 이용한 회원 검색 기능 추가

2. 회원 ID를 이용한 회원 정보 조회 기능 추가

3. 위 2개의 기능에서 발생할 수 있는 에러 처리

 

 

1. 날짜를 이용한 회원 검색 기능 추가

  • 회원 가입 일자를 기준으로 검색하는 기능 ⇒ selectByRegdate() 메서드
// p.383
public class MemberDao{
  ...
  // 특정 기간 동안에 가입한 회원 목록을 보여주는 기능
  // from, to 파라미터로 날짜를 입력받는다.
  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>() { // 쿼리를 통해 from, to 사이에 있는 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;
	}
    
    ...
    
  }

 

 

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

시작 시간 from과 끝 시간 to를 파라미터로 전달 ⇒ 커맨드 객체로 사용할 클래스(ListCommand) 작성

✏️커맨드 객체: 요청 파라미터의 값을 객체에 담는 역할, 값을 전달하고 받을 수 있는 getter/setter을 필수적으로 포함

# 커맨드 객체에 대한 내용은 281~282p 참고

// p.384
public class ListCommand {
	// LocalDateTime 타입으로 받음.
	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;
	}

}
// 검색을 위한 입력 폼 작성
<input type="text" name="from" />
<input type="text" name="to" />

 

 

🚨Problem

<input> 태그에 입력한 문자열(String)을 LocalDateTime 타입으로 변환해야 한다.

  • <input>에 2023년 1월 9일 오후 6시를 표현하기 위해 "2023010918"을 입력, 이 문자열을 LocalDateTime 변환 필요 
  • Spring은 Long과 Int 등의 기본 데이터 타입 변환은 기본적으로 처리해 줌, LocalDateTime의 변환 경우 추가 설정 필요

◾1. @DateTimeFormat 애노테이션을 파라미터에 적용한다.

ex) "2023010918"의 문자열을 pattern 속성값으로 "yyyyMMddHH"를 주면 2023년 1월 9일 18시의 값을 갖는 객체로 변환

public class ListCommand {
	// 애노테이션에 지정한 형식을 이용하여 문자열을 LocalDateTime 타입으로 변환
	// 지정 형식: pattern 속성값으로 준다.
	@DateTimeFormat(pattern = "yyyyMMddHH")
	private LocalDateTime from;
    
	@DateTimeFormat(pattern = "yyyyMMddHH")
	private LocalDateTime to;
    
    ...
}

 

◾2. 컨트롤러 클래스에서 ListCommand를 커맨드 객체로 사용한다.

from과 to 사이에 회원 가입을 진행한 유저 목록을 members 속성으로 전달

✏️ @ModelAttribute 애노테이션으로 커맨드 객체 속성 이름 변경 (286p 참고)

@ModelAttribute 애노테이션은 모델에서 사용할 속성 이름을 값으로 설정, 뷰 코드에서 설정한 값으로 커맨드 객체에 접근

// p.386
@Controller
public class MemberListController {
...

@RequestMapping("/members")
	public String list(
			@ModelAttribute("cmd") ListCommand listCommand, // 뷰에서 cmd 이름으로 객체 접근
			Model model) {
        // from, to가 모두 null이 아닐 경우 if 블럭 안에 있는 문장 실행
		if (listCommand.getFrom() != null && listCommand.getTo() != null) {
			// from, to 사이에 회원가입을 진행한 회원 목록 가져옴
            List<Member> members = memberDao.selectByRegdate(
					listCommand.getFrom(), listCommand.getTo());
			// 모델 속성 추가: "members" 이름으로 속성 추가
            model.addAttribute("members", members);
		}
		return "member/memberList"; 
	}


...

}

 

◾3. ControllerConfig 설정 클래스에 관련 빈 설정을 추가한다.

@Configuration
public class ControllerConfig {

    // 자동 주입 설정
	@Autowired
	private MemberDao memberDao;

    // 스프링에 빈 등록 
	@Bean
	public MemberListController memberListController() {
		MemberListController controller = new MemberListController();
		controller.setMemberDao(memberDao);
		return controller;
	}
}

 

◾4. JSP 코드를 작성한다.

  • 문자열이 LocalDateTime 타입 프로퍼티로 잘 변환되는지 확인

커스텀 태그 파일 작성, JSTL(커스텀 태그 라이브러리)이 제공하는 날짜 형식 태그는 LocalDateTime 타입을 지원 X

⇒ 태그 파일 사용

// formatDate.tag
<%@ 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) %>

 

<%@ 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 객체는 @DateTimeFormat 애노테이션에 설정한 패턴을 사용하여 값을 변환 후 출력
    // input 태그를 사용하여 사용자에게 값을 입력 받음
    // form:input 속성의 path를 사용하여 연결할 객체 설정
        <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>

 

웹 브라우저에서 http://localhost:8080/sp5-chap14/member/list 주소를 입력하면 from과 to 파라미터가 존재하지 않기에 각각 null 값이 된다. from과 to의 값이 모두 null이 아닐 때만 데이터를 읽어오도록 코드를 작성한다.

 

변환 처리는 누가 해주나요?

위의 예제에서 문자열을 LocalDateTime 타입으로 변환하는 것을 확인했다.

그럼 타입을 변환해주는 역할을 하는 것을 무엇인가? ⇒ WebDataBinder(12장 참고)

  • 로컬 범위 Validator 뿐만 아니라 값의 타입 변환에도 관여

스프링 MVC 구조 (250p)

  • 요청 매핑 애노테이션(Controller)과 DispatcherServlet를 연결하기 위해 RequestHandlerAdapter 객체 사용
  • HandlerAdapter는 요청 파라미터와 커맨드 객체 사이의 변환 처리를 위해 WebDataBinder 사용

✏️WebDataBinder(344~345p)

  • @InitBinder 애노테이션을 사용했을 때 나왔던 타입
  • 데이터 검증, 타입 변환

1. 커맨드 객체 생성 및 초기화

커맨드 객체의 프로퍼티를 초기화한다.

커맨드 객체 생성, 커맨드 객체의 프로퍼티와 같은 이름을 갖는 요청 파라미터를 이용하여 프로퍼티 값을 생성한다.

직접 타입을 변환하지 않고 ConversationService에 위임한다.

스프링 MVC를 위한 설정인 @EnableWebMvc 애노테이션을 사용하면 DefaultFormattingConversationService를 ConversationService으로 사용한다. 즉, 기본 데이터 타입뿐만 아니라 @DateTimeFormat 애노테이션을 사용한 시간 관련 타입도 변환할 수 있다.

 

2. <from:input>에서 사용

<form:input path="from" />

<input type="text" id ="from" name="from" value="2019010918">

이 태그를 사용하면 path 속성에 지정한 프로퍼티 값을 String으로 변환하여 <input> 태그의 value 속성값으로 생성

프로퍼티 값을 String으로 변환할 때 WebDataBinder의 ConversationService 사용

 

✏️중복 코드 정리 및 메서드 추가

RowMapper 객체를 생성하는 부분에서 중복 코드 발생(교재 394p 참고)

공통되는 부분을 줄임, 중복되는 코드 제거(395p)

 

2. 회원 ID를 사용한 회원 정보 조회 기능 추가

MemberDao 클래스에 다음 코드를 작성한다.

코드는 해당 ID를 갖는 Member의 정보를 result 변수로부터 받는다.

// 395~396p
...
   public Member selectById(Long memId) {
		List<Member> results = jdbcTemplate.query(
				"select * from MEMBER where ID = ?",
				memRowMapper, memId);

		return results.isEmpty() ? null : results.get(0);
	}
    
...

 

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

◾1. 가변 경로 처리 코드를 작성한다.

ID가 10에 해당하는 회원 정보 조회 URL: http://localhost:8080/sp5-chap14/members/10

⇒ 각 회원마다 회원 ID가 다르기 때문에 경로의 마지막 부분이 달라짐

⇒ @PathVariable 애노테이션을 통해 가변적인 경로를 처리할 수 있다.

// 396~397p
//MemberDetailController.java

...
	@GetMapping("/members/{id}") // 중괄호로 둘러싸인 부분을 경로변수라고 한다.
	    // {경로변수}에 해당하는 값은 @Pathvariable 파라미터에 전달된다.
        // 애노테이션이 적용된 memId 파라미터에 값 전달
		// 이때, String 타입의 값 "10"을 Long 타입으로 변환
    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";
	}
...

 

◾2. MemberDetailController을 설정 클래스에 빈으로 등록한다.

// 397p
// ControllerConfig.java
...
	@Bean
	public MemberDetailController memberDetailController() {
		MemberDetailController controller = new MemberDetailController();
		controller.setMemberDao(memberDao);
		return controller;
	}
    
...

 

◾3. 결과를 보여줄 JSP 코드를 작성한다.

코드 생략, 398p

 

3. 에러 처리

🚨발생할 수 있는 에러

1. pattern 속성에 입력한 날짜 지정 형식과 일치하지 않을 때

   ex) 형식은 "yyyyMMddHH"인데, 20230109(yyyyMMdd)까지 입력했을 때

2. 해당 회원 ID가 존재하지 않을 때

   ex) 존재하지 않는 회원 ID인 0을 경로변수로 사용했을 때

3. 경로변수의 타입(Long)으로 변환할 수 없는 값(String)을 입력했을 때 

   ex) http://localhost:8080/sp5-chap14/members/a를 주소창에 입력

 

사용자에게 익셉션 화면이 보이게 하는 것보다 익셉션에 따른 알맞은 처리를 통해 적합한 안내 화면을 보여주도록 한다.

 

1번의 에러 처리: Errors 객체 사용

  1. 메서드가 Errors 타입의 파라미터를 갖도록 코드를 작성한다. 이때, listCommand 파라미터 바로 뒤에 Errors 객체 위치한 것 유의한다.(344~345p)
  2. ✏️ 메서드가 Errors 타입의 파라미터를 가질 때, @DateTimeFormat에 지정한 형식이 맞지 않으면 Errors 객체에 "typeMismatch" 에러 코드를 추가한다.
  3. 프로퍼티 파일에 해당 메시지를 추가하여 에러 메시지를 보여준다.
// MemberListCnotroller.java
 ...
   @RequestMapping("/members")
	public String list(
    		// 커맨드 객체의 값의 유효성 확인 후 그 결과를 Errors 객체에 담는다.
			@ModelAttribute("cmd") ListCommand listCommand,
			Errors errors, Model model) { // Errors 객체를 파라미터에 추가
		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";
	}
...
    
// label.properties
typeMismatch.java.time.LocalDateTime=잘못된 형식

@ExceptionHandler 애노테이션

이 애노테이션을 적용한 메서드가 존재하면 해당 메서드가 Exception을 처리

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

    // Exception에 따라서 서로 다른 뷰 이름을 리턴하도록 처리
	@ExceptionHandler(TypeMismatchException.class) // 경로 변수값의 타입이 올바르지 않을 때 발생
	public String handleTypeMismatchException() { // TypeMismatchException을 해당 메서드가 처리
		return "member/invalidId"; // 뷰 이름 리턴
	}
    
	@ExceptionHandler(MemberNotFoundException.class) // 해당 Member을 찾을 수 없을 때 발생
	public String handleNotFoundException() {// MemberNotFoundException을 해당 메서드가 처리   
		return "member/noMember"; // 뷰 이름 리턴
	}
  
}

 

@ExceptionHandler 애노테이션 적용 메서드의 파라미터와 리턴 타입

  • 파라미터: HttpServletRequest, HttpServletResponse, HttpSession
  • 리턴: ModelAndView, String, (@ResponseBody 애노테이션을 붙인)임의 객체, ResponseEntity (16장)

 

@ControllerAdvice를 이용한 공통 익셉션 처리 

@ExceptionHandler는 해당 컨트롤러에서 발생한 Exception만을 처리

다수의 컨트롤러에서 동일 타입의 Exception이 발생했을 때는 어떻게 처리할까?

 

@ControllerAdvice 애노테이션을 이용하여 익셉션 처리 코드의 중복을 없앨 수 있다.

✏️ @ControllerAdvice를 적용한 클래스가 동작하려면 클래스를 스프링에 빈으로 등록 해야한다.

// spring에 해당하는 패키지와 하위 패키지에 속한 컨트롤러 클래스에서 발생하는 익셉션을 공통으로 처리
@ControllerAdvice("spring")
public class CommonExceptionHandler{
	
    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(){
    	return "error/commonException";
    }

}

 

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

  • value basePackages: String[]  | 공통 설정을 적용할 컨트롤러가 속하는 기준 패키지
  • annotations: Class<? Extends Annotation>[]  | 특정 애노테이션이 적용된 컨트롤러 대상
  • assignable Types: Class<?>[]  | 특정 타입 또는 그 하위 타입인 컨트롤러 대상

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

1. 컨트롤러 클래스에 작성한 @ExceptionHandler 적용 메서드

2. @ControllerAdvice를 적용한 클래스에 작성한 @ExceptionHandler 적용 메서드

 

 

빈칸 문제

1. 커맨드 객체의 파라미터에 (@DateTimeFormat) 애노테이션이 적용되어 있으면 문자열을 LocalDateTime 타입으로 변환해준다. 이때, (pattern) 속성은 입력된 값을 다양한 형식으로 설정하여 값을 출력한다.

2. (WebDataBinder)에서 값을 검증하고 타입을 변환하는 역할을 한다. 

3. 경로 http://localhost:8080/sp5-chap14/members/{id}에서 중괄호로 둘러싸인 부분({id})을 (경로 변수)(이)라고 한다.

4. {id}에 해당하는 값은 (@PathVariable) 애노테이션이 적용된 파라미터로 값이 전달된다. 이 애노테이션을 사용하면 ( 가변적인 ) 경로를 처리할 수 있다. 

5. Exception이 발생했을 때, 컨트롤러 내의 (@ExceptionHandler) 애노테이션을 적용한 메서드가 존재하면, 해당 메서드가 익셉션을 처리한다.

6. Exception 처리 코드의 중복을 줄이기 위해 (@ControllerAdvice) 애노테이션을 사용하여 Exception을 공통으로 처리하도록 한다.   

7. @ExceptionHandler 애노테이션 적용 메서드의 우선순위가 있다. (컨트롤러) 클래스에 작성한 @ExceptionHandler 메서드 중 해당 익셉션을 처리할 수 있는지 확인 후에 없는 경우, (@ControllerAdvice)을/를 적용한 클래스에서 @ExceptionHandler 메서드를 검색한다.

코드 문제

1. 가변 경로를 처리하는 코드를 작성하려고 한다. 다음 코드를 채워보자.

경로는 post/{경로변수}, 경로 변수의 이름은 postingCode로 하자.

@GetMapping("___________")
public String detail(__________ Long postId, Model model){
	Post post = postDao.selectById(postId);
    if(post == null){
    	throw new PostNotFoundException();
    }
    model.addAttribute("post", post);
    return "post/postDetail";
}

 

2. 여러 개의 컨트롤러에서 발생할 수 있는 동일한 에러를 처리하기 위해 에러 처리 코드를 작성해보자. 

클래스 이름: ErrorSpring20230109

에러 처리 범위: "2023y01m09d" 패키지

발생 에러: TypeMismatchException

메서드 이름: handleTypeMismatchException

리턴할 뷰 이름: "error/typemismatch"


Corner Spring 2

Editor : otcr

 
728x90

관련글 더보기