멀티캠퍼스 백엔드 과정 46일차[8월 9일] - controller, service, mapper 게시글 추가 목록보기

GoldenDusk·2023년 8월 9일
0
post-thumbnail

스프링 Web MVC 구현 Project 개념

📌 3-tier(티어)

Persistence tier(화면 계층)

  • 화면에 보여주는 기술
  • Servlet/JSP, 스프링 MVC가 담당하는 영역
  • 앱으로 제작하거나, CS(Client-Server)로 구성

Business Tier(비즈니스 계층)

  • 순수한 비즈니스 로직을 담고 있는 영역
  • 고객이 원하는 요구 사항을 반영하는 계층
  • 고객의 요구사항과 일치
  • xxxService와 같은 이름 구성, 메서드 이름 역시 고객들이 사용하는 용어 그대로 사용

Persistence Tier(영속 계층, 데이터 계층)

  • 데이터를 어떤 방식으로 보관하고, 사용하는 가에 대한 설계가 들어가는 계층
  • 네트워크 호출, 원격 호출 등의 기술 접목 ⇒ MyBatis, mybatis-spring을 이용해서 구성

스프링 MVC

✋ POJO란(Plain-Old-Java-Object)

💫 POJO

POJO는 Java 언어 사양에 의해 강제된 것 이외의 제한에 구속되지 않는 Java 개체입니다.

특별한 제한이 없는 평범한 물건입니다. POJO 파일에는 특별한 클래스 경로가 필요하지 않습니다. Java 프로그램의 가독성과 재사용성을 높입니다.

POJO는 유지 관리가 쉽기 때문에 현재 널리 사용됩니다. 그들은 읽고 쓰기 쉽습니다. POJO 클래스에는 속성 및 메서드에 대한 명명 규칙이 없습니다. Java 프레임워크 에 연결되어 있지 않습니다 . 모든 Java 프로그램에서 사용할 수 있습니다.

POJO 클래스는 가독성과 재사용성을 높이기 위해 객체를 정의하는 데 사용되기 때문에 Bean과 유사합니다. 그들 사이의 유일한 차이점은 Bean 파일에는 약간의 제한이 있지만 POJO 파일에는 특별한 제한이 없다는 것입니다.

POJO의 속성

  1. 모든 속성은 공용 setter 및 getter 메서드여야 합니다.
  2. 모든 인스턴스 변수는 비공개여야 합니다.
  3. 미리 지정된 클래스를 확장하면 안 됩니다.
  4. 미리 지정된 인터페이스를 구현하면 안 됩니다.
  5. 미리 지정된 주석을 포함하면 안 됩니다.
  6. 인수 생성자가 없을 수 있습니다.

출처 : https://stackoverflow.com/questions/3527264/how-to-create-a-pojo
출처 : https://www.javatpoint.com/pojo-in-java

📌 프로젝트 설명

  • 스프링 + MyBatis + 스프링 Web-mvc (3Tier 구조) ⇒ CRUD 기능
  • 목록은 페이징 처리와 검색 기능 구현

📌 각 영역의 명명 규칙

  • 3-tier로 구성하는 가장 일반적인 설명은 유지보수에 대한필요성
  • 각 영역은 독립적으로 설계되어 나중에 특정한 기술이 변하더라도 필요한 부분을 전자제품처럼 쉽게 교환하자 방ㅅ긱
  • 설계
    • 영역을 구분
    • 해당 연결 부위는 인터페이스를 이용해서 설계

네이밍 규칙

  • XXXController : 스프링 MVC에서 동작하는 Controller 클래스 설계 시 사용
  • XXXSerivce, XXXServiceImpl : 비즈니스 영역을 담당하는 인터페이스xxxService라는 방식을 사용, 인터페이스를 구현한 클래스는 xxxServiceImpl이라는 이름을 사용
  • XXXDAO, XXXRepository : DAO(Data-Access-Object)나 Repository(저장소)라는이름으로 영역을 구성하는 것이 보편적, 별도의 DAO 구성하는 대신 MyBatis의 Mapper의 인터페이스 활용
  • VO, DTO
    • VO와 DTO는 일반적으로 유사한 의미의 용어로 데이터를 담고 있는 객체를 의미한다는 공통점
    • VO : 주로 read only 목적이 당하고 데이터 자체도 불변하게 설계하는 것이 정석
    • DTO : 주로 데이터 수집의 용도가 강함 ⇒ 웹화면에서 로그인하는 정보

