4. Todo 기능 개발
- TodoMapper → TodoService → TodoController → JSP 순서로 처리
1) TodoMapper 개발 및 테스트
- TodoVO를 파라미터로 입력받는 insert() 추가
// TodoMapper
package org.zerock.springex.mapper;
import org.zerock.springex.domain.TodoVO;
public interface TodoMapper {
String getTime();
void insert(TodoVO todoVO);
}
- resources > mappers 폴더에 만들어둔 TodoMapper.xml에 insert를 구현
<!-- TodoMapper.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTO Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "org.zerock.springex.mapper.TodoMapper">
<select id = "getTime" resultType = "string">
select now()
</select>
<!-- insert 추가 -->
<insert id = "insert">
<!-- Mybatis 이용시, #{title}, #{dueDate}, #{writer}를 파라미터로 처리
이 파라미터 부분은 PreparedStatement로 다시 변경되면서 '?'로 처리됨
주어진 객체의 getTitle()을 호출한 결과를 적용하게 됨 -->
insert into tbl_todo(title, dueDate, writer) values (#{title}, #{dueDate}, #{writer})
</insert>
</mapper>
- 테스트 코드를 통해 TodoVO의 입력을 확인
package org.zerock.springex.mapper;
import ...
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
@Autowired(required = false)
private TodoMapper todoMapper;
...
@Test
public void testInsert() {
TodoVO todoVO = TodoVO.builder()
.title("스프링 테스트")
.dueDate(LocalDate.of(2022,10,10))
.writer("user00")
.build();
todoMapper.insert(todoVO);
}
}

2) TodoService와 TodoServiceImpl 클래스
- TodoMapper와 Todocontroller 사이에는 서비스 계층을 설계해서 적용
- TodoService 인터페이스 추가 → 이를 구현한 TodoServiceImpl을 Bean으로 처리
// TodoService
package org.zerock.springex.service;
import org.zerock.springex.dto.TodoDTO;
// register는 여러 개의 파라미터 대신 TodoDTO로 묶어서 전달받음
public interface TodoService {
void register(TodoDTO todoDTO);
}
- TodoServiceImpl에는 의존성 주입을 이용하여 데이터베이스 처리를 하는 TodoMapper와 DTO, VO의 변환을 처리하는 ModelMapper를 주입
// service > TodoServiceImpl
package org.zerock.springex.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.zerock.springex.domain.TodoVO;
import org.zerock.springex.dto.TodoDTO;
import org.zerock.springex.mapper.TodoMapper;
@Service
@Log4j2
@RequiredArgsConstructor
// 의존성 주입이 필요한 객체의 타입을 final로 지정
// @RequiredArgsConstructor를 이용해서 생성자를 생성하는 방식
public class TodoServiceImpl implements TodoService {
// 의존성 주입을 이용해 데이터베이스 처리를 하는 TodoMapper
private final TodoMapper todoMapper;
// VO의 변환을 처리하는 ModelMapper
private final ModelMapper modelMapper;
@Override
public void register(TodoDTO todoDTO) {
log.info(modelMapper);
// ModelMapper를 이용해서 TodoDTO를 TodoVO로 변환
TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
log.info(todoVO);
// TodoMapper를 통해 insert 처리
todoMapper.insert(todoVO);
}
}
- service 패키지를 webapp > WEB-INF > root-context.xml에 component-scan 패키지로 추가
<mybatis:scan base-package = "org.zerock.springex.mapper"></mybatis:scan>
<!-- ModelMapperConfiguration을 스프링의 Bean으로 인식시키기 위한 추가 -->
<context:component-scan base-package = 'org.zerock.springex.config'/>
<!-- service 패키지 추가 -->
<context:component-scan base-package = 'org.zerock.springex.service'/>
3) TodoService 테스트
- 서비스 계층에서 DTO를 VO로 변환하는 작업을 테스트
// TodoServiceTests
package org.zerock.springex.service;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.zerock.springex.dto.TodoDTO;
import java.time.LocalDate;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoServiceTests {
@Autowired
private TodoService todoService;
@Test
public void testRegister() {
TodoDTO todoDTO = TodoDTO.builder()
.title("Test.....")
.dueDate(LocalDate.now())
.writer("user1")
.build();
todoService.register(todoDTO);
}
}


