공부기록/Study

[자바 웹개발 워크북] 2장 웹과 데이터베이스

메델 2023. 2. 5. 11:22

 

1. 데이터 베이스 

 

DB

 

데이터를 보관하고 관리하는 기능을 하는 SW

보관/관리 패러다임에 따라서 여러 종류가 존재, 일반적으로 관계형 데이터베이스(RDBMS )제품을 많이 사용

ex) Oracle, MSSQL, MySQL, MariaDB, Postgresql 등

 

JDBC 프로그램(Java Database Connectivity)

 

  • 자바 프로그램과 데이터베이스는 네트워크 상에서 연결해 데이터를 교환하는 프로그램
  • java.sql 패키지와 javax.sql 패키지 이용
  • JDBC 프로그램을 작성하려면 DB와 자바 프로그램 사이에서 네트워크 데이터를 처리하는 코드가 필요  → JDBC 드라이버가 수행 

JDBC 프로그램 작성 순서

 

  1. 네트워크를 통해서 DB와 연결을 맺는 단계
  2. DB에 보낼 SQL을 작성하고 전송하는 단계
  3. 필요하다면 DB가 보낸 결과를 받아서 처리하는 단계
  4. DB와 연결을 종료하는 단계 

 

테스트 프로그램 작성

 

package org.zerock.dao;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ConnectTests {

    @Test
    public void test1(){
        int v1 = 10;
        int v2 = 10;
        Assertions.assertEquals(v1, v2);
    }
}

 

@Test로 적용하는 메소드는 반드시 public으로 선언, 파라미터나 리턴타입 없이 작성해야한다.

assertEquals()는 두 변수의 내용이 같아야만 테스트 성공 

 

MariaDB 연결 확인 테스트 코드

 

package org.zerock.dao;

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

        Connection connection = DriverManager.getConnection(
                "jdbc:mariadb://localhost:3306/webdb",
                "ID 입력",
                "PASSWORD 입력");

        Assertions.assertNotNull(connection);
        connection.close();
    }
}

 

  • Class.forName(): JDBC 드라이버 클래스는 메모리상으로 로딩하는 역할, 문자열은 패키지명과 클래스명의 대소문자까지 정확하게 일치해야함, JDBC 드라이버 파일이 없는 경우 이 부분에서 예외 발생
  • Connection connection: java.sql 패키지의 Connection 인터페이스 타입의 변수, Connection은 데이터 베이스와 네트워크 연결의미 
  • DriverManager.getConnection(): DB 내에 있는 여러 정보들을 통해서 특정한 DB(여기선 webdb)에 연결 시도
  • -'jdbc:mariadb://localhost:3306/webdb': jdbc 프로토콜을 이용한다는 의미, localhost:3306은 네트워크 연결 정보, webdb는 연결하려는 DB 정보 의미
  • -webuser: 연결을 위해서는 사용자의 계정과 패스워드 필요
  • Assertions.assertNotNull(): DB와 정상적으로 연결이 되면 Connection 타입의 객체는 null이 아니라는 것을 확신한다는 의미
  • connection.close(): DB와 연결 종료, JDBC 프로그램은 DB와 연결을 잠깐식 맺고 종료하는 방식으로 처리, 반드시 작업이 완료되면 DB와의 연결을 종료해주어야 한다.

 

데이터베이스 테이블 생성

 

관계형 데이터베이스에서는 데이터를 저장하기 위해서는 테이블을 생성

테이블은 여러 개의 column과 row로 구성, 각 cloumn에는 이름과 타입, 제약 조건 결합 

 

▶ MariaDB에서 사용하는 데이터 타입

 

[숫자형 데이터 타입]

타입 용도 크기 설명
TINYINT 매우 작은 정수 1 byte -128 ~ 127(부호 없이 0 ~ 255)
SMALLINT 작은 정수  2 byte - 32768 ~ 32767
MEDIUMINT 중간 크기의 정수 3 byte -(-8388608) ~-1(8388607)
INT 표준 정수  4 byte - 2147483648 ~ 2147483647
( 0 ~4294967295)
BIGINT 큰 정수 8 byte - 2147483648 ~ 2147483647
(unsigned 0 ~4294967295)
FLOAT 단정도 부동 소수 4 byte - 9223372036854775808 ~
9223372036854775807
DOUBLE 배정도 부동 소수 8 byte - 1.7976E+320 ~ 1.7976E+320(no unsigned)
DECIMAL(m, n) 고정 소수  m과 n에 따라 다르다 숫자 데이터지만 내부적으로 String의 형태로 저장, 최대 65자
BIT(n) 비트 필드 m에 따라 다르다 1~64bit 표현 

 