📌 패키지 Naming Convention

  • config : 프로젝트와 관련된 설정 클래스들의 보관 패키지
  • controller : 스프링의 MVC의 Controller들의 보관 패키지
  • service : 스프링의 Service 인터페이스와 구현 클래스 패키지
  • domain : VO, DTO 클래스의 패키지
  • persistence : MyBatis Mapper 인터페이스 패키지
  • exception : 웹 관련 예외 처리 패키지
  • aop : 스프링의 AOP 관련 패키지
  • security : 스프링 Security 관련 패키지
  • util : 각종 유틸리티 클래스 관련 패키지

✋ AOP란?

💫 AOP(Aspect Oriented Programming)는 이름에서 알 수 있듯이 프로그래밍에서 aspect를 사용합니다. 코드를 모듈화 라고도 하는 다른 모듈로 나누는 것으로 정의할 수 있습니다 . 여기서 측면은 모듈화의 핵심 단위입니다. 

Aspect는 기능에 대한 코드 코어를 어지럽히지 않고 비즈니스 로직의 중심이 아닌 트랜잭션, 로깅과 같은 횡단 관심사의 구현을 가능하게 합니다. 기존 코드에 조언인 추가 동작을 추가하여 이를 수행합니다.

Spring과 Aspect-Oriented Programming이 작동하는 방식

메서드를 호출하면 교차 절단 문제가 자동으로 구현될 것이라고 생각할 수 있지만 그렇지 않습니다. 메서드를 호출하는 것만으로는 어드바이스(해야 할 작업)가 호출되지 않습니다. 
Spring은 프록시 기반 메커니즘 사용합니다 . 즉, 원래 객체를 감싸고 메소드 호출과 관련된 어드바이스를 취할 프록시 객체를 생성합니다. 프록시 객체는 프록시 팩토리 빈을 통해 수동으로 생성하거나 XML 파일의 자동 프록시 구성을 통해 생성할 수 있으며 실행이 완료되면 소멸됩니다. 프록시 개체는 실제 개체의 원본 동작을 강화하는 데 사용
출처 : https://www.geeksforgeeks.org/aspect-oriented-programming-and-aop-in-spring-framework/


프로젝트 예제

  • 프로젝트 하기 전에 사용자 요구사항을 분석하고 목업 툴(파워포인트, *balsamiq mockups*)

📌 테이블 만들기

  1. 테이블 만들기
  • 잘못해서 tb1로 했으나 테이블 생성 시 tbl_이나 t_와같이 구분 가능한 것을 넣어주는 게 좋다.

  • pk 지정시에도 pk_board라는 식으로 이름 지정하는 게 좋다

    pencil mockup

CREATE table tb1_todo(
    tno int auto_increment primary key,
    title varchar(100) not null,
    dueDate date not null,
    writer varchar(50) not null,
    finished tinyint default 0
)
  1. 어제 작업 불필요한 것들 삭제

📌 DTO, VO : 스프링 빈 작업

✋ Mapper 클래스의 정확한 쓰임

💫 mapper 클래스는 데이터베이스 테이블과 그에 상응하는 객체 간의 변환, DTO(Data Transfer Object)와 도메인 객체 간의 변환, API 요청과 응답 객체 간의 변환 등을 다루는데 사용된다.

이를테면, 데이터베이스로부터 읽어온 데이터를 객체로 변환하거나, 반대로 객체를 데이터베이스에 저장 가능한 형태로 변환하는 작업을 수행

✋ ModelMapper 라이브러리란?

💫 java 객체 간의 데이터 매핑을 간단하게 처리하기 위한 라이브러리

  • 데이터 매핑은 한 객체의 데이터를 다른 객체로 복사하거나 이동하는 작업을 의미

  • 예를 들어, 데이터베이스 엔티티와 DTO(Data Transfer Object) 간의 변환, 또는 서로 다른 두 객체 간의 필드 복사 등이 데이터 매핑에 해당

  • ModelMapper는 이러한 데이터 매핑 작업을 자동화하고 편리하게 처리할 수 있도록 도움 개발자는 별도의 복잡한 매핑 로직을 작성하지 않아도 되며, 설정을 통해 어떤 필드를 어떤 필드에 매핑할지, 어떤 변환을 수행할지 등을 지정 가능

  • 데이터베이스의 엔티티 객체를 웹 애플리케이션의 화면에 보여줄 DTO로 변환

  • 웹 애플리케이션에서 사용자 입력 데이터를 데이터베이스 엔티티로 변환하여 저장

  • REST API에서 입력 받은 JSON 데이터를 도메인 객체로 변환하여 비즈니스 로직 처리