4) TodoController의 GET/POST 처리
- register.jsp에 class 속성이 "card-body"로 지정된 부분의 코드를 다음과 같이 수정
<!-- register.jsp -->
<%@ taglib prefix ="form" uri = "http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello, world</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
crossorigin="anonymous">
</head>
<body>
<div class = "container-fluid">
...
<div class = "row content">
...
<div class="card-body">
<form action = "/todo/register" method = "post">
<div class = "input-group mb-3">
<span class = "input-group-text">Title</span>
<input type = "text" name = "title" class = "form-control" placeholder = "Title">
</div>
<div class = "input-group mb-3">
<span class = "input-group-text">DueDate</span>
<input type = "date" name = "dueDate" class = "form-control" placeholder = "Writer">
</div>
<div class = "input-group mb-3">
<span class = "input-group-text">Writer</span>
<input type = "date" name = "dueDate" class = "form-control" placeholder = "Writer">
</div>
<div class = "my-4">
<div class = "float-end">
<button type = "submit" class = "btn btn-primary">Submit</button>
<button type = "result" class = "btn btn-secondary">Reset</button>
</div>
</div>
</form>
</div>
...
</div>
</body>
</html>

- post 방식의 처리
- register.jsp의 <form action = "/todo/register" method = "post"> 태그에 의해 [Submit]버튼을 클릭하면 POST 방식으로 "title, dueDate, writer"을 전송
- TodoController에서는 TodoDTO로 바로 전달된 파라미터 값들을 수집
// TodoController
package org.zerock.springex.controller;
import ...
@Controller
@RequestMapping("/todo")
@Log4j2
public class TodoController {
...
//POST 방식으로 처리한 후에 "/todo/list"로 이동해야 하므로 "redirect:/todo/list"로 이동할 수 있도록 문자열을 반환할 수 있게 처리
@PostMapping("/register")
public String registerPOST(TodoDTO todoDTO, RedirectAttributes redirectAttributes) {
log.info("POST todo register...............");
log.info(todoDTO);
return "redirect:/todo/list";
}
}
- todo/register에서 정보 입력 후 submit 버튼을 클릭하면 로그에 다음과 같이 정보가 출력됨
- 페이지는 todo/list로 redirect됨


5) 한글 처리를 위한 필터 설정

- 위와 같이 한글을 쓰면 로그에 한글이 깨져서 출력되므로 스프링 MVC에서 제공하는 필터로 처리가 필요
<web-app ... >
...
<!-- 한글 설정을 위한 필터(web-app 태그 안에 입력) -->
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
</web-app>