[날짜형 데이터 타입]

데이터 타입 형태 크기  설명
DATE YYYY-MM-DD 3 byte 1000-01-01 ~ 9999-12-31
DATETIME YYYY-MM-DD hh:mm:ss 8 byte 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
TIMESTAMP YYYY-MM-DD hh:mm:ss 4 byte 1970-01-01 00:00:00 ~ 2037
TIME hh:mm:ss 3 byte -839:59:59 ~ 839:59:59
YEAR YYYY 또는 YY 1 byte 1901 ~ 2155

 

[문자형 데이터 타입]

데이터 타입 용도 크기  설명
CHAR(n) 고정 길이 비이진(문자) 문자열 n byte  
VARCHAR(n) 가변 길이 비이진 문자열 Length + 1byte  
BINARY(n) 고정 길이 이진 문자열 n byte  
VARBINARY(n) 가변 길이 이진 문자열 Length + 1byte/2byte  
TINYBLOB 매우 작은 BLOB(Binary Large Object) Length + 1byte  
BLOB 작은 BLOB Length + 2byte 최대 크기 64KB
MEDIUMBLOB 중간 크기 BLOB Length + 3byte 최대 크기 16MB
LONGBLOB 큰 BLOB Length + 4byte 최대 크기 4GB
TINYTEXT 매우 작은 비이진 문자열 Length + 1byte  
TEXT 작은 비이진 문자열 Length + 2byte 최대 크기 64KB
MEDIUMTEXT 중간 크기 비이진 문자열 Length + 3byte 최대 크기 16MB
LONGTEXT 큰 비이진 문자열 Length + 4byte 최대  크기 4GB

 

SQL 테이블 생성

 

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

 

  • 테이블 생성은 'create table'로 시작하고 테이블 이름 지정
  • auto_increment는 식별키(primary key)를 지정하기 위해 사용
  • auto_increment는 새로운 데이터가 추가될 때 자동으로 새로운 번호 생성 → 같은 번호가 생성되지 않기 때문에 고유한 식별을 위한 용도로 사용
  • MariaDB에서 boolean 값은 true/false 값 대신에 0과 1을 사용하는 경우가 많으므로 tinyint 타입을 이용해 처리
  • DDL(Data Definition Language): DB에서 실행하는 SQL의 종류 중에 앞선 방식의 테이블을 생성하거나 특정한 객체들을 생성할때 사용하는 SQL

 

데이터 insert

 

테이블은 클래스와 유사하게 데이터의 '형식이나 틀'을 만드는 것이기 때문에 실제 데이터를 추가하는 작업 별도로 진행

DML(Data Manipulation Language) : 데이터를 조작할 때 사용하는 SQL로 insert/update/delete 등을 사용

 

insert into tbl_todo(title, dueDate, finished)
values('TEST....', '2022-12-31', 1);

 

  • insert 문은 특정한 테이블에 데이터를 추가하기 위해서 사용 
  • 테이블과 칼럼을 지정하고 values문을 이용해서 타입에 맞는 데이터를 모아서 '()'를 이용해 처리 
  • 데이터를 추가할 때는 칼럼의 타입과 추가하는 데이터의 타입을 맞춰주어야함 (문자의 경우 '', "", 날짜의 경우 변환하여 사용)
  • dueDate의 경우 문자열의 포맷과 데이터의 저장 형태가 같기때문에 사용 가능 
  • insert문에서 tno의 칼럼은 지정 x →  tno 칼럼의 값은 자동으로 생성되는 auto_increment로 지정 했기 때문

 

데이터 select

 

  • 데이터를 조회하는 SQL은 query라고 하며 'select'를 이용해서 작성
  • select 문은 'from절'을 이용해서 가져오려는 데이터의 대상 테이블 등을 지정하고, 'where절'을 이용해서 대상에서 필터링을 지정
  • where 조건에 해당하는 데이터가 여러건이라면 모든 데이터를 다 가져옴 
