본문 바로가기

Server Programming/Spring Boot Backend Programming

2장. 웹과 데이터베이스

반응형

MariaDB 설치

-homebrew를 이용해 설치

 

터미널

brew install mariadb

 

git must be installed and in your path! 에러

xcode-select --install

 

더보기

https://velog.io/@skwx50000/mac-m1OS-Monterey-homebrew%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-mac%EC%97%90-mariadb%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%97%90%EB%9F%AC-%EC%88%98%EC%A0%95

 

[mac m1/OS Monterey] homebrew를 이용하여 mac에 mariadb설치 및 에러 수정

인턴과제로 스프링 api개발과 mariadb 데이터베이스 구축하기를 받았다.약 1주동안 api코드 분석 및 대략적인 다이어그램을 그린 후, 본격적으로 데이터베이스를 구축하기 위하여, 현재 내 macbook에

velog.io

 

https://velog.io/@delvering17/Mac-MariaDB-1.-MariaDB-%EC%84%A4%EC%B9%98%EC%99%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8

 

Mac [MariaDB] 1. MariaDB 설치와 로그인

(중요) 터미널 실행.brew 명령어로 mariadb설치brew install mariadbbrew로 서비스 확인 brew services list root 계정 생성하기sudo mariadb-secure-installation \- sudo 비밀번호를 입

velog.io

 

https://bestcoding.tistory.com/3

 

[Error] MySQL ERROR 1045 (28000)

사용자 환경 : macOS Sierra 10.12.6, 5.7.20 MySQL Community Server (GPL) MySQL에 접속을 시도하다 보면 다음과 같은 에러문을 쉽게 볼 수 있을 것이다. $> /usr/local/mysql/bin/mysql ERROR 1045 (28000): Access denied for user 'roo

bestcoding.tistory.com

 

https://wanna-b.tistory.com/50

 

Mac OS에서 인코딩 UTF-8 설정하기 (한글 깨질 때 해결방법)

Mysql 인코딩 UTF-8로 설정하기 오늘 Mysql-스프링 웹프로젝트 진행 중에 한글이 깨졌다.MySQLWorkbench 에서 데이터를 추가하면, 정상적으로 한글로 저장이 되는데, 연동한 웹에서 insert를 해주면 깨지는

wanna-b.tistory.com

 

 

1. 사용할 DB 만들기

2. 사용자 생성

3. 사용자 권한 부여

Create database webdb;
create user 'webuser'@'%' identified by 'root1234';
GRANT ALL PRIVILEGES ON webdb.* TO 'webuser'@'%';

 


mariadb JDBC gradle 의존성 추가

// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
implementation 'org.mariadb.jdbc:mariadb-java-client:3.1.0'

 


JDBC 프로그램 구조

-Java Database Connectivity

-자바와 db를 네트워크 상에서 연결해 데이터 교환하는 프로그램

-java.sql 패키지와 javax.sql 패키지 이용

-JDBC 드라이버가 자바와 DB 사이에서 네트워크 데이터 처리를 담당한다.

 

JDBC 프로그램 작성 순서

  1. 네트워크를 통해서 DB와 연결을 맺는다.
  2. DB에 보낼 SQL을 작성하고 전송
  3. DB가 응답한 결과를 받아서 처리
  4. DB와의 연결을 종료

JDBC 연결 테스트코드 작성

-@Test 어노테이션을 이용해 Assertions.assertEquals() 메서드 호출

package org.zerock.dao;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.sql.Connection;
import java.sql.DriverManager;

public class ConnectTests {

    @Test
    public void testConnection() throws Exception {

        Class.forName("org.mariadb.jdbc.Driver");
        // JDBC드라이버 클래스를 메모리 상으로 로딩하는 역할

        Connection connection = DriverManager.getConnection(
        //java.sql의 Connection 인터페이스 타입의 변수에
        //데이터베이스내에 있는 여러 정보들을 통해서 특정한 데이터베이스 연결 획득해서 대입
        
                "jdbc:mariadb://localhost:3306/webdb",
                //jdbc 프로토콜을 이용해 네트워크 연결정보 DB 정보 전달을 파라미터로 전달
                "USERNAME",
                "PASSWORD");

        Assertions.assertNotNull(connection);
        //데이터베이스와 정상적으로 연결이 된다면 Connection 타입의 객체는 null이 아니다

        connection.close();
        //데이터베이스와의 연결을 종료
    }