ModelMapper는 반복적이고 번거로운 매핑 작업을 간단하게 처리할 수 있도록 해주며, 코드의 가독성과 유지보수성을 높여준다.

  1. ModelMapperConfig.java
  • DTO(데이터를 담아서 전송 객체), VO(데이터만을 담은 객체)
  • 프로젝트 개발 시 DTO를 VO 변환, VO를 DTO 변환해야 하는 작업이 빈번하게 발생
  • ModelMapper를 스프링의 빈으로 등록해서 작업
  • ModelMapper를 스프링 애플리케이션 컨텍스트에 빈으로 등록하는 방법의 코드
  • ModelMapper를 스프링의 빈으로 등록하는 이유는 설정의 중앙화, 인스턴스 관리, 테스트 용이성, 일관성 유지 등 다양한 이점을 활용하기 위해서이다.

package com.multicampus.springex.config;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.modelmapper.spi.MatchingStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration //ModelMapperConfig가 스프링의 설정 빈으로 등록함을 명시
public class ModelMapperConfig {
    @Bean //bean 등록
    public ModelMapper getMapper(){
        //VO를 받아오는 코드
        // ModelMapper 라이브러리를 이용해서 객체 간의 매핑을 수행하는 설정 변경
        // ModelMapper는 객체 간의 속성값을 자동으로 매핑해주는 라이브러리로 객체 간의 변환 작업 편리
        ModelMapper modelMapper = new ModelMapper();
        // setFieldMatchingEnabled(true) : 필드 기반 설정, 객체의 필드들 간의 이름이 같은 경우 자동 매칭
        // setFieldAccessLevel : 매핑할 필드의 접근 레벨 지정하며, PRIVATE 레벨로 설정되어 있는 경우 private 접근 제어자로
        // 선언된 필드도 매핑 대상에 포함
        // setMatchingStrategy은 매칭 전략을 지정하면 STRICT 전략시, 매핑되지 않는 속성이 있는 경우 예외 발생하며 매핑 작업 실패
        modelMapper.getConfiguration().setFieldMatchingEnabled(true)
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE) //setFieldAccessLevel : 레벨을 정함
                .setMatchingStrategy(MatchingStrategies.STRICT);
        return modelMapper;
    }
}

📌 화면 처리 : 부트스트랩 - css 지원

  1. 화면처리
  • 사용자 화면 디자인
  • 컴파일된 css 및 js
  • 소스 파일
  • 예제 다운로드 받기
  • 부트스트랩 사이트

Get started with Bootstrap

  • 확인해보기

  1. test.html 작성
  • 부트스트랩 틀을 가져온 html - 간단하게

<!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="https://www.daum.net">Daum</a>
                                <a class="nav-link" href="https://www.naver.com">Naver</a>
                                <a class="nav-link" href="#">Pricing</a>
                                <a class="nav-link disabled">Disabled</a>
                            </div>
                        </div>
                    </div>
                </nav>
            </div>
        </div>
    </div>
    <div class="row content">
        <h1>Content</h1>
        <div class="row content">
            <div class="col">
                <div class="card">
                    <div class="card-header">
                        Featured
                    </div>
                    <div class="card-body">
                        <h5 class="card-title">Todo List</h5>
                        <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
                        <a href="https://www.naver.com" class="btn btn-primary">네이버 이동</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="row footer">
        <div class="row   fixed-bottom" style="z-index: -100">
            <footer class="py-1 my-1 ">
                <p class="text-center text-muted">geumjuLee</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>

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

  • domain : 순수 영속성 처리 패키지

MyBatis 이용하는 개발 단계

  1. VO 선언 : domain > TodoVO
package com.multicampus.springex.dto;

import lombok.*;

import java.time.LocalDate;

@ToString
@Data //setter, getter을 알아서 처리 해줌
@Builder
// 모든 속성에 대한 아규먼트 처리
@AllArgsConstructor //public Todo(Long tno, String title){}을 만들어줌
@NoArgsConstructor //Default 생성자를 만들어줌
public class TodoDTO {

    private Long tno;

    private String title;

    private LocalDate dueDate;

    private boolean finished;

    private String writer;
}
  1. Mapper 인터페이스 개발 -TodoMapper.java
  • TodoMapper 인터페이스 구현
  • TodoMapper.getTime() : 실제 동작하는 객체
package com.multicampus.springex.mapper;

