MariaDB 설치
-homebrew를 이용해 설치
터미널
brew install mariadb
git must be installed and in your path! 에러
xcode-select --install
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 프로그램 작성 순서
- 네트워크를 통해서 DB와 연결을 맺는다.
- DB에 보낼 SQL을 작성하고 전송
- DB가 응답한 결과를 받아서 처리
- 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
- java.sql.Connection
- DB와 네트워크상 연결을 통해, SQL을 실행할 수 있는 객체 생성
- 반드시 성립된 연결은 try~catch~finally / try-with-resources를 이용해 close()
- java.sql.Statement/PreparedStatement
- SQL을 DB로 보내기 위한 타입
- setXXX()
- executeUpdate()
- executeQuery()
- SQL을 DB로 보내기 위한 타입
- 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에 데이터 전달
- 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("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를 이용해 책임과 역할의 구분한 코드 작성
-개선해야할 사항들
- 여러 개 컨트롤러 작성하는 번거로움
- 동일한 로직 반복 사용
- 예외 처리 부재
- 반복 메소드 호출
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
4장-1. 스프링과 스프링 Web MVC (1) | 2022.11.29 |
---|---|
3장. 세션과 필터, 쿠키와 리스너 (+ 한글 깨짐 처리 / Optional<> / 옵저버 패턴) (0) | 2022.11.27 |
1장. 웹 프로그래밍 시작 (0) | 2022.11.24 |
[Spring 부트 - 운동 클럽 프로젝트] 2. 소셜 로그인 연동 (0) | 2022.10.19 |
[Spring 부트 - 운동 클럽 프로젝트] 1. 스프링 시큐리티 연동 (2) CSRF 와 접근 제한 설정 (0) | 2022.10.19 |