프로젝트 내 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 이상의 레벨 사용)
> 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() 같은 코드 등이 반복.
내용 - 자바 웹 개발 워크북 참조
댓글