select * from tbl_todo where tno = 1;

 

데이터 update

 

기존 데이터를 수정하려면 update 문을 이용해 처리

update 문은 set을 이용해 특정한 칼럼의 내용을 수정, where 조건을 이용해서 수정하는 대상 데이터들을 지정 

 

update tbl_todo set finished = 0, title = 'not yet' where tno = 2;

 

데이터 delete

 

  • 데이터 삭제는 delete 문을 이용해 작성
  • delete 문은 where 조건에 해당하는 데이터들을 삭제하기 때문에 주의해야 함
  • where 조건이 없는 경우 모든 데이터를 삭제할 수 있는 위험성이 존재하여 경고 메시지를 출력하고 실행되지 않도록 설정

 

DML과 쿼리(select)의 차이 

 

  • DML은 몇 개의 데이터가 처리되어있는지 숫자로 결과 반환
    • insert/update/delete를 실행하고 나면 추가된 데이터를 보여주는 것이 아니라 '몇 개의 row가 추가/변경/삭제 되었는지'를 알려줌
  • select 문은 데이터로 변환 

→ 결과에 대한 처리 방식이 다르기 때문에 JDBC 프로그램을 작성할 때에도 분리해서 작성

 

 

JDBC 프로그래밍을 위한 API와 용어

(1) java.sql.Connection

 

  • Connection 인터페이스는 데이터베이스와 네트워크상의 연결 의미 
  • DB에 SQL을 실행하기 위해서는 반드시 Connection 타입의 객체를 생성
  • 개발자는 Connection 인터페이스를 활용하고 실제 구현 클래스는 JDBC 드라이버 파일 내부의 클래스를 이용
  • Connection는 반드시 close()  해야함 → DB는 많은 연결을 처리해야하는데 연결이 종료 되지 않으면 새로운 연결을 받을 수 없음
  • Connection의 close()는 DB 쪽에 연결을 끊어도 좋다는 신호를 주고 네트워크 연결을 종료하는역할 
  • Connection 종료를 위해서는 코드 내에 try-catch-finally를 이용해서 종료하거나 try-with-resources 방식 이용(try-with-resources를 이용하면 자동으로 close()가 호출되는 것을 보장)
  • Connection의 가장 중요한 기능은 Statement 혹은 Prepared-Statement와 같이 SQL을 실행할 수 있는 객체를 생성하는 기능

 

(2) java.sql.Statement/PreparedStatement

 

  • JDBC에서 SQL은 DB를 보내기 위해서 Statement/PreparedStatement 타입을 이용(이외에도 DB 내 프로시저 등을 호출 하기 위한 CallableStatement 존재 하지만 순수한 SQL는 Statement/PreparedStatement를 이용)
  • Statement와 PreparedStatement는 SQL을 전달하는 점에서는 동일하지만 SQL 문을 미리 전달하고 나중에 데이터를 보내는 방식(PreparedStatement)와 SQL 문 내부에 모든 데이터를 같이 전송하는 방식(Statement) 차이 존재 
  • SQL 내부에 고의적으로 다른 처리가 가능한 SQL 문자열을 심어서 보내는 SQL injection 공격을 막기 위해 실제 개발에서는 PreparedStatement만을 이용하는 것이 관례 
  • Statement/PreparedStatement의 중요한 기능
    • setXXX(): setInt(), setString(), setDate()와 같이 다양한 타입에 맞게 데이터를 세팅할 수 있음
    • executeUpdate(): DML(insert/update/delete)을 실행하고 결과를 int 타입으로 반환, 결과는 '몇 개의 행이 영향을 받았는가'(ex. 1개의 데이터가 insert되었는지와 같은 결과)
    • executeQuery(): 쿼리를 실행할 때 사용, executeQuery()의 경우에는 ResultSet이라는 리턴타입 이용
  • Statement 역시 마지막에는 close()를 통해 종료 → DB 내부에서도 메모리와 같이 사용했던 자원들을 즉각적으로 정리

 

