Spring - CRUD

HI_DO·2024년 6월 13일
post-thumbnail

스프링 프레임워크와 MyBatis, 스프링 MVC를 모두 결합한 프로젝트 진행

MyBatis와 스프링을 이용한 영속처리

  • 영속처리: 데이터를 영구적으로 저장하고 유지
    1) VO 선언
    2) Mapper
    3) xml
    4) 테스트 코드

Todo기능 개발

  • 등록작업 TodoMapper -> TodoService -> TodoController ->jsp 순서로 진행

TodoMapper

  • TodoMapper.java
  • TodoMapper.xml
  • TodoMapperTests.java

TodoService

  • TodoService.java
  • TodoServiceImpl.java
  • TodoServiceTests.java

TodoController

  • TodoController.java
package org.zerock.springex.controller;
import org.zerock.springex.dto.TodoDTO;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
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.bind.annotation.RequestMethod;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/todo")
@Log4j2
public class TodoController {
    @RequestMapping("/list")
    public void list() {
        log.info("todo list...............");
    }
    @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~~~~~");
        if(bindingResult.hasErrors()) {
            log.info("has errors!!!!!!!");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/todo/register";
        }
        log.info(todoDTO);
        return "redirect:/todo/list";
    }
}

jsp

  • register.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>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
    <div class="row">
        <!-- 기존의 <h1>Header</h1> -->
        <div class="row">
            <div class="col">
                <nav class="navbar navbar-expand-lg navbar-light bg-light">
                    <div class="container-fluid">
                        <a class="navbar-brand" href="#">Navbar</a>
                        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
                            <span class="navbar-toggler-icon"></span>
                        </button>
                        <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
                            <div class="navbar-nav">
                                <a class="nav-link active" aria-current="page" href="#">Home</a>
                                <a class="nav-link" href="#">Features</a>
                                <a class="nav-link" href="#">Pricing</a>
                                <a class="nav-link disabled">Disabled</a>
                            </div>
                        </div>
                    </div>
                </nav>
            </div>
        </div>
        <!-- header end -->
        <!-- 기존의 <h1>Header</h1>끝 -->
        <div class="row content">
            <div class="col">
                <div class="card">
                    <div class="card-header">
                        Featured
                    </div>
                    <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="text" name="writer" 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>
                        <script>
                            const serverValidResult = {}
                            <c:forEach items="${errors}" var="error">
                            serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
                            </c:forEach>
                            console.log(serverValidResult)
                        </script>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="row content">
        <h1>Content</h1>
    </div>
    <div class="row footer">
        <!--<h1>Footer</h1>-->
        <div class="row   fixed-bottom" style="z-index: -100">
            <footer class="py-1 my-1 ">
                <p class="text-center text-muted">Footer</p>
            </footer>
        </div>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
  • web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/servlet-context.xml</param-value>
        </init-param>
        <init-param>
            <param-name>throwExceptionIfNoHandlerFound</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <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>

hibernate-validator
@NotNull: 필드나 메서드 매개변수가 null이 아니어야 함을 나타냅니다.
@Null: Null만 입력 가능
@NotEmpty: 문자열, 배열, 컬렉션 등이 비어 있지 않아야 함을 나타냅니다.
@NotBlank: 문자열 필드가 비어 있지 않아야 함을 나타냅니다.
@Pattern(regexp): 정규식 패턴을 사용하여 문자열의 형식을 검사합니다.
@Size(min, max): 문자열, 배열, 컬렉션의 길이 또는 크기에 대한 제약을 설정합니다.
@Min(value): 숫자 형식의 필드가 지정된 최소 값 이상이어야 함을 나타냅니다.
@Max(value): 숫자 형식의 필드가 지정된 최대 값 이하여야 함을 나타냅니다.
@Past: 날짜와 시간 필드가 현재 날짜 이전인지 확인합니다.
@Future: 날짜와 시간 필드가 이후인지 확인합니다.
@Positive: 양수만 가능
@PositiveOrZero: 양수와 0만 가능
@Negative: 음수만 가능
@NegativeOrZero: 음수와 0만 가능
@Email: 이메일 주소의 유효성을 검사합니다.
@AssertTrue, @AssertFalse: 논리 값 (true 또는 false)를 검사합니다.
@CreditCardNumber: 신용 카드 번호의 유효성을 검사합니다.
@Length(min, max): 문자열의 길이 제한을 설정합니다.
@Range(min, max): 숫자 필드가 지정된 범위 내에 있어야 함을 나타냅니다.