6) @Valid를 이용한 서버사이드 검증
- 브라우저를 사용하는 프론트 쪽과 더불어 서버를 사용하는 백에서도 입력되는 값들의 유효성 검증을 하는 것이 일반적
- 검증 작업은 Controller에서 진행되며 스프링 MVC의 경우 @Valid와 BindingResult를 이용해서 처리
- 스프링 MVC에서 검증을 처리하기 위해 hibernate-validate 라이브러리가 필요
// build.gradle
// 유효성 검증을 위한 Validate 관련 라이브러리
implementation group: 'org.hibernate', name: 'hibernate-validator', version: '6.2.1.Final'
- hibernate-validator를 이용해서 사용하는 대표적인 어노테이션
@NotNull | Null 불가 |
@Null | Null만 입력 가능 |
@NotEmpty | Null, 빈 문자열 불가 |
@NotBlank | Null, 빈 문자열, 스페이스만 있는 문자열 불가 |
@Size(min=,max=) | 문자열, 배열 등의 크기가 만족하는가? |
@Pattern(regex=) | 정규식을 만족하는가? |
@Max(num) | 지정 값 이하인가? |
@Min(num) | 지정 값 이상인가? |
@Future | 현재보다 미래인가? |
@Past | 현재보다 과거인가? |
@Positive | 양수만 가능 |
@PositiveOrZero | 양수와 0만 가능 |
@Negative | 음수만 가능 |
@NegativeOrZero | 음수와 0만 가능 |
- TodoDTO 검증하기
- TodoDTO에 어노테이션 적용하여 수정
// TodoDTO
package org.zerock.springex.dto;
import lombok.*;
import javax.validation.constraints.Future;
import javax.validation.constraints.NotEmpty;
import java.time.LocalDate;
@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {
private Long tno;
@NotEmpty
private String title;
@Future
private LocalDate dueDate;
private boolean finished;
@NotEmpty
private String writer;
}
- TodoController에서 POST 방식으로 처리할 때 이를 반영하도록 BindingResult와 @Valid 어노테이션을 적용
// TodoController
package org.zerock.springex.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.zerock.springex.dto.TodoDTO;
import org.zerock.springex.service.TodoService;
import javax.validation.Valid;
@Controller
@RequestMapping("/todo")
@Log4j2
@RequiredArgsConstructor
public class TodoController {
// 클래스 선언부에서 RequestMapping의 value가 "/todo"이고 list() 메서드에서 ReuestMapping의 value가 "/list"이므로
// 최종 경로는 "/todo/list"가 됨
@RequestMapping("/list")
public void list() {
log.info("todo list..........");
}
// @RequestMapping(value = "/register", method = RequestMethod.GET)
@GetMapping("/register")
public void registerGET() {
log.info("GET todo register...............");
}
@PostMapping("/register")
public String registerPOST(@Valid TodoDTO todoDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
log.info("POST todo register...............");
// 검증 과정에 문제가 있다면 다시 입력 화면으로 Redirect 되도록 처리
// 잘못된 결과는 RedirectAttributes의 addFlashAttributes()를 이용해서 전달
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
return "redirect:/todo/register";
}
log.info(todoDTO);
return "redirect:/todo/list";
}
}
- writer에 @NotNull가 적용되어 있으므로 다음과 같이 Writer항목이 없다면 todo/register의 처음 화면으로 redirect 됨



- JSP에서 검증 에러 메세지 확인하기
- JSP 상단에 태그 라이브러리 추가
<%@ taglib prefix ="c" uri = "http://java.sun.com/jsp/jstl/core" %>
- <form> 태그가 끝난 후 <script> 태그 추가
<!-- register.jsp -->
...
</form>
<script>
const serverValidResult = {}
<c:forEach items = "${errors}" var = "error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>

- 빈 화면으로 Submit 클릭 시, 다음과 같은 내용이 출력됨

- 과거 날짜 입력 시, 다음과 같은 내용이 출력되기도 함

7) Todo 등록 기능 완성
- TodoService 주입하고 연동하도록 구성
- TodoController의 클래스 선언부에서 TodoService 주입
- registerPost()에서는 TodoService의 기능을 호출하도록 구성
// TodoController
public class TodoController {
private final TodoService todoService;
...
@PostMapping("/register")
public String registerPOST(@Valid TodoDTO todoDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
log.info("POST todo register...............");
// 검증 과정에 문제가 있다면 다시 입력 화면으로 Redirect 되도록 처리
// 잘못된 결과는 RedirectAttributes의 addFlashAttributes()를 이용해서 전달
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
return "redirect:/todo/register";
}
log.info(todoDTO);
todoService.register(todoDTO);
return "redirect:/todo/list";
}



8) Todo 목록 기능 개발
- TodoMapper의 개발
- TodoMapper 인터페이스에는 가장 최근 등록된 TodoVO가 우선적으로 나올 수 있도록 selectAll() 추가
// TodoMapper
package org.zerock.springex.mapper;
import org.zerock.springex.domain.TodoVO;
import java.util.List;
public interface TodoMapper {
String getTime();
void insert(TodoVO todoVO);
List<TodoVO> selectAll();
}
- resource > mappers > TodoMapper.xml에 selectAll()의 실제 쿼리문 작성
<!-- TodoMapper.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTO Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "org.zerock.springex.mapper.TodoMapper">
<select id = "getTime" resultType = "string">
select now()
</select>
<insert id = "insert">
insert into tbl_todo(title, dueDate, writer) values (#{title}, #{dueDate}, #{writer})
</insert>
<!-- resultType은JDBC의 한 행을 어떤 타입의 객체로 만들것 인지 지정 -->
<select id = "selectAll" resultType = "org.zerock.springex.domain.TodoVO">
select * from tbl_todo order by tno desc
</select>
</mapper>
- test폴더에 TodoMapperTests에 테스트 코드 작성
// TodoMapperTests
...
@Test
public void testSelectAll() {
List<TodoVO> voList = todoMapper.selectAll();
voList.forEach(vo -> log.info(vo));
}
}
- 테스트 코드 수행 시 다음과 같이 나중에 추가된 데이터를 우선 출력

