여니의 프로그래밍 study/Spring & Spring Boot

[Spring] MVC 4 : 날짜 값 변환, @Path Variable, 익셉션 처리

여니's 2023. 12. 27. 22:16

 

참고 자료 : 초보 개발자를 위한 스프링5 프로그래밍 입문

 


 

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

> selectBtRegdate() 메서드는

REGDATE 값이 두 파라미터로 전달받은 from과 to 사이에 있는

Member 목록을 구한다.

 

package spring;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

public class MemberDao {

	private JdbcTemplate jdbcTemplate;
	private RowMapper<Member> memRowMapper = 
			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;
				}
			};

	public MemberDao(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	.....

	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>() {
                	@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;
	}

	

}

 

 

 

2. 커맨드 객체 DATE 타입 프로퍼티 변환처리 : @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;
	}

}

 

 

스프링은 Long이나 int 같은 기본 데이터 타입으로의 변환은

기본적으로 처리해주지만,

LocalDateTime 타입으로의 변환은 추가 설정이 필요함

 

 

@DateTimeFormat 어노테이션을 적용하면 된다.

해당 어노테이션이 적용되어 있으면

@DateTimeFormat 에서 지정한 형식을 이용하여

문자열을 LocalDateTIme 타입으로 변환함. 

 

 

yyyyMMddHH

2018030115의 문자열을

2018년 3월 1일 15시 값을 갖는 LocalDateTIme 객체로 변환해줌

 

 

컨트롤러 클래스는 별도 설정 없이

ListCommand 클래스를

커맨드 객체로 사용하면 된다.

 

package controller;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import spring.Member;
import spring.MemberDao;

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

}

 

 

새로운 컨트롤러 코드를 작성했으니

ControllerConfig 설정 클래스에

빈 설정을 추가해주면 된다.

 

 

Error 타입 파라미터를 가지게 되면,

@DateTImeFormat에 지정한 형식에 맞지 않을 경우

Errors 객체에 typeMismatch 에러 코드를 추가함.

 

 

에러코드로 typeMismatch를 추가하므로

메시지 프로퍼티 파일에 해당 메시지를 추가하면

에러 메세지를 보여줄 수 있음.

 

 

3. 변환 처리에 대한 이해

WebDataBinder가 문자열을 LocalDateTIme 타입으로 변환하는데 관여함

 

 

스프링 MVC는 요청 매핑 어노테이션 적용 메서드와

DispatcherServlet 사이를 연결하기 위해서

RequestMappingHandlerAdapter 객체를 사용함

 

 

이 핸들러 어댑터 객체는

요청 파라미터와 커맨드 객체 사이의 변환 처리를 위해

WebDataBinder를 이용함

 

 

커맨드 객체란?

소프트웨어 디자인 패턴 중 하나인 "커맨트 패턴"에서 나온 용어로

특정한 작업을 나타내는 객체를 만들어

해당 작업을 수행하는 방법을 캡슐화하고

이를 호출하는 객체와 실제 작업을 수행하는 객체를 

분리하는 것을 목적으로 함

 

 

WebDataBinder느 직접 타입을 변환하지 앟고

ConversionService에 그 역할을 위임한다.

 

@EnableWebMvc 어노테이션을 사용하면

DefaultFormattingConversionService를

ConversionService로 사용함.

 

 

4. MemberDao 클래스 중복 코드 정리 및 메서드 추가

:

package spring;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

public class MemberDao {