public interface TodoMapper {
    String getTime();
}
  1. XML 개발(CRUD) : TodoMapper.xml ⇒ db와 연동
  • 꼭 인터페이스와 이름 같게 해야지 찾을 수 있다.

**<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.multicampus.springex.mapper.TodoMapper">

    <!--어노테이션이 아니라 xml 파일로 sql 작성-->
    <!--TodoMapperjava에 있는 메소드 명이랑 select id가 같아야 함-->
    <select id="getTime" resultType="string">
        select now()
    </select>

</mapper>**
  1. 테스트 코드 개발 - TodoMapperTests
  • mybatis가 동작 하는지 확인 완료
  • @ContextConfiguration을 통해 테스트 클래스가 필요한 스프링 빈 및 설정 정보를 로드하여 테스트 환경을 설정하고, 이를 통해 스프링의 다양한 기능을 테스트할 수 있다.

package com.multicampus.springex.mapper;

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;

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
    @Autowired(required = false)
    private TodoMapper todoMapper;

    // mybatis 잘 동작하는지 getTime 수행여부를 알 수 있다.
    @Test
    public void testGetTime(){
        log.info(todoMapper.getTime());
    }

}
  1. log4j.xml 로그 레벨 설정
<!--로그레벨 TRACE 좀 더 상세하게 보겠다라는 의미-->
        <logger name="com.multicampus.springex.mapper" level="TRACE" additivity="false">
            <appender-ref ref="console"/>
        </logger>

🍀 ToDo 기능 개발

  1. 등록작업 순서
    TodoMapper ⇒ TodoService ⇒ TodoController ⇒ JSP

📌 ToDoMapper 개발/테스트

  1. TodoMapper.java 만들기 : insert 정의
package com.multicampus.springex.mapper;

import com.multicampus.springex.domain.TodoVO;

public interface TodoMapper {
    String getTime();

    /*TodoVO를 파라미터로 입력받는 insert() 정의*/
    void insert(TodoVO todoVO); //일은 TodoMapper.xml이 함
}
  1. TodoMapper.xml
  • 꼭 이름 똑같이 하기
  • id 부분도 맞춰주기
  • 💥 주의 나는 강사님과 달리 테이블 tbl_todo를 tb1_todo로 함
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.multicampus.springex.mapper.TodoMapper">

    <!--어노테이션이 아니라 xml 파일로 sql 작성-->
    <!--TodoMapperjava에 있는 메소드 명이랑 select id가 같아야 함-->
    <select id="getTime" resultType="string">
        select now()
    </select>
    <!--TodoMapper.java에 있는 메소드 명이랑 inesert id가 같아야 함-->
    <insert id="insert">
        /*prepareStatment에서 쓴 ?대신 인파라미터를 #으로 사용*/
        insert into tb1_todo (title, dueDate, writer) values (#{title}, #{dueDate}, #{writer})
    </insert>
</mapper>
  1. 테스트 하기 ToMapperTest
package com.multicampus.springex.mapper;

import com.multicampus.springex.domain.TodoVO;
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 java.time.LocalDate;

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
    @Autowired(required = false)
    private TodoMapper todoMapper;

    // mybatis 잘 동작하는지 getTime 수행여부를 알 수 있다.
    @Test
    public void testGetTime(){
        log.info(todoMapper.getTime());
    }

    @Test
    public void testInsert(){
        TodoVO todoVO = TodoVO.builder()
                .title("스프링 TodoTest")
                .dueDate(LocalDate.of(2023, 8, 9))
                .writer("user1").build();
        todoMapper.insert(todoVO);
    }

}

📌 ToDoMapper와 TodoController 사이에 서비스계층을 설계하여 적용

비즈니스 계층 예시

service

  • service/TodoService 인터페이스 void register(TodoDTO todoDTO)

  1. TodoService.java
package com.multicampus.springex.service;

import com.multicampus.springex.dto.TodoDTO;

public interface TodoService {
		// TodoDTO 객체를 매개변수로 받아들여 Todo 아이템을 등록하는 역할을 수행하는 것
    void register(TodoDTO todoDTO);
}
  1. TodoServiceImpl.java : 인터페이스 실제 구현 객체
  • @Service : 계층 구조상 주로 비즈니스 영역을 담당하는 객체임을 표시하기 위해 사용
  • 할일을 등록하는 서비스를 구현하는 클래스
  • @RequiredArgsConstructor: Lombok 어노테이션으로, 해당 클래스의 final 필드에 대한 생성자를 생성 생성자 주입을 통해 의존성을 주입받을 수 있다.

package com.multicampus.springex.service;