    @Test
    public void test1() {

        int v1 = 10;
        int v2 = 110;

        Assertions.assertEquals(v1,v2);

    }

}

Todo 애플리케이션

 

1. 테이블 생성

select now();
create table tbl_todo(
    tno int auto_increment primary key ,
    title varchar(100) not null ,
    dueDate date not null ,
    finished tinyint default 0
);

2. 데이터 insert

 

#DML문과 SELECT의 차이

-DML은 변경된 데이터의 수를 반환

-SELECT는 요청한 실제 데이터를 반환

 

-> 따라서 JDBC 프로그램 작성시에도 DML과 SELECT는 분리해서 작성해야한다.

 

3. TodoVO 클래스 생성

-읽기 전용이므로 @Getter 어노테이션 사용

 

package org.zerock.jdbcex.domain;

import lombok.*;

import java.time.LocalDate;

@Getter
@Builder
@ToString
public class TodoVO {

    private Long tno;

    private String title;

    private LocalDate dueDate;

    private boolean finished;

}

 

4. TodoDAO 클래스 생성해 getTime() 메서드 작성

-HikariDataSource을 쉽게 이용하기 위해 enum으로 구성한 ConnectionUtil 클래스 생성

-TodoService에서 호출해 ConnectionUtil 클래스를 이용해 연결을 얻어 DB에 직접 접근하는 클래스

 

package org.zerock.jdbcex.dao;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;

public enum ConnectionUtil {
	
    //외부에서 접속하기 위한 변수
    INSTANCE;

    private HikariDataSource ds;

    ConnectionUtil()  {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("org.mariadb.jdbc.Driver");
        config.setJdbcUrl("jdbc:mariadb://localhost:3306/webdb");
        config.setUsername("USERNAME");
        config.setPassword("PASSWORD");
        //
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

        ds = new HikariDataSource(config);
    }

    public Connection getConnection()throws Exception {
        return ds.getConnection();
    }

}

 

package org.zerock.jdbcex.dao;

import lombok.Cleanup;
import org.zerock.jdbcex.domain.TodoVO;

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

public class TodoDAO {


	//try-with-resources를 이용해 
    //try()내 AutoClosable 인터페이스를 구현한 타입의 변수들로 자동으로 close()
    public String getTime(){

        String now = null;

        try(Connection connection = ConnectionUtil.INSTANCE.getConnection();
            PreparedStatement preparedStatement = connection.prepareStatement("select now()");
            ResultSet resultSet = preparedStatement.executeQuery();
        ) {

            resultSet.next();

            now = resultSet.getString(1);
        }catch(Exception e){
            e.printStackTrace();
        }
        return now;
    }

}

 

-> 테스트 코드 작성

package org.zerock.dao;


import lombok.Cleanup;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.jdbcex.dao.ConnectionUtil;
import org.zerock.jdbcex.dao.TodoDAO;
import org.zerock.jdbcex.domain.TodoVO;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.time.LocalDate;
import java.util.List;

public class TodoDAOTests {

    private TodoDAO todoDAO;

	//모든 테스트 전에 DAO타입 객체를 생성
    @BeforeEach
    public void ready(){
        todoDAO = new TodoDAO();
    }

	//생성한 객체를 이용해 getTime()메서드 동작 확인
    @Test
    public void testTime() throws Exception{

        System.out.println(todoDAO.getTime() );

    }
}

 

Lombok의 @CleanUp 어노테이션을 이용한 getTime() 메소드 생성

-try-with-resource을 대체해, 중첩 try-catch을 이용하기 위한 어노테이션

-해당 메소드 종료시 close()를 자동으로 호출한다.

    public String getTime2() throws Exception {

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement("select now()");
        @Cleanup ResultSet resultSet = preparedStatement.executeQuery();

        resultSet.next();

        String now = resultSet.getString(1);

        return now;
    }

-> try-catch 대신에 메소드 선언부에 throws Exception을 붙인다.

 

 

5. TodoDAO에 insert() 메서드 작성