valid를 이용한 검증

  • 유효성 검사를 자바스크립트를 했었지만 모바일과 같은 다양한 환경에서 서버를 이용하는 현재에는 브라우저를 사용하는 프론트쪽에서의 검증과 더불어 백서버에서도 입력되는 값들을 검증하는 것이 일반적이다.
  • 이러한 검증 작업은 컨트롤러에서 진행하는데 스프링 MVC의 경우 @Valid와 BindingResult라는 것을 이용해서 간단하게 처리할 수 있다.
  • @Valid의 애너테이션을 이용해서 검증하고, BindingResult는 스프링 프레임워크에서 유효성 검사 후에 발생하는 오류 및 검증 결과를 저장
  1. @Valid annotion을 사용한 모델 객체 검증
  2. BindingResult에 검증에 대한 결과를 저장
  3. RedirectAttribute 리다이렉트 한 후 데이터 전달
    순서를 지켜 사용하면 스프링 mvc는 유효성 검사를 수행하고 유효성 검사 결과를BindingResult 에 저장한 후 리다이렉트 한 후에 데이터를 RedirectAttributes를 통해 안전하게 전달할 수 있다

checkbox를 Formatter

  • 화면에서 체크박스를 이용해서 완료여부(finished)를 처리.
    문제는 브라우저가 체크박스가 클릭된 상태 전송되는값이 'on'이라는 값을 전달한다.
    TodoDTO로 데이터를 수집할때 문자열 'on'을 boolean타입으로 처리할 수 있어야 한다.
  • 컨트롤러에서 데이터를 수집할 때 타입을 변경해줘야 한다.

페이징 처리

  • 가져오는수
    select * from tbl_todo order by tno desc limit 10;
  • 첫번째 10 건너뛰는 데이터수(skip), 두번째 10가져오는 데이터수 (fetch)
    2페이지 == select from tbl_todo order by tno desc limit 10, 10;
    5페이지 == select
    from tbl_todo order by tno desc limit 40, 10;
  • 전체 데이터 개수
    select count(tno) from tbl_todo;

페이징 처리를 위한 DTO(PageRequestDTO)

  • 현재 페이지의 번호(page)
  • 한페이지당 보여주는 데이터의 수(size)
package com.springex.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
    @Builder.Default    // 필드에 기본값 설정
    @Min(value = 1)     // 최솟값
    @Positive           // 양수여야 한다.
    private int page = 1;// 현재 페이지 번호
    @Builder.Default    // 필드에 기본값을 설정
    @Min(value = 10)    // 최솟값 10
    @Max(value = 100)   // 최댓값 100
    @Positive
    private int size = 10;  //한페이지당 보여주는 데이터의 수
    public int getSkip() {
        // 일반적으로 데이터베이스나 컬렉션에서 데이터를 가져올때
        // 0부터 시작하는 인덱스를 사용하는 경우가 많아서
        // 페이지를 가져오려면 인덱스 0번부터 시작해야 한다.
        // ex)페이지번호 1이면 (1-1)*10 은 0 -> 첫번째 페이지의 데이터를 가져올때 사용
        // 페이지번호가 2이면 (2-1)*10 -> 10부터는 두번째 페이지의 데이터를 가져올때 사용
        return (page -1) * 10;
    }
}

TodoMapper의 count 처리

  • 화면에 페이지 번호들을 구성하기 위해서는 전체 데이터의 수를 알아야만 가능하다. 예를 들어 마지막 페이지가 7에서 끝나야 하는 상황이 생긴다면 화면상에서도 페이지 번호를 조절해야 하기 때문이다.