	private JdbcTemplate jdbcTemplate;
	private RowMapper<Member> memRowMapper = 
			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;
				}
			};

	public MemberDao(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	public Member selectByEmail(String email) {
		List<Member> results = jdbcTemplate.query(
				"select * from MEMBER where EMAIL = ?",
				memRowMapper, email);

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

	public void insert(Member member) {
		KeyHolder keyHolder = new GeneratedKeyHolder();
		jdbcTemplate.update(new PreparedStatementCreator() {
			@Override
			public PreparedStatement createPreparedStatement(Connection con)
					throws SQLException {
				// 파라미터로 전달받은 Connection을 이용해서 PreparedStatement 생성
				PreparedStatement pstmt = con.prepareStatement(
						"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
								"values (?, ?, ?, ?)",
						new String[] { "ID" });
				// 인덱스 파라미터 값 설정
				pstmt.setString(1, member.getEmail());
				pstmt.setString(2, member.getPassword());
				pstmt.setString(3, member.getName());
				pstmt.setTimestamp(4,
						Timestamp.valueOf(member.getRegisterDateTime()));
				// 생성한 PreparedStatement 객체 리턴
				return pstmt;
			}
		}, keyHolder);
		Number keyValue = keyHolder.getKey();
		member.setId(keyValue.longValue());
	}

	public void update(Member member) {
		jdbcTemplate.update(
				"update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
				member.getName(), member.getPassword(), member.getEmail());
	}

	public List<Member> selectAll() {
		List<Member> results = jdbcTemplate.query("select * from MEMBER",
				memRowMapper);
		return results;
	}

	public int count() {
		Integer count = jdbcTemplate.queryForObject(
				"select count(*) from MEMBER", Integer.class);
		return count;
	}

	public List<Member> selectByRegdate(LocalDateTime from, LocalDateTime to) {
		List<Member> results = jdbcTemplate.query(
				"select * from MEMBER where REGDATE between ? and ? " +
						"order by REGDATE desc",
				memRowMapper,
				from, to);
		return results;
	}

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

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

}

 

 

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

 

경로의 일부가 고정되어 있지 않고

달라질 때 사용할 수 있는 것이

@PathVariable 어노테이션 !

가변 경로를 처리할 수 있음

 

{경로변수}와 같이

중괄호로 둘러 쌓인 부분을 경로 변수라고 부른다.

 

여기에 해당하는 값은 

같은 경로 변수 이름을 지정한 @PathVariable 파라미터에 전달이 된다.

 

예를 들어

/member/{id}에서 {id}에 해당하는 부분의 경로 값을

@PathVariable("id") 어노테이션이 적용되

memId 파라미터에 전달된다. 

 

 

6. 컨트롤러 익셉션 처리하기

 

익셉션 화면이 보이는 것보다

알맞게 익셉션을 처리하여 

사용자에게 더 적합한 안내를 해주는 것이 좋다.

 

 

타입 변환 실패에 따른 익셉션 시

@ExceptionHandler 어노테이션을 사용한다. 

 

같은 컨트롤러에 @ExceptionHandler 어노테이션을 적용한 메서드가 존재하면

그 메서드가 익셉션을 처리함.

 

즉, 컨트롤러에서 발생한 익셉션을 직접 처리하고 싶다면

해당 어노테이션을 적용하 메서드를 구현하면 된다.

 

 

익셉션 객체에 대한 정보를 알고 싶다면

메서드의 파라미터로 익셉션 객체를 전달받아 사용한다.

 

@ExceptionHandler(TypeMismatchException.class)
public String handleTypeMisMatchException(TypeMismatchException ex) {
	// ex 사용해서 로그 남기는 등 작업
    return "member/invalide";
}

 

 

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

: @ExceptionHandle 어노테이션을 적용하면

해당 컨트롤러에서 발생한 익셉션 만을 처리함.

 

 

여러 컨트롤러에서 동일하게 처리할 익셉션이 발생하면

@ControllerAdvice 어노테이션을 이용하여

중복을 없앨 수 있음. 

 

@ControllerAdvice("spring")
public class CommandExceptionHandle {
	@ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException() {
    	return "error/commonException";
    }
}

 

 

@ControllerAdvice 어노테이션이 적용된 클래스는

지정한 범위의 컨트롤러에

공통으로 사용될 설정을 지정할 수 있음.

 

 

위 코드는 spring 패키지와 그 하위 패키지에 속한 컨트롤러 클래스를 위한

공통기능을 정의함

 

 

단, @ControllerAdvice 적용 클래스가 동작하려면

해당 클래스를 스프링에 빈으로 등록해야 함