- TodoService / TodoServiceImpl의 개발
- 서비스 계층의 개발은 특별한 파라미터가 없는 경우 TodoMapper를 호출하는 것이 전부
- TodoMapper가 반환하는 데이터의 타입이 List<TodoVO>이기 때문에 이를 List<TodoDTO>로 변환하는 작업이 필요
- TodoService 인터페이스에 getAll() 기능 추가
// TodoService
package org.zerock.springex.service;
import org.zerock.springex.dto.TodoDTO;
import java.util.List;
// register는 여러 개의 파라미터 대신 TodoDTO로 묶어서 전달받음
public interface TodoService {
void register(TodoDTO todoDTO);
List<TodoDTO> getAll();
}
- TodoServiceImpl에서 getAll()을 다음과 같이 개발
// TodoServiceImpl
package org.zerock.springex.service;
import ...
@Service
@Log4j2
@RequiredArgsConstructor
public class TodoServiceImpl implements TodoService {
...
@Override
public List<TodoDTO> getAll() {
// stream의 map()을 이용해서 TodoVOfmf TodoDTO로 변경
// collect()를 이용해서 List<TodoDTO>로 묶어줌
List<TodoDTO> dtoList = todoMapper.selectAll().stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
return dtoList;
}
}
- TodoController의 처리
- TodoController의 list() 기능에서 TodoService를 처리하고 Model에 데이터를 담아서 JSP로 전달
// TodoController
package org.zerock.springex.controller;
import ...
public class TodoController {
...
@RequestMapping("/list")
public void list(Model model) {
log.info("todo list.......");
// Model에 dtoList라는 이름으로 목록 데이터를 담았기 때문에 JSP에서 JSTL을 이용해서 목록 출력
model.addAttribute("dtoList", todoService.getAll());
}
...
}
- WEB-INF > views > todo > list.jsp 페이지 상단에 JSP 관련 설정과 JSTL 설정 추가
<!-- list.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix ="c" uri = "http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
...
</head>
<body>
<div class = "row content">
<div class = "col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<table class = "table">
<thead>
<tr>
<th scope = "col">Tno</th>
<th scope = "col">Title</th>
<th scope = "col">Writer</th>
<th scope = "col">DueDate</th>
<th scope = "col">Finished</th>
</tr>
</thead>
<tbody>
<c:forEach items = "${dtoList}" var = "dto">
<tr>
<th scope = "row"><c:out value = "${dto.tno}"/></th>
<td><c:out value = "${dto.title}"/></td>
<td><c:out value = "${dto.writer}"/></td>
<td><c:out value = "${dto.dueDate}"/></td>
<td><c:out value = "${dto.finished}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</div>
...
</body>
</html>
- localhost:8080/todo/list 경로로 접속했을 때, 가장 최근 등록된 것부터 출력됨

'back-end > Java' 카테고리의 다른 글
[자바 웹 개발 워크북] 4.4 - 스프링 Web MVC 구현하기(4) (0) | 2023.02.21 |
---|---|
[자바 웹 개발 워크북] 4.4 - 스프링 Web MVC 구현하기(3) (0) | 2023.02.15 |
[자바 웹 개발 워크북] 4.4 - 스프링 Web MVC 구현하기(1) (0) | 2023.02.09 |
[자바 웹 개발 워크북] 4.3 - 스프링 Web MVC 기초 (0) | 2023.01.17 |
[자바 웹 개발 워크북] 4.2 - MyBatis와 스프링 연동 (0) | 2023.01.13 |