(3) java.sql.ResultSet

 

  • PreparedStatement를 이용해 insert/update/delete를 처리하는 DML의 경우 int로 반환되는 것과 달리 쿼리(select)를 실행했을 때 DB에서 반환하는 데이터를 읽어 들이기 위해서는 ResultSet이라는 인터페이스 이용
  • ResultSet은 getInt(), getString() 등의 메소드 들을 이용해서 필요한 타입으로 데이터를 읽어 들임
  • ResultSet은 데이터를 순차적으로 읽는 방식으로 구성되어 있기 때문에 next()를 이용해 다음행의 데이터를 읽을 수 있도록 이동하는 작업 필요
  • ResultSet 역시 네트워크를 통해 data를 읽어 들이기 때문에 작업이 끝난 후에 반드시 close() 해야함

 

(4) Connection Pool과 DataSource

 

JDBC 프로그램은 필요한 순간에 DB와 네트워크를 연결하고 데이터를 보내는 방식으로 구성

→ DB와의 연결을 맺는 작업은 많은 시간과 자원을 쓰기 때문에 여러번 SQL을 실행할수록 성능 저하

→ JDBC에서는 이 문제를 해결하고자 Connection Pool을 이용 

 

Connection Pool

  • 미리 Connection들을 생성해서 보관하고 필요할 때마다 꺼내서 쓰는 방식 
  • Connection Pool은 DB와 연결된 Connection을 보관하기 때문에 DB와 연결에 걸리는 시간과 자원을 절약할 수 있어 실제 웹서비스에서는 Connection Pool 기본적으로 사용 
  • javax.sql.DataSource 인터페이스는 Connection Pool을 자바에서 API 형태로 지원하는 것 → Connection Pool을 이용하는 라이브러리 들은 모두 DataSource 인터페이스를 구현하고 있어 이를 활용하여 JDBC 코드 작성 

 

DAO(Data Access Object)

 

  • 데이터를 전문적으로 처리하는 객체
  • DB의 접근과 처리를 전담하는 객체 
  • 주로 VO 단위로 처리
  • DAO를 호출하는 객체는 DAO가 내부에서 어떤식으로 데이터를 처리하는지 알수 없도록 구성 → JDBC 프로그램을 작성한다는 것은 DAO를 작성한다는 의미

 

VO(Value Object) 혹은 Entity(엔티티)

 

  • DB에서 하나의 데이터를 엔티티라고 함 
  • JAVA 프로그램에서 엔티티를 처리하기 위해서 테이블과 유사한 구조의 클래스를 만들어 객체를 처리하는 방식 사용 → 이때 만든 객체는 '값을 보관하는 용도'라는 의미에서 VO라 함 
  • VO는 데이터 베이스의 엔티티를 자바 객체로 표현한 것 
  • DTO의 경우 getter/setter를 이용해서 자유롭게 데이터를 가공 가능하지만 VO의 경우 주로 데이터를 의미해 getter만 이용하는 경우가 대부분

 

2. 프로젝트 내 JDBC 구현  

 

Lombok 라이브러리

 

롬복을 이용하면 간단한 어노테이션을 추가하는 것만으로 끝낼 수 있음

롬복을 이용해서 다음과 같은 작업 처리 가능

 

  • getter/setter관련: @Getter, @Setter, @Data 등을 이용해서 자동 생성
  • toString(): @ToString을 이용한 toString() 메소드 자동 생성
  • equals()/hashCode(): @EqualsAndHashCode를 이용한 자동 생성
  • 생성자 자동 생성: @AllArgsConstructor, @NoArgsConstructor 등을 이용한 생성자 자동 생성
  • 빌더 생성: @Builder를 이용한 빌더 패턴 코드 생성 

TodoVO 클래스 작성

 

DB에 만든 테이블의 데이터를 자바 객체로 처리하기 위해 TodoVO 클래스와 객체들을 이용

Lombok을 이용하면 반복적으로 생성하는 코드를 줄여주기 때문에 DTO, VO 등을 작성할 때 편리

 

package org.zerock.jdbcex.domain;


import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

import java.time.LocalDate;

@Getter
@Builder
@ToString
public class TodoVO {
    
    private long tno;
    private String title;
    private LocalDate dueDate;
    private boolean finished;
    
    
}

 