-TodoVO 객체를 DB에 추가하는 메서드

    public void insert(TodoVO vo) throws Exception {
        String sql = "insert into tbl_todo (title, dueDate, finished) values (?, ?, ?)";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

		//'?'에 setXXX를 이용해 전달할 데이터 지정
        preparedStatement.setString(1, vo.getTitle());
        //날짜의 경우 LocalDate -> java.sql.Date로 변환해 추가
        preparedStatement.setDate(2, Date.valueOf(vo.getDueDate()));
        preparedStatement.setBoolean(3, vo.isFinished());
		
        //TodoVO 객체 정보를 이용해 DML실행하기 위한 메서드
        preparedStatement.executeUpdate();

    }

 

-> 테스트 코드 작성

생성자와 달리 필요한 만큼만 데이터 세팅 가능한 빌드 패턴을 이용해 testinsert()메서드 작성

 

    @Test
    public void testInsert() throws Exception {
        TodoVO todoVO = TodoVO.builder()
                .title("Sample Title...")
                .dueDate(LocalDate.of(2021,12,31))
                .build();

        todoDAO.insert(todoVO);
    }

 

 

6. TodoDAO 목록 기능 작성

-테이블 각 행이 하나의 TodoVO 객체, 모든 객체를 담는 List<TodoVO>타입을 리턴 타입으로

public List<TodoVO> selectAll()throws Exception  {

        String sql = "select * from tbl_todo";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);
        @Cleanup ResultSet resultSet = preparedStatement.executeQuery();


        List<TodoVO> list = new ArrayList<>();

		//다음이 존재하지 않을 때까지 반복해서 리스트에 추가
        while(resultSet.next()) {
            TodoVO vo = TodoVO.builder()
                    .tno(resultSet.getLong("tno"))
                    .title(resultSet.getString("title"))
                    .dueDate( resultSet.getDate("dueDate").toLocalDate())
                    .finished(resultSet.getBoolean("finished"))
                    .build();

            list.add(vo);
        }

        return list;
    }

-> 테스트 코드 작성

 

    @Test
    public void testList() throws Exception {

        List<TodoVO> list = todoDAO.selectAll();

        list.forEach(vo -> System.out.println(vo));

    }

 

7. TodoDAO 조회 기능 작성

-특정한 번호의 데이터를 가져오는 기능으로, 특정 번호가 파라미터 / TodoVO가 리턴 타입

public TodoVO selectOne(Long tno)throws Exception {

        String sql = "select * from tbl_todo where tno = ?";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setLong(1, tno);

        @Cleanup ResultSet resultSet = preparedStatement.executeQuery();

        resultSet.next();
        TodoVO vo = TodoVO.builder()
                .tno(resultSet.getLong("tno"))
                .title(resultSet.getString("title"))
                .dueDate( resultSet.getDate("dueDate").toLocalDate())
                .finished(resultSet.getBoolean("finished"))
                .build();

        return vo;
    }

 

-> 테스트 코드 작성

    @Test
    public void testSelectOne() throws Exception {

        Long tno = 1L; //반드시 존재하는 번호를 이용

        TodoVO vo = todoDAO.selectOne(tno);

        System.out.println(vo);
    }

 

8. TodoDAO의 삭제/수정 기능 작성

-SELECT가 아닌 DML문으로 특정한 번호를 파라미터로 전달

    public void deleteOne(Long tno) throws Exception {

        String sql = "delete from tbl_todo where tno = ?";

        @Cleanup Connection    connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setLong(1, tno);

        preparedStatement.executeUpdate();
    }

 

    public void updateOne(TodoVO todoVO)throws Exception{

        String sql = "update tbl_todo set title =?, dueDate = ?, finished = ? where tno =?";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setString(1, todoVO.getTitle());
        preparedStatement.setDate(2, Date.valueOf(todoVO.getDueDate()));
        preparedStatement.setBoolean(3, todoVO.isFinished());
        preparedStatement.setLong(4, todoVO.getTno());

        preparedStatement.executeUpdate();
    }

-> 제목, 만료일, 완료여부를 업데이트하는데 TodoVO를 받아 executeUpdate()를 실행

 

 

-> 테스트 코드 작성

    @Test
    public void testUpdateOne() throws Exception {
        TodoVO todoVO = TodoVO.builder()
                .tno(1L)
                .title("Sample Title...")
                .dueDate(LocalDate.of(2021,12,31))
                .finished(true)
                .build();

        todoDAO.updateOne(todoVO);

    }

 