PageResponseDTO

  • TodoDTO의 목록
  • 전체 데이터 수
  • 페이지 번호의 처리를 위한 데이터들(시작 페이지 번호/ 끝 페이지 번호)
  • 화면상에서 페이지 번호들을 출력하려면 현재 페이지번호(page)와 페이지당 데이터 수(size)를 이용해서 계산해야 한다.
  • PageResponseDTO는 생성자를 통해서 필요한 page나 size등을 전달받도록 구성한다

페이지 번호의 계산

  • 페이지 번호를 계산하려면 현재 페이지의 번호(page)가 필요
  • 화면에 10개의 페이지 번호를 출력한다고 했을 때 경우의 수들이 발생한다
    1) page가 1인 경우: 시작페이지(start)는 1, 마지막페이지(end)는 10
    1) page가 10인 경우: 시작페이지(start)는 1, 마지막페이지(end)는 10
    1) page가 11인 경우: 시작페이지(start)는 11, 마지막페이지(end)는 20
  • 마지막 페이지/시작페이지 번호의 계산
    흔히들 처음에 구해야 하는 것이 start라고 생각하지만, 마지막 패아자(end)를 구하는 계산이 더 편할 수 있다.
  • end는 현재 페이지 번호를 기준으로 계산한다.
  • 마지막 페이지를 먼저 계산한 이유가 시작페이지(start)의 계산을 쉽게 하기 위함. 시작페이지(start)의 경우 계산한 마지막 페이지에서 9를 빼면 된다.
    this.start = this.end -9
    10-9=1
    20-9=11
  • 시작페이지의 구성은 끝났지만 마지막 페이지의 경우 다시 전체 개수(total)를 고려해야 한다. 만일 10개씩(size) 보여주는 경우 전체 개수(total)가 75라면 마지막 페이지는 10이 아닌 8이 되어야 한다.
int last = (int)(Math.ceil((total/(double)size)));
123 / 10.0 => 12.3 => 13
100 / 10.0 => 10.0 => 10
75 / 10.0 => 7.5 => 8
  • 마지막 페이지(end)는 앞에서 구한 last 값보다 작은 경우에는 last값이 end가 되어야한다
end 7
last9
7 > 8
// last가 최종이어야한다
int last = (int)(Math.ceil((total/(double)size)));
this.end = end > last ? last: end;
  • 이전(prev)/다음(next) 계산
    이전페이지의 존재 여부는 시작 페이지(start)가 1이 아니라면 무조건 true가 되어야 한다
    (1페이지부터 시작이고 아니라면 2,3,4,, 이기 때문에 페이지가 존재하고 있다)
  • 다음(next)은 마지막 페이지(end)와 페이지당 개수(size)를 곱한 값보다 전체 개수(total)가 더 많으면 다음(next)가 더 있다고 판단해야 한다.
this.prev = this.start >1;
this.next = total > this.end * this.size;

조회페이지로의 이동

  • 목록 페이지는 특정 Todo의 제목(title)을 누르면 조회 페이지로 이동됨
  • 기존에는 단순히 tno만을 전달해서 '/todo/read?tno=22'과 같은 방식으로 이동했지만 페이지 번호가 붙을 때는 page와 size 같이 전달이 되어야만 조회 페이지에서 다시 목록 이동할대 기존 페이지를 볼 수 있게 된다

수정/삭제 처리 후 페이지 이동

  • 수정/삭제 작업은 POST방식으로 처리되고 삭제 처리가 된 후에는 다시 목록으로 이동해야 한다.
    그렇기 때문에 수정 화면에서[form] 태그로 데이터를 전송할때 관련 정보를 같이 추가해서 전달해야만 한다.
 <input type="hidden" name="page" value="${pageRequestDTO.page}">
 <input type="hidden" name="size" value="${pageRequestDTO.size}">

post 방식으로 이루어지는 삭체 처리도 PageRequestDTO를 이용해서 [form] 태그로 전송되는 태그들을 수집하고 수정 후에 목록페이지로 이동할 때 page는 무조건 1페이지로 이동하도록 만들 수 있다

  • 수정 처리 후 이동
    Todo를 수정한 후에 목록으로 이동할때 페이지 정보를 이용해야
    TodoController의 Modify()에서 PageRequestDTO를 받도록 구현한다
profile
하이도의 BackEnd 입문

0개의 댓글