import com.multicampus.springex.domain.TodoVO;
import com.multicampus.springex.dto.TodoDTO;
import com.multicampus.springex.mapper.TodoMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;

@Service //서비스 빈으로 등록
@Log4j2
@RequiredArgsConstructor //싱글톤으로 지정한 빈을 생성자 주입에 의해 injection
public class TodoServiceImpl implements TodoService{
    //의존성 주입이 필요한 객체타입을 final로 고정하고 생성자를 생성하여 주입
    private final TodoMapper todoMapper;
    private final ModelMapper modelMapper;

    @Override
    public void register(TodoDTO todoDTO) {
        log.info(modelMapper);
        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
        log.info(todoVO);
        //TodoMapper가 받음 => TodoMapper.xml이 들어가서 동작
        todoMapper.insert(todoVO);
    }
}
  1. root-context.xml 수정 ⇒ config, service로 상세 나누기
<!--timeMapper와 연결해줘, 빈에 등록할거야의 의미-->
    <context:component-scan base-package="com.multicampus.springex.config"/>
    <context:component-scan base-package="com.multicampus.springex.service"/>
  1. TodoServiceTest.java : 테스트 and todo 서비스 연결 및 값 넣기
package com.multicampus.springex.service;

import com.multicampus.springex.dto.TodoDTO;
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 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 = TodoDTO.builder()
                .title("Test Todo 1")
                .dueDate(LocalDate.now())
                .writer("user2")
                .build();
        // 값을 넣어줌
        todoService.register(todoDTO);
    }
}

📌 TodoController의 Get/Post 방식 처리

  1. register.jsp 작성

<!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>Todo Register</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="https://www.daum.net">Daum</a>
                                <a class="nav-link" href="https://www.naver.com">Naver</a>
                                <a class="nav-link" href="#">Pricing</a>
                                <a class="nav-link disabled">Disabled</a>
                            </div>
                        </div>
                    </div>
                </nav>
            </div>
        </div>
    </div>
    <div class="row content">
        <h1>Content</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-contro" placeholder="Title">
                           </div>

                               <div class="input-group mb-3">
                                   <span class="input-group-text">DueDate</span>
                                   <input type="date" name="dueDate" class="form-contro" placeholder="Duedate">
                               </div>

                               <div class="input-group mb-3">
                                       <span class="input-group-text">Writer</span>
                                       <input type="Writer" name="writer" class="form-contro" placeholder="Writer">
                               </div>

                           <div class="my-4">
                               <div class="float-end">
                                   <button type="submit" class="btn btn-primary">Submit</button>
                                   <button type="reset" class="btn btn-secondary">Reset</button>
                               </div>
                           </div>
                       </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="row footer">
        <div class="row   fixed-bottom" style="z-index: -100">
            <footer class="py-1 my-1 ">
                <p class="text-center text-muted">geumjuLee</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>
  1. TodoController.java 수정

    💫 이 컨트롤러는 클라이언트의 요청에 따라 각각의 요청을 처리하고, 적절한 로직을 수행한 후 뷰로 결과를 반환하거나 리다이렉트,이를 통해 사용자와 애플리케이션 간의 상호작용을 관리하고, 서비스 로직과 뷰 간의 중재 역할을 수행

  • 컨트롤러 - 확인
  • 여기까지는 데이터베이스로 안넘어가진다.
  • RedirectAttributes redirectAttributes 매개변수는 리다이렉트 시에 데이터를 전달할 수 있는 객체

public class TodoController {
    @RequestMapping("/list") //localhost:8090/todo/list
    public void list(Model model){
        log.info("todo_list");
    }

    //@RequestMapping(value="/register", method = RequestMethod.GET) //localhost:8090/todo/register
    @GetMapping("/register")
    /*제대로 불러오는지 확인하는 작업, 제대로 불러오면 콘솔에 뜬다. */
    public void registerGET(){
        log.info("GET todo_register.....");
    }

    // register은 post와 get방식 따로 따로, 객체를 알아서 넣어줌
    @PostMapping("/register")
    public String registerPost(TodoDTO todoDTO, RedirectAttributes redirectAttributes){
        log.info("Post todo register");
        log.info(todoDTO);

       return "redirect:/todo/list";
    }
}

📌 @Vaild 이용한 서버사이드 검증

  • 스프링 MVC에서 검증을 위해 hibernate-validate 라이브러리 이용해서 검증 6.2.1 Final
  1. TodoDTO.java 수정
  • 빈 문자열 불가, null 불가 : @NotEmpty 추가
  • 현재보다 미래인가? : @Future 추가