JDBC API

  1. java.sql.Connection
    • DB와 네트워크상 연결을 통해, SQL을 실행할 수 있는 객체 생성
    • 반드시 성립된 연결은 try~catch~finally / try-with-resources를 이용해 close()
  2. java.sql.Statement/PreparedStatement
    • SQL을 DB로 보내기 위한 타입 
      1. setXXX()
      2. executeUpdate()
      3. executeQuery()
  3. java.sql.ResultSet
    • PreparedStatement를 이용해 DML 처리하는 경우 -> DB에서 받환하는 데이터를 읽기위한 인터페이스
    • 데이터를 순차적으로 읽는 방식으로 구성되어 있으며 마찬가지로 close()을 이용한 자원회수 필요
    • getInt(), getString(), next() 등의 메서드를 이용

 

JDBC 용어

  • Connection Pool과 DataSource
    • 연결을 맺는 작업을 대신해 연결을 생성해 보관하는 Connection Pool
    • javax.sql.DataSource 인터페이스는 커넥션 풀을 지원하는 API
    • 커넥션 풀을 이용하는 라이브러리 DBCP / C3PO / HikariCP
  • DAO(Data Access Object)
    • DB의 접근과 처리를 전담하는 객체로 단위로 VO를 사용한다.
    • 캡슐화를 이용해 외부에서 어떤 로직으로 데이터를 처리하는지 숨긴다.
  • VO(Value Object) / Entity
    • DB의 단위인 엔티티를 객체로 다루기 위해 테이블 구조와 유사한 클래스를 만들어 처리하는 방식
    • VO/DTO 차이
      • VO : DB의 엔티티를 자바 객체로 표현하는데 데이터 자체를 의미하기 때문에 게터만 이용
      • DTO : 각 계층을 오고가는 택배 상자로 게터와 세터를 둘 다 이용해 데이터를 가공

프로젝트 내 JDBC 구현

 

Lombok 라이브러리

: 게터, 세터나 생성자 함수를 만들어주는 라이브러리

 

주요 어노테이션

  • @Getter, @Setter, @Data
  • @ToString()
  • @EqaulsAndHashCode
  • @AllArgsConstructor, @NoArgsConstructor
  • @Builder

 

Lombok 의존성 추가

// https://mvnrepository.com/artifact/org.projectlombok/lombok
compileOnly 'org.projectlombok:lombok:1.18.24'
annotaionProcessor 'org.projectlombok:lombok:1.18.24'

HikariCP

-HikariCP를 이용해 커넥션 생성

 

HikariCP 의존성 추가

// https://mvnrepository.com/artifact/com.zaxxer/HikariCP
implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.0.1'

 

커넥션 풀 이용한 Test클래스 생성

-HikariConfig 객체 생성해 HikaraiCP 이용

 

-HikariConfig : 커넥션 풀 설정 정보 객체로 HikariConfig객체를 이용해 HikariDataSource 객체 생성

-HikariDataSource : getConnection() 메서드를 이용해 Connection 객체 생성

 

 

    @Test
    public void testHikariCP() throws Exception {

        HikariConfig config = new HikariConfig();
        config.setDriverClassName("org.mariadb.jdbc.Driver");
        config.setJdbcUrl("jdbc:mariadb://localhost:3306/webdb");
        config.setUsername("webuser");
        config.setPassword("webuser");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

        HikariDataSource ds = new HikariDataSource(config);
        Connection connection = ds.getConnection();

        System.out.println(connection);

        connection.close();

    }

웹 MVC와 JDBC의 결합

-서비스 객체와 컨트롤러 객체 연동

 

ModelMapper 라이브러리

: DTO <-> VO 변환을 위한 라이브러리로 getter/setter를 이용해 객체 정보를 다른 객체로 복사한다.

 

1. TodoDTO에 롬복을 적용

 

적용 전, TodoDTO

package org.zerock.w1.todo.dto;

import java.time.LocalDate;

public class TodoDTO {

    private Long tno;

    private String title;

    private LocalDate dueDate;

    private boolean finished;

    public Long getTno() {
        return tno;
    }

