본문 바로가기
카테고리 없음

Web & Database 개념

by SuldenLion 2023. 9. 8.
반응형

프로젝트 내 JDBC 구현과 웹 MVC + JDBC 결합에 대한 내용 정리

 

- JDBC 프로그램은 연결 가능한 DB와 JDBC 드라이버만 있으면 구현 자체는 가능. 하지만 DAO를 위한 테스트 환경이나 Connection Pool등의 환경이 갖춰지면 좀 더 편한 개발이 가능.

 

 

▶ Lombok 라이브러리

- 개발자 입장에서 번거로운 getter/setter, 생성자 정의하는 작업은 Lombok을 이용하여 어노테이션을 추가하는 것만으로 줄일 수 있음.

- getter/setter 관련 : @Getter, @Setter, @Data 등을 이용해서 자동 생성

- toString() : @ToString을 이용하여 메서드 자동생성

- equals()/hashCode() : @EqualsAndHashCode를 이용한 자동 생성

- 생성자 자동 생성 : @AllArgsConstructor, @NoArgsConstructor 등을 이용한 생성자 자동 생성

- 빌더 생성 : @Builder를 이용한 빌더 패턴 코드 생성

 

> Lombok 라이브러리 추가

- Lombok 사용을 위해서는 라이브러리를 추가하는 설정을 넣어야 함. (Lombok 라이브러리는 https://projectlombok.org/를 를 통해 build.gradle에 필요한 설정 추가 가능)

- build.gradle에 Lombok 라이브러리를 추가하고 annotationProcessor 항목을 통해서 프로젝트를 빌드할 때 Lombok을 사용하도록 지정함.

 

> ex) TodoVO 클래스

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

VO는 주로 읽기 전용으로 사용하는 경우가 많으므로 @Getter 추가. (getTno, getTitle() 등 호출 가능)

객체 생성시 빌더 패턴을 이용하기 위해 @Builder 어노테이션 추가

@Builder를 이용해서 TodoVO.builder().build()와 같은 형태로 객체 생성 가능.

 

▷ HikariCP 설정

- Connection Pool인 HikariCP로 Connection 생성

- build.gradle 파일의 dependencies에 라이브러리 추가

dependencies {
    compileOnly('javax.servlet:javax.servlet-api:4.0.1')
    
    ...
    
    implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.0.0'
}

- HikariCP 이용을 위해서는 HikariConfig라는 타입의 객체를 생성해야 함. (HikariConfig는 Connection Pool을 설정하는데 있어서 필요한 정보를 가지고 있는 객체로 이를 이용해서 HikariDataSource라는 객체를 생성)

- HikariDataSource는 getConnection()을 제공하므로 Connection 객체 얻어올 수 있음.

 

> 테스트 메서드 코드

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

@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 conn = ds.getConnection();
    
    System.out.println(conn);
    
    conn.close();
}

testHikariCP()의 결과로 동일한 Connection을 얻어오지만 HikariCP를 통해서 얻어왔다는 것을 출력 결과로 알 수 있음. (HikariProxyConnection ...)

DB연결을 많이 할수록 HikariCP를 사용하는 것과 사용하지 않는 것의 성능 차이가 크다고 함.

 

> HikariDataSource 처리를 쉽게 할 ConnectionUtil 클래스

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("webuser");
        config.setPassword("webuser");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        
        ds = new HikariDataSource(config);
    }
    
    public Connection getConnection() throws Exception {
    	return ds.getConnection();
    }
}

구성된 HikariDataSource는 getConnection()을 통해 사용하는데, 외부에서는 ConnectionUtil.INSTANCE.getConnection()을 통해서 Connection을 얻을 수 있도록 구성.

 

> ConnectionUtil을 사용하는 DAO 코드 샘플

import java.sql.*;

public class TodoDAO {
    public String getTime() {
        String now = null;
        
        try (Connection conn = ConnectionUtil.INSTANCE.getConnection();
        	PreparedStatement pstmt = conn.prepareStatement("select now()");
            ResultSet rs = pstmt.executeQuery();
            ) {
            
            rs.next();
            
            now = rs.getString(1);
        } catch(Exception e) {
            e.printStackTrace();
        }
        return now;
    }
}