TodoVo 클래스는 DB 내에 생성한 tbl_todo 테이블의 칼럼들을 기준으로 작성

VO는 주로 읽기 전으로 사용하는 경우가 많아 @Getter 추가

객체 생성시 빌더 패턴을 이용하기 위해 @Builder 추가 → TodoVO.builder().build() 와 같은 형태로 객체를 생성

 

HiKariCP의 설정

 

Connection의 생성은 Connection Pool인 HikariCP 이용

 

implementation group: 'com.zaxxer', name:'HikariCP', version:'5.0.0'

 

Connection Pool 이용

 

  • HikariCP를 이용하기 위해서는 HikariConfig라는 타입의 객체를 생성
  • HikariConfig는 Connection Pool를 설정하는데  있어서 필요한 정보를 가지고 있는 객체를 이용해서 HikariDataSource라는 객체 생성
  • HikariDataSource는 GetConnection()을 제공하므로 이를 이용해서 Connection  객체를 얻어서 사용할 수 있음
  • DB 연결을 많이할 수록 HikariCP를 이용하는 것과 사용하지 않는 것에는 상당한 성능 차이 발생
  • DB가 원격지에 떨어져 있는 경우에는 네트워크 연결에  더 많은 시간을 소비해야 하기 때문에 차이 ↑
package org.zerock.dao;

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

import java.sql.Connection;


public class ConnectTests {

    @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("아이디");
        config.setPassword("비밀번호");
        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();
    }
}

 

TodoDAO와 @Cleanup

 

ConnectionUtil은 하나의 객체를 만들어서 사용하는 방식으로 구성

→ HikariConfig를 이용해 하나의 HikariDataSource를 구성

 

구성된 HikariDataSource는 getConnection()을 통해 사용하게 되는데

외부에서는 ConnectionUtil.INSTANCE.getConnection()을 통해서 Connection을 얻을 수 있도록 구성 

 

ConnectionUtil 클래스

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:3307/webdb");
        config.setUsername("아이디");
        config.setPassword("비밀번호");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", 250);
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        
        ds = new HikariDataSource(config);
    }
    public Connection getConnection()throws Exception{
        return ds.getConnection();
    }
}

 

TodoDAO 클래스

 

getTime()은 try-with-resources 기능을 이용해서 try() 내에 선언된 변수들이 자동으로 close()될 수 있는 구조로 작성

(try()내에 선언된 변수들은 Auto Closeable이라는 인터페이스를 구현한 타입들이어야만 함)

 

package org.zerock.jdbcex.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class TodoDAO {

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

 

DAO를 작성하면 항상 테스트 코드를 이용해서 동작에 문제 없는지 확인

 

TodoDAOTests는

@BeforeEach를 이용하는 ready()를 통해서 모든 테스트 전에 TodoDAO 타입의 객체를 생성,

testTime()을 통해 TodoDAO에 작성한 getTime()이  정상 동작하는지 확인

 

package org.zerock.dao;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.dao.jdbcex.dao.TodoDAO;

public class TodoDAOTests {

    private TodoDAO todoDAO;

    @BeforeEach
    public void ready(){
        todoDAO = new TodoDAO();
    }

    @Test
    public void testTime() throws Exception{
        System.out.println(todoDAO.getTime());
    }
}

 

Lombok의 @Cleanup

 

try-with-resource의 경우 중첩으로 try-catch를 사용하게 되면 가독성이 나빠짐 

→Lombok의 @Cleanup 이용하면 간결한 코드 생성 가능

 

@Cleanup이 추가된 변수는 해당 메서드가 날 때 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이 붙는 점만 달라짐 

 

@Cleanup을 이용하면 Lombok 라이브러리에 상당히 종속적인 코드를 작성하게 된다는 단점이 존재 하지만 최소한의 코드로 close()가 보장되는 코드를 작성할 수 있다는 장점이 존재 

 

 

TodoDAO의 등록 기능 구현

 

insert()는 파라미터로 입력된 TodoVO 객체의 정보를 이용해서 DML(insert/update/delete)를 실행하기 때문에 executeUpdate()를 실행하도록 구성

PreparedStatement는 "?"를 이용해서 나중에 전달할 데이터들을 지정

setXXX()를 이용해서 실제 값들을 지정 (이때 인덱스가 0이 아니라 1부터 시작)

예제의 경우 "?'가 3개 존재하므로 setXXX() 역시 3개 지정해야 함

setXXX()는 다양한 타입에 맞춰서 값을 지정할 수 있음

날짜의 경우 LocalDate 타입을 지원하지 않기 때문에 java.sql.Date() 타입을 이용해 변환해서 추가 

 

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);
    
    preparedStatement.setString(1, vo.getTitle());
    preparedStatement.setDate(2, Date.valueOf(vo.getDueDate()));
    preparedStatement.setBoolean(3, vo.isFinished());
    preparedStatement.executeUpdate();
    
}

 