    public void setTno(Long tno) {
        this.tno = tno;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public LocalDate getDueDate() {
        return dueDate;
    }

    public void setDueDate(LocalDate dueDate) {
        this.dueDate = dueDate;
    }

    public boolean isFinished() {
        return finished;
    }

    public void setFinished(boolean finished) {
        this.finished = finished;
    }

    @Override
    public String toString() {
        return "TodoDTO{" +
                "tno=" + tno +
                ", title='" + title + '\'' +
                ", dueDate=" + dueDate +
                ", finished=" + finished +
                '}';
    }
}

 

적용 후, TodoDTO

package org.zerock.jdbcex.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;


import java.time.LocalDate;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodoDTO {

    private Long tno;

    private String title;

    private LocalDate dueDate;

    private boolean finished;
}

 

->TodoVO와 구조는 같지만 다른 어노테이션

-TodoDTO : @Data 

-> getter/setter/toString/equals/hashCode 생성

 

-TodoVO : @Getter

-> getter만 생성

 

// https://mvnrepository.com/artifact/org.modelmapper/modelmapper
implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.1.1'

->DTO는 getter/setter, VO는 getter만 사용하기 위한 ModelMapper 설정 

 

(1) 대상 클래스의 생성자를 이용할 수 있도록 TodoVO에 생성자 관련 어노테이션 추가

package org.zerock.jdbcex.domain;

import lombok.*;

import java.time.LocalDate;

@Getter
@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class TodoVO {

    private Long tno;

    private String title;

    private LocalDate dueDate;

    private boolean finished;

}

-> @AllArgsConstructor : 모든 필드값이 필요한 생성자

-> @NoArgsContructor  : 파라미터가 없는 생성자

 

(2) util패키지를 추가해 ModelMapper 설정 변경하기 위한 MapperUtil을 enum으로 생성

-getConfiguraion()을 이용해 ModelMapper 설정 변경

-get()을 이용해 ModelMapper 사용

 

package org.zerock.jdbcex.util;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;

public enum MapperUtil {
    INSTANCE;

    private ModelMapper modelMapper;

    MapperUtil() {
        this.modelMapper = new ModelMapper();
        this.modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                .setMatchingStrategy(MatchingStrategies.LOOSE);

    }

    public ModelMapper get() {
        return modelMapper;
    }
}

 

2. 목록 기능 구현

 

(1) TodoListController 작성

-doGet() 메서드를 오버라이드해서 처리

package org.zerock.jdbcex.controller;

import lombok.extern.log4j.Log4j2;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.service.TodoService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "todoListController", value = "/todo/list")
@Log4j2
public class TodoListController extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;


    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("todo list..................");

    }
}

 

(2) TodoService 목록 기능 작성

    public List<TodoDTO> listAll()throws Exception {

        List<TodoVO> voList = dao.selectAll();

        log.info("voList.................");
        log.info(voList);

        List<TodoDTO> dtoList = voList.stream()
                .map(vo -> modelMapper.map(vo,TodoDTO.class))
                .collect(Collectors.toList());

        return dtoList;
    }

-> 서비스에서 dao 연결

 

(3) TodoListController과 TodoService 연동 후, JSP에 데이터 전달

  1. HttpServletRequest의 setAttribute()를 이용해 TodoService 객체가 반환하는 데이터 저장
  2. RequestDispatcher를 이용해 데이터를 JSP로 전달
package org.zerock.jdbcex.controller;

import lombok.extern.log4j.Log4j2;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.service.TodoService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "todoListController", value = "/todo/list")
@Log4j2
public class TodoListController extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;


    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("todo list..................");

        try {
            List<TodoDTO> dtoList = todoService.listAll();
            req.setAttribute("dtoList", dtoList);
            req.getRequestDispatcher("/WEB-INF/todo/list.jsp").forward(req,resp);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new ServletException("list error");
        }
    }
}

 

(4) list.jsp 작성

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html>
<head>
    <title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>

<ul>
    <c:forEach items="${dtoList}" var="dto">
        <li>
            <span><a href="/todo/read?tno=${dto.tno}">${dto.tno}</a></span>
            <span>${dto.title}</span>
            <span>${dto.dueDate}</span>
            <span>${dto.finished? "DONE": "NOT YET"}</span>
        </li>
    </c:forEach>
</ul>


</body>
</html>

 

3. 등록 기능 구현

-PRG 패턴을 이용해 GET 방식의 등록 화면에서, 입력 폼을 채워서 POST 방식 처리 후 목록화면으로 redirect

 

(1) TodoService에 register() 메소드 추가

    public void register(TodoDTO todoDTO)throws Exception{

        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);

        //System.out.println("todoVO: " + todoVO);
        log.info(todoVO);

        dao.insert(todoVO); //int 를 반환하므로 이를 이용해서 예외처리도 가능
    }