package com.multicampus.springex.dto;

import lombok.*;

import javax.validation.constraints.Future;
import javax.validation.constraints.NotEmpty;
import java.time.LocalDate;

@ToString
@Data //setter, getter을 알아서 처리 해줌
@Builder
// 모든 속성에 대한 아규먼트 처리
@AllArgsConstructor //public Todo(Long tno, String title){}을 만들어줌
@NoArgsConstructor //Default 생성자를 만들어줌
public class TodoDTO {

    private Long tno;
    @NotEmpty //Null, 빈 문자열 불가
    private String title;

    @Future //현재 보다 미래인가?
    private LocalDate dueDate;
    private boolean finished;

    @NotEmpty
    private String writer;
}
  1. TodoController.java
  • final로 지정해서 지정된 것만 주입하겠다.
  • @RequiredArgConstructor : final로 지정해서 지정된것만 넣겠다.
  • 에러 메세지 보여주기 위해 addFlashAttribute 추가
  • 서버사이드 검증을 위해 @Valid 바인딩을 위해 BindingResult bindingResult 추가

💫 BindingResult는 입력값과 관련된 유효성 검사의 결과와 에러 정보를 담는 컨테이너

만약 입력값이 유효하지 않을 경우, 해당 에러 정보가 BindingResult에 저장되어 컨트롤러에서 이를 확인하고 처리할 수 있게 되며 이를 통해 사용자로부터 들어오는 데이터의 검증과 관련된 처리를 간편하게 구현할 수 있다.

  • 데이터 베이스에 넘어온 값 추가해주기 위해 todoService.register(todoDTO);처리
  • 비어 있거나 미래가 아니라면 다시 register.jsp로 리턴
  • addFlashAttribute : 한 번만 일회성으로 사용자에게 에러를 보여줌
package com.multicampus.springex.controller;

import com.multicampus.springex.dto.TodoDTO;
import com.multicampus.springex.service.TodoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
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.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.validation.Valid;

@Controller //스프링 MVC에서 컨트롤러 역할, 스프링의 빈(Bean)으로 등록
@RequestMapping("/todo")
@Log4j2
@RequiredArgsConstructor //final로 지정해서 지정된 것만 주입하겠다.
public class TodoController {
    // injection을 안해서 데이터 베이스에 값이 넣어지지 않은 것
    private final TodoService todoService;

    @RequestMapping("/list") //localhost:8090/todo/list
    public void list(Model model){
        log.info("todo_list");
    }

    //@RequestMapping(value="/register", method = RequestMethod.GET) //localhost:8090/todo/register
    @GetMapping("/register")
    /*제대로 불러오는지 확인하는 작업, 제대로 불러오면 콘솔에 뜬다. */
    public void registerGET(){
        log.info("GET todo_register.....");
    }

    // register은 post와 get방식 따로 따로, 객체를 알아서 넣어줌
    @PostMapping("/register")
    //서버 사이드 검증 redirect에 바인딩
    public String registerPost(@Valid TodoDTO todoDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes){
        log.info("Post todo register");
        if(bindingResult.hasErrors()){
            log.info("has error");
            // addFlashAttribute : 한 번만 일회성으로 사용자에게 에러를 보여줌 a
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/todo/register";
        }

       return "redirect:/todo/list";
    }
}
  • 비어 있다면

  • 과거 날짜라면? ⇒ @Future 지정해서

  1. register.jsp
  • 에러 메세지를 사용자에게 보여주기 위해 프론트로 넘겨주는 자바 스크립트 작성
<%@ page contentType="text/html; UTF-8" pageEncoding="UTF-8"%>
<%@ 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>Todo Register</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="https://www.daum.net">Daum</a>
                                <a class="nav-link" href="https://www.naver.com">Naver</a>
                                <a class="nav-link" href="#">Pricing</a>
                                <a class="nav-link disabled">Disabled</a>
                            </div>
                        </div>
                    </div>
                </nav>
            </div>
        </div>
    </div>
    <div class="row content">
        <h1>Content</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-contro" placeholder="Title">
                           </div>

                               <div class="input-group mb-3">
                                   <span class="input-group-text">DueDate</span>
                                   <input type="date" name="dueDate" class="form-contro" placeholder="Duedate">
                               </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="reset" class="btn btn-secondary">Reset</button>
                               </div>
                           </div>
                       </form>
                        <script>
                            const serverVaildResult ={}
                            //TodoController의 에러
                            <c:forEach items="${errors}" var="error">
                            // 객체에다가 넣기
                            serverVaildResult['${error.getField()}'] ='${error.defaultMessage}'
                            </c:forEach>
                            console.log(serverVaildResult)
                        </script>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="row footer">
        <div class="row   fixed-bottom" style="z-index: -100">
            <footer class="py-1 my-1 ">
                <p class="text-center text-muted">geumjuLee</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>
  1. TodoController.java
  • 위에서 맨 밑에 추가