TodoDAOTests 테스트 코드 

 

빌더 패턴은 생성자와 달리 필요한 만큼만 데이터를 세팅할 수 있음

finish 속성은 false로 기본 지정되어 있고, 변경할 필요가 없기 때문에 세팅하는 부분이 없음

 

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

    todoDAO.insert(todoVO);
}

 

testInsert()를 실행하고 문제가 없다면 tbl_todo 테이블에 새로운 번호의 데이터가 추가됨

 

 

TodoDAO의 목록 기능 구현

TodoDAO를 이용해서 tbl_todo 내의 모든 데이터를 가져오는 기능 

 

테이블의 각 행(row)은 하나의 TodoVO 객체

모든 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;
    
}

 

  • selectAll()은 쿼리(select)를 실행해야 하기 때문에 PreparedStatement의 executeQuery()를 이용해 ResultSet을 구함
  • ResultSet으로 각 행을 이동하면서(next()의 결과는 이동할 수 있는 행(row)이 존재 하는 경우에는 true, 아닌 경우에 false) 각 행의 데이터를 TodoVO로 변환
  • TodoVO는 빌더 패턴을 이용해서 간편하게 TodoVO 객체를 생성
  • tno/title 등의 속성값을 ResultSet에서 가져온 데이터로 처리 
  •  ResultSet의 getXXX()는 칼럼의 인덱스 번호를 이용하거나 칼럼의 이름을 지정해서 가져올 수 있음 인덱스 번호를 이용하는 경우 반드시 1부터 시작

 

TodoDAOTests에 selectALL() 메서드 테스트

 

@Test
public void testList() throws Exception{

    List<TodoVO> list = todoDAO.selectAll();
    list.forEach(vo -> System.out.println(vo));
}

tbl_todo 테이블 내용 모두 출력

 

TodoDAO의 조회 기능 구현

 

  • 특정한 번호(tno)의 데이터만 가져오는 기능 필요
  • selectOne()이라는 메서드로 작성하고 특정한 번호(tno)가 파라미터가 되고, TodoVO가 리턴타입으로 지정
  • TodoDAO에 selectOne() 작성
  • selectOne()은 쿼리를 실행하기 때문에 ResultSet이 필요 
  • selectOne()은 한 행의 데이터만 나오기 때문에  한번만 resultSet.next()를 실행하면 된다.
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;
}

 

TodoDAOTests

 

@Test
public void testSelectOne() throws Exception{
    Long tno = 1L;
    TodoVO vo = todoDAO.selectOne(tno);
    System.out.println(vo);
}

정상적으로 테스트를 통과하면 아래와 같은 결과를 볼 수 있다.

 

TodoDAO의 삭제/수정 기능 구현

 

삭제 기능

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

 

수정기능

 

특정한 번호(tno)를 가진 데이터의 제목, 만료인, 완료 여부를 update 하도록 구성

 

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

 

TodoDAOTests

 

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

 정상적으로 테스트코드가 진행되었다면 tno=1이 변경된 값을 가지게 된다.

 

3. 웹 MVC와 JDBC 의 결합

 

TodoDTO 클래스 선언 

 

@Data 는 getter/setter/toString/equals/hashCode 등을 모두 컴파일 할때 생성 

 

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;

}

 

DTO → VO, VO → DTO 변환은 ModelMapper 라이브러리 이용 

ModelMapper: getter/setter 등을 이용해서 객체의 정보를 다른 객체로 복사하는 기능 제공

 

build.gradle 파일에 추가 

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

 