↳ try() 내에 선언된 변수들이 자동으로 close() 될 수 있는 try -with -resources 구조. (변수들은 모두 AutoCloseable 인터페이스를 구현한 타입들이어야만 함)

 

> DAO를 실행할 테스트 코드

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.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());
    }
}

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

 

 

▷ Lombok의 @Cleanup

- @Cleanup을 사용하면 try-with-resource보다 좀 더 깔끔한 코드 생성 가능

- try~catch문 안에 try~catch가 있는 경우 가독성이 나빠지는데 이런 경우 @Cleanup 적용

- @Cleanup이 추가된 변수는 해당 메서드가 끝날때 close() 호출이 보장됨.

// 아래와 같이 사용.
@Cleanup Connection conn = ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement pstmt = conn.prepareStatement("select now()");
@Cleanup ResultSet rs = pstmt.executeQuery();

@Cleanup 사용은 Lombok 라이브러리에 종속적인 코드를 작성한다는 부담이 있기는 하지만 최소한의 코드로 close()가 보장되는 코드를 작성할 수 있다는 장점을 가짐.

 

> DAO에 VO객체 DB추가(등록) 기능 예제

import java.sql.*;

public void insert(TodoVO vo) throws Exception {
    String sql = "insert into tbl_todo (title, dueDate, finished) values (?, ?, ?)";
    
    @Cleanup Connection conn = ConnectionUtil.INSTANCE.getConnection();
    @Cleanup PreparedStatement pstmt = conn.prepareStatement(sql);
    
    pstmt.setString(1, vo.getTitle());
    pstmt.setDate(2, Date.valueOf(vo.getDueDate()));
    pstmt.setBoolean(3, vo.isFinished());
    
    pstmt.executeUpdate();
}

 

> DAOTest에 testInsert() 메서드 추가

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

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

 

> DAO의 목록 예제

public List<TodoVO> selectAll() throws Exception {
    String sql = "select * from tbl_todo";
    
    @Cleanup Connection conn = ConnectionUtil.INSTANCE.getConnection();
    @Cleanup PreparedStatement pstmt = conn.prepareStatement(sql);
    @Cleanup ResultSet rs = pstmt.executeQuery();
    
    List<TodoVO> list = new ArrayList<>();
    
    while (rs.next()) {
        TodoVO vo = TodoVO.builder()
                .tno(rs.getLong("tno"))
                .title(rs.getString("title"))
                .dueDate(rs.getDate("dueDate").toLocalDate())
                .finished(rs.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));
}

 

> 리스트 삭제 기능 메서드

public void deleteOne(Long tno) throws Exception {
    String sql = "delete from tbl_todo where tno = ?";
    
    @Cleanup Connection conn = ConnectionUtil.INSTANCE.getConnection();
    @Cleanup PreparedStatement pstmt = conn.prepareStatement(sql);

    pstmt.setLong(1, tno);
    
    pstmt.executeUpdate();
}

 

> 리스트 수정 기능 메서드와 테스트 코드

public void updateOne(TodoVO todoVO) throws Exception {
    String sql = "update tbl_todo set title = ?, dueDate = ?, finished = ? where tno = ?";
    
    @Cleanup Connection conn = ConnectionUtil.INSTANCE.getConnection();
    @Cleanup PreparedStatement pstmt = conn.prepareStatement(sql);

    pstmt.setString(1, todoVO.getTitle());
    pstmt.setDate(2, Date.valueOf(todoVO.getDueDate()));
    pstmt.setBoolean(3, todoVO.isFinished());
    pstmt.setLong(4, todoVO.getTno));
    
    pstmt.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);
}

 

 

 

▶ 웹 MVC와 JDBC의 결합

ModelMapper 라이브러리

- 포스팅 초기의 TodoVO 클래스 → TodoDTO 클래스로 재구성

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

↳ TodoDTO는 TodoVO와 완전히 같은 구조를 가지지만 다음과 같은 차이를 가짐.

↳ DTO에서 @Data 어노테이션을 쓰는데 getter/setter/toString/equals/hashCode 등을 모두 컴파일할때 씀.

↳ VO의 경우 getter만을 이용해서 주로 읽기 전용으로 구성함.