// 추가해주기 => 데이터베이스에 드디어 추가 가능
        log.info(todoDTO);
        todoService.register(todoDTO);
       return "redirect:/todo/list";

📌 Todo List 개발

  1. TodoMapper.java 수정
  • 모든 데이터베이스 테이블을 다 가져올 거임 ⇒ list로 보여주기 위해
package com.multicampus.springex.mapper;

import com.multicampus.springex.domain.TodoVO;

import java.util.List;

public interface TodoMapper {
    String getTime();

    /*TodoVO를 파라미터로 입력받는 insert() 정의*/
    void insert(TodoVO todoVO); //일은 TodoMapper.xml이 함

    // 가장 최근에 등록된 글 순서대로 tb1_todo 테이블의 모든 row들을 가져온다. todolist_selectall 작업
    List<TodoVO> selectAll();

}
  1. TodoService.java 수정
  • 인터페이스에 getAll 추가
package com.multicampus.springex.service;

import com.multicampus.springex.dto.TodoDTO;

import java.util.List;

public interface TodoService {
    void register(TodoDTO todoDTO);
    List<TodoDTO> getAll();
}
  1. TodoServiceImpl.java
  • 구현 객체 ⇒ override 필요
package com.multicampus.springex.service;

import com.multicampus.springex.domain.TodoVO;
import com.multicampus.springex.dto.TodoDTO;
import com.multicampus.springex.mapper.TodoMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service //서비스 빈으로 등록
@Log4j2
@RequiredArgsConstructor //싱글톤으로 지정한 빈을 생성자 주입에 의해 injection
public class TodoServiceImpl implements TodoService{
    //의존성 주입이 필요한 객체타입을 final로 고정하고 생성자를 생성하여 주입
    // 해당 클래스의 의존성이며, final로 선언되어 변경되지 않음을 보장
    private final TodoMapper todoMapper;
    private final ModelMapper modelMapper;

    // todo 항목등록하는 메서드
    @Override
    public void register(TodoDTO todoDTO) {
        log.info(modelMapper);
        // modelMapper을 사용하여 TodoDTO 객체를 TodoVO객체로 변환하고
        // 이를 TodoMapper를 통해 데이터베이스 등록
        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
        log.info(todoVO);
        //TodoMapper가 받음 => TodoMapper.xml이 들어가서 동작
        todoMapper.insert(todoVO);
    }

    // 구현 객체이기 때문에 TodoService가 바뀌면 바뀜
    // 모든 할 일 항목 조회
    @Override
    public List<TodoDTO> getAll() {

        // 데이터베이스에서 조회한 할일 항목을 VO에서 DTO로 변환하여 리스트로 반환하는 작업을 수행
        // todoMapper를 통해 데이터 베이스에서 모든 할일 조회 => stream 변환 => TodoDTO 변환
        // VO객체를 DTO 객체로 변환하여 매핑 => 리스트 수집후 생성
        List<TodoDTO> dtoList = todoMapper.selectAll().stream()
                .map(vo-> modelMapper.map(vo, TodoDTO.class))
                .collect(Collectors.toList());
        return dtoList;
    }
}
  1. TodoControll.java
  • TodoService에서 리턴한 List getAll();을 model에다가 담기
  • list 부분에 model로 담기