(2) TodoRegisterController에서 doGet(), doPost() 메서드 오버라이딩

package org.zerock.jdbcex.controller;

import lombok.extern.log4j.Log4j2;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.service.TodoService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@WebServlet(name = "todoRegisterController", value = "/todo/register")
@Log4j2
public class TodoRegisterController extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;
    private final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("/todo/register GET .......");
        req.getRequestDispatcher("/WEB-INF/todo/register.jsp").forward(req,resp);

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        TodoDTO todoDTO = TodoDTO.builder()
                .title(req.getParameter("title"))
                .dueDate(LocalDate.parse(req.getParameter("dueDate"),DATEFORMATTER ))
                .build();

        log.info("/todo/register POST...");
        log.info(todoDTO);
        try {
            todoService.register(todoDTO);
        } catch (Exception e) {
            e.printStackTrace();
        }
        resp.sendRedirect("/todo/list");

    }
}

(3) register.jsp 작성

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="/todo/register" method="post">
    <div>
        <input type="text" name="title" placeholder="INSERT TITLE">
    </div>
    <div>
        <input type="date" name="dueDate">
    </div>
    <div>
        <button type="reset">RESET</button>
        <button type="submit">REGISTER</button>
    </div>
</form>
</body>
</html>

 

등록 기능 순서

(1) register.jsp에서 POST 방식으로 title과 dueDate 전송

(2) TodoRegisterController에서 doPost()로 HttpServletRequest의 getParameter()로 TodoDTO 구성

(3) 구성한 TodoDTO를 TodoService의 register() 호출해 파라미터로 전달

(4) GET 방식으로 목록화면으로 리다이렉트

 

4. 조회 기능 구현

-GET 방식으로 동작하는데, tno라는 파라미터를 쿼리 스트링으로 번호를 TodoService에 전달하면 TodoDTO를 반환해 컨트롤러에서 HttpServletRequest에 담아서 JSP에 출력

-쿼리 스트링 : '?' + '&'

 

(1) TodoService에 get() 메소드 추가

    public TodoDTO get(Long tno)throws Exception {

        log.info("tno: " + tno);
        TodoVO todoVO = dao.selectOne(tno);
        TodoDTO todoDTO = modelMapper.map(todoVO, TodoDTO.class);
        return todoDTO;
    }

(2) TodoReadController 구현

package org.zerock.jdbcex.controller;

import lombok.extern.log4j.Log4j2;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.service.TodoService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "todoReadController", value = "/todo/read")
@Log4j2
public class TodoReadController extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        try {
            Long tno = Long.parseLong(req.getParameter("tno"));

            TodoDTO todoDTO = todoService.get(tno);

            //모델 담기
            req.setAttribute("dto", todoDTO);

            req.getRequestDispatcher("/WEB-INF/todo/read.jsp").forward(req, resp);

        }catch(Exception e){
            log.error(e.getMessage());
            throw new ServletException("read error");
        }
    }
}

(3) read.jsp 작성

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Todo Read</title>
</head>
<body>
    <div>
        <input type="text" name="tno" value="${dto.tno}" readonly>
    </div>
    <div>
        <input type="text" name="title" value="${dto.title}" readonly>
    </div>
    <div>
        <input type="date" name="dueDate" value="${dto.dueDate}">
    </div>
    <div>
        <input type="checkbox" name="finished" ${dto.finished ? "checked": ""} readonly >
    </div>
    <div>
        <a href="/todo/modify?tno=${dto.tno}">Modify/Remove</a>
        <a href="/todo/list">List</a>
    </div>
</body>
</html>

(4) list.jsp의 Todo에 링크처리

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html>
<head>
    <title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>

<ul>
    <c:forEach items="${dtoList}" var="dto">
        <li>
            <span><a href="/todo/read?tno=${dto.tno}">${dto.tno}</a></span>
            <span>${dto.title}</span>
            <span>${dto.dueDate}</span>
            <span>${dto.finished? "DONE": "NOT YET"}</span>
        </li>
    </c:forEach>
</ul>


</body>
</html>

 

5. 수정/삭제 기능 구현

(1) TodoService에 remove() / modify() 메소드 추가

    public void remove(Long tno)throws Exception {

        log.info("tno: " + tno);
        dao.deleteOne(tno);
    }

    public void modify(TodoDTO todoDTO)throws Exception {

        log.info("todoDTO: " + todoDTO );

        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);

        dao.updateOne(todoVO);

    }