ModelMapper의 설정을 변경하고 쉽게 사용할 수 있는 MapperUtil 생성

 

ModelMapper 설정을 변경하려면 getConfiguration()을 이용해서 private로 선언된 필드도 접근 가능하도록 설정 변경,

get()을 이용해서 ModelMapper를 사용할 수 있도록 구성 

 

package org.zerock.jdbcex.util;

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

public enum MapperUtil {

    INSTANCE;

    private ModelMapper modelMapper;

    MapperUtil(){
        this.modelMapper = new ModelMapper();
        this.modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE)
                .setMatchingStrategy(MatchingStrategies. STRICT);
    }
    public ModelMapper get(){
        return modelMapper;
    }
}

 

TodoService와 ModelMapper 테스트

 

TodoService는 ModelMapper와 TodoDAO를 이용할 수 있도록 구성, 새로운 TodoDTO를 등록하는 기능 추가 

 

register()는 TodoDTO를 파라미터로 받아서 TodoVO로 변환하는 과정 필요

TodoDAO를 이용해서 insert()를 실행하고 TodoVO를 등록

package org.zerock.jdbcex.service;

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;

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("todo: "+ todoVO);
        dao.insert(todoVO);
    }
}

 

TodoServiceTests

 

package org.zerock.jdbcex.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.jdbcex.dto.TodoDTO;

import java.time.LocalDate;



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

}

 

 

Log4j2와 @Log4j2

 

Log4j2

 

  • 레벨이 존재 → 필요한 레벨의 로그와 실제 운영 시에 필요한 로그를 쉽게 구분할 수 있음
  • Lombok의 경우 @Log4j2 어노테이션을 이용하여 간단하게 소스코드 내에 로그 적용
  • Log4j2에서 가장 핵심적인 개념 = 로그의 레벨, 어펜더
    • 어펜더(Appender)는 로그를 어떤 방식으로 기록할 것인지를 의미, 콘솔창에 출력할 것인지, 파일에 출력할 것인지 결정
    • 로그의 레벨 = 로그의 중요도, 로그의 레벨을 지정하면 해당 레벨 이상의 로그들만 출력
    • 개발할때 Info 이하의 레벨을 이용해서 작성하고, 운영할 때 Error나 Warn 이상을 사용

로그 레벨

Log4j2를 이용하기 위해 build.gradle에 해당 내용 추가 

 

implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.2'
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.2'
implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version:'2.17.2'

 

log4j2.xml 설정파일

 

  • Log4j2 라이브러리의 설정은 log4j2.xml 파일을 이용해서 설정
  • Appender나 로그 레벨 설정을 하는 파일
  • 파일을 통해서 설정하기 때문에 나중에 파일 내용만 변경하면 코드 수정 없이 바로 변경된 레벨을 활용할 수 있음

 

프로젝트의 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>

 

테스트 환경에서 @Log4j2 사용

 

build.gradle에 추가

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

 

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

 

여러개의 컨트롤러는 모두 하나의 TodoService를 통해서 자신이 원하는 기능을 전달하고 처리하는 구조로 이루어짐

 

목록 기능 구현

목록 기능은 controller 패키지를 추가하고 TodoListController를 추가해서 처리

GET 방식의 처리이므로 doGet()을 오버라이드해서 처리

 

TodoService의 목록기능 구현

 

listAll()은 TodoDAO에서 가져온 TodoVO의 목록을 모두 TodoDTO로 변환해서 반환

→ ModelMapper와 Java Stream의 map()을 이용하면 간단한 코드로 처리할 수 있음

 

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("todo: "+ todoVO);
        log.info(todoVO);
        dao.insert(todoVO);
    }

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

 

TodoListController 수정

 

TodoListController에서 HttpServletRequest의 setAttribute()를 이용해서 TodoService객체가 반환하는 데이터를 저장하고 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("todoList...");

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


}

 

list.jsp

 

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

</head>
<body>
<h1>TodoList</h1>
<ul>
    <c:forEach items="${dtoList}" var="dto">
        <li>${dto}</li>
    </c:forEach>
</ul>

</body>
</html>

 

등록 기능 구현

 

Todo의 등록 기능은 GET 방식으로 등록 화면을 보고 <form> 태그 내에 입력 항목들을 채운 후에 POST 방식으로 처리