@RequestMapping("/list") //localhost:8090/todo/list
    public void list(Model model){
        log.info("todo_list");
        // TodoService에서 리턴한 List<TodoDTO> getAll();을 model에다가 담기
        model.addAttribute("dtoList", todoService.getAll());
        //model 'dtoList' 이름으로 목록 데이터가 담겨있다. => list.jsp가 처리해줘야 함
    }
  1. TodoMapper.xml
  • selectAll 추가해주기
  • 리턴 타입을 정해줌.. 왜? 모든 데이터를 가져올 것이기 때문에 resultset형태로 반환해주는데 그게 모든 데이터가 있으니 row 값들이 다 넘어오는데 한줄 단위로 묶어 담아야 하니 하나의 TodoVO로 넘어와서 한줄 단위로 묶어줌
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.multicampus.springex.mapper.TodoMapper">

    <!--어노테이션이 아니라 xml 파일로 sql 작성-->
    <!--TodoMapperjava에 있는 메소드 명이랑 select id가 같아야 함-->
    <select id="getTime" resultType="string">
        select now()
    </select>
    <!--TodoMapper.java에 있는 메소드 명이랑 inesert id가 같아야 함-->
    <insert id="insert">
        /*prepareStatment에서 쓴 ?대신 인파라미터를 #으로 사용*/
        insert into tb1_todo (title, dueDate, writer) values (#{title}, #{dueDate}, #{writer})
    </insert>

    <!--리턴 타입을 정해줌..? 모든 데이터를 가져올 것이기 때문에 resultset형태로 반환해주는데 그게 모든 데이터가
     있으니 row 값들이 다 넘어오는데 한줄 단위로 묶어 담아야 하니
    하나의 TodoVO로 넘어와서 한줄 단위로 묶어줌-->
    <select id="selectAll" resultType="com.multicampus.springex.domain.TodoVO">
        select * from tb1_todo order by tno desc
    </select>
</mapper>
  1. TodoMapperTests.java 테스트
  • selectAll이 잘 들어가는지
package com.multicampus.springex.mapper;

import com.multicampus.springex.domain.TodoVO;
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 java.time.LocalDate;
import java.util.List;

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
    @Autowired(required = false)
    private TodoMapper todoMapper;

    // mybatis 잘 동작하는지 getTime 수행여부를 알 수 있다.
    @Test
    public void testGetTime(){
        log.info(todoMapper.getTime());
    }

    @Test
    public void testInsert(){
        TodoVO todoVO = TodoVO.builder()
                .title("스프링 TodoTest")
                .dueDate(LocalDate.of(2023, 8, 9))
                .writer("user1").build();
        todoMapper.insert(todoVO);
    }

    @Test
    public void testSelectALL(){
        List<TodoVO> voList = todoMapper.selectAll();
        voList.forEach(vo -> log.info(vo));

//        for(TodoVO vo:voList){
//            log.info(vo);
//        }
    }

}


7. 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>
    <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="https://www.daum.net">Daum</a>
                                <a class="nav-link" href="https://www.naver.com">Naver</a>
                                <a class="nav-link" href="#">Pricing</a>
                                <a class="nav-link disabled">Disabled</a>
                            </div>
                        </div>
                    </div>
                </nav>
            </div>
        </div>
    </div>
    <div class="row content">
        <h1>Content</h1>
        <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>
                            <%--모델의 dtoList를 가져올 것이니--%>
                            <c:forEach items="${dtoList}" var="dto">
                                <tr>
                                    <th scope="row"><c:out value="${dto.tno}"/></th>
                                    <td>
                                        <a href="" class="text-decoration-none" data-tno="${dto.tno}" >
                                            <c:out value="${dto.title}"/>
                                        </a>
                                    </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>
    </div>
    <div class="row footer">
        <div class="row   fixed-bottom" style="z-index: -100">
            <footer class="py-1 my-1 ">
                <p class="text-center text-muted">geumjuLee</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>
  1. 패키지 캡처본

    기능별로 분리를 잘해두는 것이 중요

회고

오늘은 CS 스터디 방향성에 대해 팀원분들과 나눴다. 한 분은 아무래도 지금은 CS스터디 보다는 알고리즘 쪽이나 스프링부분을 좀 더 집중하고 싶다고 하셔서 오늘까지만 진행하기로 하셨다. 강사님과 나눈 피드백을 공유해주셔서 방향성과 계획을 다시 잡고 공부하기로 했다. CS를 주로 가되 다른 수업부분이나 알고리즘 모르는 부분있으면 카톡방에서 공유하기로 했다. 이렇게 조율해가며 완성해 나가는 것도 하나의 즐거움인 것 같다. 그 뿐 아니라 다른 얘기 목표나, 어느정도의 지식 수준인지 등에 대한 대화도 나눴다. 한 결 가까워진 느낌..?

그리고 나서 오늘 공부한 거 좀 더 주석이랑 코드 해석하면서 복습했다. 후 이제 GITHUB에 링크 연동해두고 자야지... 내일도 화이팅 하자

profile
내 지식을 기록하여, 다른 사람들과 공유하여 함께 발전하는 사람이 되고 싶다. 참고로 워드프레스는 아직 수정중

1개의 댓글

comment-user-thumbnail
2023년 8월 9일

잘 봤습니다. 좋은 글 감사합니다.

답글 달기