(2) TodoModifyController에서 GET 방식으로 tno 파라미터를 이용해 내용 출력 후, POST 방식으로 수정 작업 처리

package org.zerock.jdbcex.controller;

import lombok.extern.log4j.Log4j2;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.service.TodoService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@WebServlet(name = "todoModifyController", value = "/todo/modify")
@Log4j2
public class TodoModifyController  extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;
    private final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            Long tno = Long.parseLong(req.getParameter("tno"));
            TodoDTO todoDTO = todoService.get(tno);
            //모델 담기
            req.setAttribute("dto", todoDTO);
            req.getRequestDispatcher("/WEB-INF/todo/modify.jsp").forward(req, resp);

        }catch(Exception e){
            log.error(e.getMessage());
            throw new ServletException("modify get... error");
        }
    }


    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String finishedStr = req.getParameter("finished");

        TodoDTO todoDTO = TodoDTO.builder()
                .tno(Long.parseLong(req.getParameter("tno")))
                .title(req.getParameter("title"))
                .dueDate(LocalDate.parse(req.getParameter("dueDate"),DATEFORMATTER ))
                .finished( finishedStr !=null && finishedStr.equals("on")  )
                .build();

        log.info("/todo/modify POST...");
        log.info(todoDTO);
        try {
            todoService.modify(todoDTO);
        } catch (Exception e) {
            e.printStackTrace();
        }
        resp.sendRedirect("/todo/list");

    }


}

(3) modify.jsp 작성

-삭제는 tno값이 보이지 않도록 <input type='hidden'>으로 처리

-수정은 TodoModifyController에서 POST 방식의 doPost()를 이용해 처리

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Todo Modify/Remove </title>
</head>
<body>

<form id="form1" action="/todo/modify" method="post">
    <div>
        <input type="text" name="tno" value="${dto.tno}" readonly>
    </div>
    <div>
        <input type="text" name="title" value="${dto.title}" >
    </div>
    <div>
        <input type="date" name="dueDate" value="${dto.dueDate}">
    </div>
    <div>
        <input type="checkbox" name="finished" ${dto.finished ? "checked": ""} >
    </div>

    <div>
        <button type="submit">Modify</button>
    </div>
</form>

<form id="form2" action="/todo/remove" method="post">
    <input type="hidden" name="tno" value="${dto.tno}" readonly>
    <div>
        <button type="submit">Remove</button>
    </div>
</form>


</body>
</html>

 

(4) TodoRemoveContoller에서 doPost() 메서드 오버라이딩

-특정 번호를 이용해 삭제 후, HttpServletResponse의 sendRedirect()로 목록 으로 이동

 

package org.zerock.jdbcex.controller;

import lombok.extern.log4j.Log4j2;
import org.zerock.jdbcex.service.TodoService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "todoRemoveController", value = "/todo/remove")
@Log4j2
public class TodoRemoveController extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        Long tno = Long.parseLong(req.getParameter("tno"));
        log.info("tno: " + tno);

        try{
            todoService.remove(tno);
        }catch(Exception e){
            log.error(e.getMessage());
            throw new ServletException("read error");
        }
        resp.sendRedirect("/todo/list");

    }
}

 

 

 


TodoService 구성해 ModelMapper 동작 확인

-TodoService가 ModelMapper와 TodoDAo를 이용하도록 구성

 

 

1. 새로운 TodoDTO를 등록하는 register()메서드 작성

package org.zerock.jdbcex.service;

import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.zerock.jdbcex.dao.TodoDAO;
import org.zerock.jdbcex.domain.TodoVO;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.util.MapperUtil;

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

@Log4j2
public enum TodoService {
    INSTANCE;

    private TodoDAO dao;
    private ModelMapper modelMapper;

    TodoService() {
        dao = new TodoDAO();
        modelMapper = MapperUtil.INSTANCE.get();
    }

    public void register(TodoDTO todoDTO)throws Exception{

        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);

        System.out.println("todoVO: " + todoVO);

        dao.insert(todoVO); //int 를 반환하므로 이를 이용해서 예외처리도 가능
    }

}

-> 테스트 코드 작성

 

package org.zerock.service;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.service.TodoService;

import java.time.LocalDate;