- DTO to VO와 VO to DTO 변환 작업은 ModelMapper 라이브러리를 이용해서 처리.

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

- 사용을 위해 ModelMapper 라이브러리를 build.gradle 파일 dependencies에 추가

dependencies {
    compileOnly('javax.servlet:javax.servlet-api:4.0.1')
    
    ...
    
    implementation group: 'org.modelmapper', name: 'modelmapper', version: '3.0.0'
}

 

- ModelMapper를 이용할때는 대상 클래스의 생성자를 이용할 수 있도록 생성자 관련 어노테이션들을 추가함.

- @NoArgsConstructor와 @AllArgsConstructor로 파라미터가 없는 생성자와 모든 필드값이 필요한 생성자를 만듦.

 

 

> ModelMapper 설정 변경 및 사용을 위한 MapperUtil 예제

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

public enum MapperUtil {
    INSTANCE;
    
    private ModelMapper modelMapper;
    
    MapperUtil() {
        modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                .setMatchingStrategy(MatchingStrategies.STRICT);
    }
    
    public ModelMapper get() {
        return modelMapper;
    }
}

↳ ModelMapper 설정 변경은 getConfiguration() 이용해서 private으로 선언된 필드도 접근 가능하도록 설정을 변경. 그리고 get()을 이용해서 ModelMapper를 사용할 수 있도록 구성.

 

> DTO와 VO를 둘 다 이용해야하는 Service 객체 코드

- 아래의 enum 객체 TodoService는 ModelMapper와 TodoDAO를 이용할 수 있도록 구성하고, 새로운 TodoDTO를 등록하는 기능을 가짐

// ... ModelMapper, TodoDAO, TodoVO, TodoDTO, 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("todoVO: " + todoVO);
        dao.insert(todoVO);
    }
}

 

 

▶ Log4j2와 @Log4j2

- 프로젝트 개발시 값 출력시킬 일이 많은데, 개발이 끝난 후 대부분의 System.out.println()이 필요없어지는 문제가 있음. 해당 부분들을 모두 삭제하거나 주석 처리를 할 필요없이 사용할 수 있는 로그 기능.

- Log4j2에서 가장 핵심적인 개념은 로그의 Level과 Appender

- Appender는 로그를 어떤 방식으로 기록할 것인지를 의미함. (콘솔에 출력할 것인지, 파일로 출력할 것인지 등)

- System.out.println() 대신 Console Appender라는 것을 지정해서 사용

- 로그의 Level은 로그의 '중요도' 개념. System.out.println() 작성시 모든 내용이 출력되지만 로그의 레벨을 지정하면 해당 레벨 이상의 로그들만 출력됨. 개발할 때 로그의 레벨을 많이 낮게 설정해서 개발하고 운영할 때 중요한 로그들만 기록하게 설정함. (일반적으로 개발 = INFO 이하의 레벨, 운영 = WARN 이상의 레벨 사용)

Level 지정시 모든 상위 Level의 로그가 출력

 

> Log4j2 사용을 위한 라이브러리 추가

dependencies {
    compileOnly('javax.servlet:javax.servlet-api:4.0.1')
    
    ...
    
    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 설정 파일

- Appender와 Log Level 설정하는 파일.

<?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 테스트

- @Log4j2 어노테이션을 추가하고, System.out.println() 대신 log.info()를 사용. 

// Log4j2, ModelMapper, TodoDAO, TodoVO, TodoDTO, MapperUtil 임포트

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

↳ Log4j2 적용 후의 출력은 HikariCP의 로그도 다르게 출력된다 함. HikariCP가 내부적으로 slf4j 라이브러리를 이용하는데, build.gradle의 log4j-slf4j-impl 라이브러리가 Log4j2를 이용할 수 있도록 설정되기 때문.

 

 

> 테스트 환경에서 @Log4j2 사용하기

- testAnnotationProcessor와 testCompileOnly 설정 추가

dependencies {
    compileOnly('javax.servlet:javax.servlet-api:4.0.1')
    
    ...
    
    testCompileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.24'
    testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.24'
}

 

> TodoServiceTests

// Log4j2, BeforeEach, Test, TodoDTO, TodoService, 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("---------------------------------");
        log.info(todoDTO);
        
        todoService.register(todoDTO);
    }
}

 

 

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