처리 후에는 목록화면으로 redirect하는 PRG(post-redirect-get) 패턴 방식

 

TodoService의 등록 기능 구현

 

register()는 파라미터로 TodoDTO를 받아서 TodoVO로 변환하고 이를 저장

 

    public void register(TodoDTO todoDTO)throws Exception{
        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
//        System.out.println("todo: "+ todoVO);
        log.info(todoVO);
        dao.insert(todoVO);
    }

 

TodoRegisterController 구현

 

GET/POST 방식 모두 사용하므로 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 DATETIMEFORMATTER = 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"),DATETIMEFORMATTER))
                .build();

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

 

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>

 

TodoRegisterController의 doPost()에는 HttpServletRequest의 getParameter()로 title/dueDate를 이용해서 TodoDTO 구성, 최종적으로는 TodoService의 register()를 호출

 

정상적으로 등록된 후 GET방식으로 바로 리다이렉트 → 목록 화면으로 이동 

 

조회 기능 구현

 

조회 기능은 GET 방식으로 동작, TNO라는 파라미터를 쿼리 스트링으로 번호를 전달하는 방식

TodoService에서는 TodoDTO를 반환하고 컨트롤러에서 HttpServletRequest에 담아서 JSP에서 출력

 

TodoService에 get 메서드 추가

 

get()에서는 TodoDAO의 selectOne()을 통해서 TodoVO 객체를 가져오고, ModelMapper를 이용해서 TodoDTO로 변환 

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

 

TodoReadController의 구현 

 

doGet()을 이용하고 , read.jsp로 TodoDTO를 전달하도록 구성

doGet()에는 'dto'라는 이름으로 TodoDTO를 담아주고 read.jsp는 EL을 이용해서 출력 

 

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

 

read.jsp

 

 

목록에서 조회 링크 처리 

 

list.jsp 수정

 

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

</head>
<body>
<h1>TodoList</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>

수정/삭제 기능 구현

TodoService에서 remove()와 modify() 메서드를 추가해서 기능 구현

remove() → 번호(tno)만을 이용

modify() → TodoDTO 타입을 파라미터로 이용 

 

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

 

TodoModifyController의 구현

 

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

 

modify.jsp

 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>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}" readonly>
  </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>

 

수정 작업의 경우 TodoModifyController에서 POST 방식으로 동작하는 doPost()를 이용하여 처리

doPost()의 내용은 <form> 태그에서 전송된 title, finished 등을 이용해서 TodoDTO 구성

만들어진 TodoDTO는 TodoService 객체로 전달 → 목록 화면으로 다시 이동 ( 수정된 결과)

 

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

TodoRemoveController의 구현

 

doPost()에서는 특정한 번호를 이용해서 삭제하고 HttpServletResponse의 sendRedirect()를 이용해서 '/todo/list'로 이동하도록 처리

 

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

 

코드의 개선 사항들

웹 MVC 구조를 이용하면 책임과 역할을 구분해서 작업을 진행할 수 있는 장점이 존재하지만 여러 개의 코드를 만들어야한다는 단점

 

  • 여러 개의 컨트롤러를 작성하는 번거로움 : TodoDAO나 TodoService와 달리 HttpServlet을 상속하는 여러 개의 컨트롤러를 작성해야하는 불편함
  • 동일한 로직의 반복적인 사용: 게시물의 조회나 수정 작업은 둘다 GET 방식으로 동작하지만 결과를 보여주는 JSP만 다른 형태인 상황 → 동일한 코드를 여러 번 작성해야하는 번거로움
  • 예외 처리의 부재 : 예외가 발생하면 어떤 식으로 처리해야 하는지에 대한 설계가 없기 때문에 비정상적인 호출이 발생했을경우 대비 X
  • 반복적인 메소드 호출: HttpServletRequest나 HttpServletResponse를 이용해서 TodoDTO를 구성하는 작업 등이 동일한 코드들로 작성되어서 이에 대한 개선 필요 , Long.paseLong() 과 같은 코드들도 많이 반복되는 문제 

 

→ 스프링 프레임워크가 이 문제를 어떻게 해결하는지 check

 

 

(참고: 자바 웹 개발 워크북)