@Log4j2
public class TodoServiceTests {

    private TodoService todoService;

    @BeforeEach
    public void ready() {
        todoService = TodoService.INSTANCE;
    }


    @Test
    public void testRegister()throws Exception {

        TodoDTO todoDTO = TodoDTO.builder()
                .title("JDBC Test Title")
                .dueDate(LocalDate.now())
                .build();

        todoService.register(todoDTO);
    }

}

 

2. TodoService에 Log4j2 적용

 

(1) resources/log4j2.xml 생성

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

 

(2) @Log4j2 어노테이션을 이용해 log4j2xml 동작 확인

-TodoService 코드에 어노테이션 추가해서 log.info()로 변경

 

package org.zerock.jdbcex.service;

import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.zerock.jdbcex.dao.TodoDAO;
import org.zerock.jdbcex.domain.TodoVO;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.util.MapperUtil;

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

@Log4j2
public enum TodoService {
    INSTANCE;

    private TodoDAO dao;
    private ModelMapper modelMapper;

    TodoService() {
        dao = new TodoDAO();
        modelMapper = MapperUtil.INSTANCE.get();
    }

    public void register(TodoDTO todoDTO)throws Exception{

        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);

        //System.out.println("todoVO: " + todoVO);
        log.info(todoVO);

        dao.insert(todoVO); //int 를 반환하므로 이를 이용해서 예외처리도 가능
    }

}

-> log4j2를 적용하면 HikariCP가 내부에서 slf4j라이브러리를 이용하므로 다르게 출력한다.

-log4j-slf4j-impl 라이브러리가 log4j2을 이용하도록 설정되었기 때문에

 

3. 테스트 환경에서 @Log4j2 활용

-테스트 환경에서도 사용하기 위해 build.gradl에 testAnnotation, testCompileOnly 설정 추가

    testCompileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.22'
    testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.22'

 

설정 반영하고, 테스트 코드를 @Log4j2를 이용하도록 수정

package org.zerock.service;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.jdbcex.dto.TodoDTO;
import org.zerock.jdbcex.service.TodoService;

import java.time.LocalDate;

@Log4j2
public class TodoServiceTests {

    private TodoService todoService;

    @BeforeEach
    public void ready() {
        todoService = TodoService.INSTANCE;
    }

    @Test
    public void testRegister()throws Exception {

        TodoDTO todoDTO = TodoDTO.builder()
                .title("JDBC Test Title")
                .dueDate(LocalDate.now())
                .build();

        log.info("---------------------------------"); //log4j2
        log.info(todoDTO);

        todoService.register(todoDTO);
    }

}

 

 

 


Log4j2와 @Log4j2

-Log4j2를 이용해 로그 레벨과 어펜더를 이용해 로그 기능을 구현

-어펜더 : 로그를 콘솔창에 출력할지, 파일로 출력할지 결정

 

 

 

 

 

Log4j 의존성 추가

// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.19.0'

// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.19.0'

// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.19.0'

 

log4j2.xml 설정파일

-log4j2 라이브러리 설정하는 파일로 어펜더나 로그 레벨을 결정

 


컨트롤러와 서비스 객체의 연동

-필터를 통해 한글 처리 하지 않은 상태에서 연동

기능 동작 방식 컨트롤러 JSP
목록 GET TodoListController WEB-INF/todo/list.jsp
등록(입력) GET TodoRegisterController WEB-INF/todo/register.jsp
등록(처리) POST TodoRegisterController Redirect 
조회 GET TodoReadController WEB-INF/todo/read.jsp
수정(입력) GET TodoModifyController WEB-INF/todo/modify.jsp
수정(처리) POST TodoModifyController Redirect
삭제(처리) POST TodoRemoveController Redirect

# 애플리케이션 컨텍스트를 '/'로 변경하고, Deployment에 .war파일을 exploded로 변경

 

# 변경 파일을 업데이트 설정 추가

-On 'Update' action : Update classes and resources

-On frame deactivation : Update classes and resources

 

JSTL 라이브러리 추가

implementation group: 'jstl', name: 'jstl', version : '1.2'

 


웹 MVC를 이용해 책임과 역할의 구분한 코드 작성

 

-개선해야할 사항들

  • 여러 개 컨트롤러 작성하는 번거로움
  • 동일한 로직 반복 사용
  • 예외 처리 부재
  • 반복 메소드 호출
반응형