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

위의 테스트 코드로 tbl_todo 테이블에 정상적으로 추가된 정보

 

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

정상적으로 테스트 내용이 추가된 테이블
로그에서 TodoServiceImpl이 동작하는 것을 확인

 

 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>

localhost:8080/todo/register의 화면

 

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

정상적으로 처리되어 todo/list로 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 경로로 접속했을 때, 가장 최근 등록된 것부터 출력됨

+ Recent posts