- 구현할 컨트롤러 목록

기능 동작 방식 컨트롤러 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

 

▷ 목록

> TodoListController

// Log4j2, TodoService, Servlet 관련 패키지 임포트

@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());
            throws new ServletException("list error");
        }
    }
}

 

> TodoService의 목록 기능 ( listAll() )

@Log4j2
public enum 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;
    }
}

 

> WEB-INF/todo/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>${dto}</li>
    </c:forEach>
</ul>
<body>
</html>

 

 등록

> TodoService의 등록 기능 ( register() )

@Log4j2
public enum TodoService {
	
    ...
    
    public void register(TodoDTO todoDTO) throws Exception {
        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
                
        log.info(todoVO);
        dao.insert(todoVO);
    }
}

 

> TodoRegisterController.

- GET과 POST 모두 사용

- 등록 기능은 GET 방식으로 화면을 보고 <form> 태그 내에 입력 항목들을 채운 후에 POST 방식으로 처리함. 처리 후에는 목록화면으로 redirect하는 PRG (Post-Redirect-Get) 패턴 방식

// Log4j2, TodoDTO, TodoService, Servlet 관련 패키지, Exception, LocalDate, 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");
    }
}

 

> WEB-INF/todo/register.jsp 코드

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

 

 조회

- 조회 기능은 GET 방식으로 동작하며 쿼리 스트링으로 번호를 전달. (번호는 DB에 저장된 일련 번호)

- Service에서 DTO 반환 후 컨트롤러에서 Request에 담아 JSP에 출력.

 

> TodoService의 등록 기능 ( get() )

@Log4j2
public enum TodoService {
	
    ...
    
    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

// Log4j2, TodoDTO, TodoService, Servlet 관련 패키지, Exception 임포트

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

 

> WEB-INF/todo/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>

 

> list.jsp 수정

- read 페이지로 가기 위한 링크 처리

<%@ 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>

 

수정 / 삭제

- 수정과 삭제는 보통 하나의 화면에 같이 존재하며 수정/삭제 모두 POST 방식으로 처리되므로 화면에 두 개의 <form> 태그를 작성해서 처리하거나 자바스크립트를 이용해서 <form> 태그의 action 속성을 변경해서 처리할 수 있음.

 

> TodoService의 수정/삭제 기능 ( remove(), modify() )

- remove()는 번호만 이용, modify()는 DTO 타입을 파라미터로 이용

@Log4j2
public enum TodoService {
	
    ...
    
    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

// Log4j2, TodoDTO, TodoService, Servlet 관련 패키지, Exception, LocalDate, 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");
        }
    }
    
}

↳ GET 방식으로 특정 TodoDTO를 보는 기능은 조회(Read) 기능과 동일

 

> WEB-INF/todo/modify.jsp 코드

<%@ 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>

↳ 2개의 <form> 태그 사용. 삭제의 경우 tno 값이 보이지 않도록 type을 hidden으로 설정.

 

> TodoModifyController doPost() 추가

@WebServlet(name = "todoModifyController", value = "/todo/modify")
@Log4j2
public class TodoModifyController extends HttpServlet {
	
   ...
   
    @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

// Log4j2, TodoDTO, TodoService, Servlet 관련 패키지, Exception 임포트

@WebServlet(name = "todoRemoveController", value = "/todo/remove")
@Log4j2
public class TodoRemoveController extends HttpServlet {
    
    private TodoService todoService = TodoService.INSTANCE;
    private final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    
    @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 CRUD 만들기 및 JDBC 결합 끝

☞ 작업한 코드 개선 사항

- 여러 개의 컨트롤러 : DAO나 Service와 달리 HttpServlet을 상속받는 컨트롤러가 여러개

- 동일한 로직 반복 : 조회와 수정 작업의 GET 방식은 코드가 같음.

- 예외 처리 부재 : 예외 처리에 대한 설계가 없음.

- 메서드 반복 호출 : req와 resp를 이용해서 DTO를 구성하는 작업이나 Long.parseLong() 같은 코드 등이 반복.

 

 

 

 

내용 - 자바 웹 개발 워크북 참조

반응형

댓글