11 게시판 만들기( JDBC 구현 )
2023.06.21 ~ 22 수업
교재 : 자바 웹 개발 워크북 2.2장 118p ~
● JDBC 구현
◎ Lombok 라이브러리
어노테이션(@)을 통해 getter\setter, toString, equals\hashCode, 생성자, 빌더와 같은 작업 처리가능
* 롬복의 필요성
- 이클립스의 generate를 통해 getter\setter를 작성하는 경우 멤버변수를 고치게 되면 직접 수정해주어야하지만 롬복을 이용하면 자동으로 관리가 됨
1) 롬복라이브러리 다운받기
https://projectlombok.org/download
Download
projectlombok.org
- C:\dev\ide\sts-4.18.0.RELEASE 경로에 파일 옮기기(롬복을 열어 경로 설정하기)
- 명령프롬프터 열고 cd 작성하고 오른쪽 마우스 누르면 복사한 텍스트가 붙여넣기 됨
2) 롬복라이브러리 이클립스(sts)에 등록
- 서블릿은 복붙해도 작동함
- 프로젝트 > src > webapp > web-inf > lib 경로에 붙여넣기
-> 안되면 프로젝트에서 빌드패스로 모듈패스에 롬복 추가하기
▶ 실습
* 패키지 분리하기 : org.zerock.jdbcex
- jdbcex 패키지 만들어 새로운 프로젝트가 될 것
▷ VO 클래스 작성
패키지명 : org.zerock.jdbcex.domain
- domain 추가
클래스명 : TodoVO
- 롬복과 변수 작성
- 롬복을 이용하여 반복적으로 생성하는 코드를 줄임
package org.zerock.jdbcex.domain;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter // 멤버변수의 getter속성함수 작성
@Builder // 멤버변수를 이용한 Builder 메소드 실행
@ToString // 멤버변수의 설명메소드 Object로부터 상속
public class TodoVO {
private long tno;
private String title;
private LocalDate dueDate; // 라이브러리 필요
private boolean finished;
}
- tbl_todo 테이블의 데이터를 자바 객체로 처리하기 위해서 테이블과 유사한 구조의 TodoVO클래스와 객체를 이용
- 읽기 전용(@Getter만 사용)
▷ HikariCP의 설정
* HikariCP란 가벼운 용량과 빠른 속도를 가지는 JDBC의 커넥션 풀 프레임워크
* 라이브러리 다운로드
https://mvnrepository.com/
- HikariCP검색하고 5.0.1ver bundle 다운로드
- 프로젝트 > src > webapp > web-inf > lib 경로에 붙여넣기
▷ Connection Pool 이용하기
- 히카리 컨넥션풀 설정파일 만들기
패키지명 : org.zerock.jdbcex
클래스명 : HikariCPTestApp
- HikariCP를 이용하기 위해서 HikariConfig 타입의 객체를 생성해 주어야 함
- HikariConfig는 Connection Pool을 설정하는데 있어 필요한 정보를 가지고 있는 객체로 이용해서 HikariDataSource 객체 생성
- HikariDataSource는 getConnection()을 제공하여 Connection객체를 얻어서 사용할 수 있게 된다.
package org.zerock.jdbcex;
import java.sql.Connection;
import java.sql.SQLException;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public class HikariCPTestApp {
public static void main(String[] args) 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");
// 히카리로 데이터소스 생성(단일 디비연결 방식 : 오라클, 마리아디비 등 상관없이 히카리로 연결관리)
// -> 히카리는 통합 JDBC 드라이버임
HikariDataSource ds = new HikariDataSource(config);
Connection connection = ds.getConnection(); // 컨넥션은 예외 발생
System.out.println("데이터소스에 연결 성공!");
System.out.println(connection);
// 데이터소스를 이용한 SQL작업...
// 데이터소스를 닫는 것이 아니고 데이터소스에 연결한 객체를 해제한다
// 데이터소스는 내장된 Connection객체를 닫는 것이 아니고 다른 연결을 기다린다(재사용)
// null의 상태가 되는 것으로 가비지콜렉터가 작동하지 않음
connection.close(); // 데이터소스와의 연결을 해제
System.out.println("데이터소스에 연결 분리!");
}
}
-> 기존과 동일하게 Connection을 얻어내지만 히카리를 통해 얻어옴을 알 수 있음
▷ TodoDAO
- 실제 SQL처리를 담당
- HikariDataSource를 이용
데이터정제
TodoService <--> TodoDAO <--> DB
○ ConnectionUtil 분리하여 만들기
패키지명 : org.zerock.jdbcex.dao
클래스명 : ConnectionUtil 클래스를 enum타입으로 만들기
- 싱글톤 패턴을 적용하여 언제든지 객체가 한번만 생성되도록 enum클래스에 INSTANCE 적용
-> TodoDAO에 필요한 작업을 수행할때 HikariDataSource를 이용하게 되는데 이에대한 처리를 쉽게 하기위해 만든 것
ConnectionUtil은 HikariConfig을 이용하여 하나의 HikariDataSource를 구성한다
구성된 HikariDataSource는 getConnection()을 통해 사용, 외부에서 Connection을 얻을 수 있도록 구성
* enum의 생성자는 private
package org.zerock.jdbcex.dao;
import java.sql.Connection;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
// 싱글톤 패턴을 적용하여 언제든지 객체가 한번만 생성되도록 enum클래스에 INSTANCE 적용
public enum ConnectionUtil {
// 싱글톤패턴 적용
INSTANCE;
// 멤버변수
private HikariDataSource ds;
ConnectionUtil() {
HikariConfig config = new HikariConfig();
// config 설정 데이터베이스 JDBC드라이버 등록
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();
}
}
○ TodoDAO클래스
패키지명 : org.zerock.jdbcex.dao
클래스명 : TodoDAO
-> ConnectionUtil을 사용하는 코드 추가
package org.zerock.jdbcex.dao;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.zerock.jdbcex.domain.TodoVO;
import lombok.Cleanup;
public class TodoDAO {
public String getTime() {
String now = null;
try {
Connection conn = ConnectionUtil.INSTANCE.getConnection();
PreparedStatement preparedStatement =
conn.prepareStatement("SELECT now();");
ResultSet rs = preparedStatement.executeQuery();
rs.next();
now = rs.getString(1);
}catch(Exception e) {
e.printStackTrace();
}
return now;
}
}
* (참고)try - with - resources 기능
- try()내에 선언된 변수들은 모두 Auto-Closeable이라는 인터페이스를 구현한 타입이어야함
- ()를 별도의 메소드로 분리가 가능함
- 참이면 {}를 실행하고 거짓이면 예외로 이동함
try (Connection conn = ConnectionUtil.INSTANCE.getConnection();
PreparedStatement preparedStatement =
conn.prepareStatement("SELECT now();");
ResultSet rs = preparedStatement.executeQuery();
){
rs.next();
now = rs.getString(1);
}catch(Exception e) {
e.printStackTrace();
}
▷ 실행
에러발생
1) NoClassDefFoundError: org/slf4j/LoggerFactory
-> 오픈소스 라이브러리인 slf4j(Simple Logging Facade for Java) 다운필요
https://mvnrepository.com/artifact/org.slf4j/slf4j-api 링크 접속하여 최신버전 var파일 다운
- C:\app\java\JavaWeb\src\main\webapp\WEB-INF\lib에 추가하기
2) SLF4J: No SLF4J providers were found.
provider : 기능을 제공하는 클래스, jdbc의 Driver같은 것
메이븐에서 slf4j-simple 검색
SLF4J Simple Binding ? 2.0.7 > jar 다운하여
- C:\app\java\JavaWeb\src\main\webapp\WEB-INF\lib에 추가하기
3) [main] INFO ~~ 정상실행시 나타내는 스택
※ sts(이클립스)개발시 라이브러리 적용순서
- JRE(프로젝트 생성) 자동추가
- 앱에 추가하는 법 : build Path
- 웹에 추가하는 법 : 1. Tomcat lib에 복사 | 2. WEP-INF/lib에 추가
* 반영이 안될 시 조치법
1. Refresh
2. sts 재시작
3. servers의 Tomcat의 Clean
(참고) JRE System Library가 없다면
sysout도 작동이 안됨(핵심 라이브러리)
-> add library(많이 사용하는 것으로 구성되어있음)에서 JRE System Library 추가하기
+ JSP와 Servlet은 WAS(Tomcat)이 있어야함
▷TodoDAO test하기
* 테스트 파일 만드는 법
src > java > other > test 검색
패키지 : org.zerock.jdbcex
JUnit Test Case : TodoDAOTestExApp 만들기
-> DAO를 테스트하는 클래스
-> 라이브러리를 자동으로 다운받을 수 있음
@Test : 테스트용 메소드임을 알림
test는 JUnit Test로 실행
package org.zerock.jdbcex;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.jdbcex.dao.TodoDAO;
import org.zerock.jdbcex.domain.TodoVO;
class TodoDAOTestExApp {
private TodoDAO todoDAO;
@BeforeEach // 테스트가 시작되기 전 새로 DAO객체를 만듦
// TodoDAO객체를 초기화 하는 작업
public void ready() {
todoDAO = new TodoDAO();
}
@Test
// TodoDAO의 멤버메소드 getTime() 동작확인
public void testGetTime() {
System.out.println(
"TodoDAO//getTime() : " + todoDAO.getTime());
}
}
▷ Lombok의 @Cleanup
- 변수에 @Cleanup을 추가하면 해당 메소드가 끝날 때 close()가 호출되는 것을 보장
-> try-catch의 역할을 해줌
TodoDAO에 getTime2() 메소드 추가
- @Cleanup 활용해보기
package org.zerock.jdbcex.dao;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.zerock.jdbcex.domain.TodoVO;
import lombok.Cleanup;
public class TodoDAO {
동일
public String getTime2() throws Exception {
String now = null;
// @Cleanup을 이용한 자동 close하기
// 예외가 발생할 구간이며 자원이 열려있는 경우
// 그열려진 자원 객체를 종료해준다
// try resouce 구문보다 간결
@Cleanup Connection conn =
ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement pstmt =
conn.prepareStatement("SELECT now();");
@Cleanup ResultSet rs = pstmt.executeQuery();
rs.next();
now = rs.getString(1);
return now;
}
}
▷ todoDAO의 등록기능 구현하기
* TodoVO객체를 데이터베이스에 추가하는 기능
- TodoDAO에 insert() 메소드 추가
- TodoVO의 @Builder 사용
@Builder : 한 항목에 같은 파라미터를 집어넣을때 간단하게 할 수 있게하는 기능
package org.zerock.jdbcex.dao;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.zerock.jdbcex.domain.TodoVO;
import lombok.Cleanup;
public class TodoDAO {
동일
public void insert(TodoVO vo) throws Exception {
// insert sql구현
String insertSql = """
INSERT INTO tbl_todo
(title, dueDate, finished)
VALUES ( ?, ?, ? );
""";
@Cleanup Connection conn =
ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement pstmt =
conn.prepareStatement(insertSql);
pstmt.setString(1, vo.getTitle());
pstmt.setDate(2, Date.valueOf(vo.getDueDate()));
pstmt.setBoolean(3, vo.isFinished());
pstmt.executeUpdate();
}
}
- insert() 는 파라미터로입력된 TodoVO객체를 이용해서 DML을 실행하니까 executeUpdate() 사용
- PreparedStatement는 "?"를 활용하여 나중에 전달할 데이터 set을 통해 실제 값 지정
-> index번호는 1부터 시작
* (참고)데이터 객체를 만들고나서 데이터에 변동이 없는 경우를 VO(Value Object)라고 부른다.
따라서 제일 처음 데이터객체를 생성할 방법이 필요합니다.
1) 생성자에 의한 방법
2) 모든 멤버변수를 파라미터로 전달받아 필요할 때 초기화 하는 setter 메소드
3) 빌더패턴
빌더패턴은 각속성을 설정할 때 같은 객체에 대하여 같은 형식의 메소드를 사용하므로 클래스이름으로 builder()를 만들고 각 멤버변수의 값을 할당해서 마지막 멤버변수의 값을 설정하고 build()함수를 호출하면 된다.
ex) TodoVO.builder().title("제목").dueDate("날짜").build();
▷ TodoDAO 목록 구현하기
- tbl_todo의 모든 데이터 가져오기
- TodoDAO에 selectAll() 메소드 추가
- 테이블의 각 행은 하나의 TodoVO객체
-> 모든 VO를 담기위해 List 활용
1. sql 문자열 작성
- 테스트블럭 활용
2. jdbc 연결객체 얻기(싱글톤 패턴) 및 sql 실행
- 롬복 활용
3. ResultSet객체는 마리아디비와 아직 연결되어 있어 사용 후 비워야함
-> List에 레코드를 복사하고 ResultSet객체를 종료하기
package org.zerock.jdbcex.dao;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.zerock.jdbcex.domain.TodoVO;
import lombok.Cleanup;
public class TodoDAO {
동일
/// 전체목록 가져오기 - List<TodoVO> selectAll()
public List<TodoVO> selectAll() throws Exception {
// 1. sql 문자열 작성
String sql = """
SELECT * FROM tbl_todo;
""";
// 2. jdbc 연결객체 얻기(싱글톤 패턴) 및 sql 실행
@Cleanup Connection conn = ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement pstmt = conn.prepareStatement(sql);
@Cleanup ResultSet rs = pstmt.executeQuery();
// 3. ResultSet객체는 마리아디비와 아직 연결되어 있어 사용 후 비워야 한다.
// List에 레코드를 복사하고 ResultSet객체를 종료하기
List<TodoVO> list = new ArrayList<>(); // 실체화시킬때는 타입을 안적어도 됨
// 실제 레코드를 list에 추가
// next() 다음 레코드 있는지 확인하고 있으면 계속 루프를 돈다
while(rs.next()) {
// 한개의 레코드를 가져와서 관련객체인 TodoVO객체에 추가한다
// 롬복을 사용하므로 builder패턴을 이용하며 TodoVO객체에 자료를 추가한다
TodoVO vo = TodoVO.builder()
.tno(rs.getLong("tno"))
.title(rs.getNString("title"))
.dueDate(rs.getDate("dueDate").toLocalDate())
.finished(rs.getBoolean("finished"))
.build();
// list에 레코드 하나 vo객체를 추가
// list : 레코드들
list.add(vo);
}
return list;
}
}
* List<> 는 인터페이스
-> 구현클래스(ArrayList, implements) 필요
* 제네릭프로그래밍<>
- 인터페이스와 구현클래스가 일치해야함
* selectAll()은 쿼리문이라 executeQuery() 활용
▷TodoDAO selectAll() 테스트
- TodoDAOTestExApp에 testList() 추가
package org.zerock.jdbcex;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.jdbcex.dao.TodoDAO;
import org.zerock.jdbcex.domain.TodoVO;
class TodoDAOTestExApp {
private TodoDAO todoDAO;
@BeforeEach // 테스트가 시작되기 전 새로 DAO객체를 만듦
// TodoDAO객체를 초기화 하는 작업
public void ready() {
todoDAO = new TodoDAO();
}
동일
@Test
public void testList() throws Exception {
List<TodoVO> list = todoDAO.selectAll();
System.out.println("TodoDAO.selectList() 테스트를 시작합니다,");
list.forEach(vo -> System.out.println(vo));
}
}
* List컬렉션의 중요한점
1. 인터페이스로 제네릭(Generic)을 사용하여 저장 모든 자료형에 동일한 코드 적용
2. 자체에 루프를 가지고 있음(절차적 프로그램을 작성할 필요가 없음)
코드의 추상화
3. 크기제한이 없다. 필요하면 자동증가 감소, 속도저하가 거의 없다
4. forEach()안에 처리할 기능(=함수)을 추가
* list는 배열 취급
▷ TodoDAO의 조회기능 구현하기
- 내가 원하는 부분만 추출
- where절 필요
- selectOne() : 특정번호가 파라미터가 되고 TodoVO가 리턴타입
package org.zerock.jdbcex.dao;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.zerock.jdbcex.domain.TodoVO;
import lombok.Cleanup;
public class TodoDAO {
동일
/// 게시물 하나 얻기
// TodoVO selectOne(tno)
public TodoVO selectOne(Long tno) throws Exception {
// 1. 질의할 sql
String sql = """
SELECT * FROM tbl_todo WHERE tno = ?;
""";
// 2. 마리아디비와 연결할 연결객체 생성
@Cleanup Connection con = ConnectionUtil.INSTANCE.getConnection();
// 3. 명령객체 생성
@Cleanup PreparedStatement pstmt = con.prepareStatement(sql);
// 3.5. (중요)질의하기전 검색조건(tno)값 설정
pstmt.setLong(1, tno);
// 4. 결과레코드 저장
@Cleanup ResultSet rs = pstmt.executeQuery();
// while을 사용하면 TodoVO vo = null을 밖으로 빼기
rs.next(); // 한 행의 데이터만 나옴
TodoVO vo = TodoVO.builder()
.tno(rs.getLong("tno"))
.title(rs.getNString("title"))
.dueDate(rs.getDate("dueDate").toLocalDate())
.finished(rs.getBoolean("finished"))
.build();
return vo;
}
}
▷TodoDAO testSelectOne() 테스트
- TodoDAOTestExApp에 testSelectOne() 추가
- 존재하는 번호를 이용하기
- 임시 게시물번호 = 3을 찾아라
package org.zerock.jdbcex;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.jdbcex.dao.TodoDAO;
import org.zerock.jdbcex.domain.TodoVO;
class TodoDAOTestExApp {
private TodoDAO todoDAO;
@BeforeEach // 테스트가 시작되기 전 새로 DAO객체를 만듦
// TodoDAO객체를 초기화 하는 작업
public void ready() {
todoDAO = new TodoDAO();
}
동일
@Test
public void testSelectOne() throws Exception {
// 정수상수는 Long에 넣지 못함 'L'작성하기
Long tno = 3L; // 반드시 테이블에 추가된 내용중에 있어야함
TodoVO vo = todoDAO.selectOne(tno);
System.out.println("" + tno + "게시물 내용 : " + vo);
}
}
▷ TodoDAO의 삭제/수정기능 구현하기
* deleteOne()
* updateOne()
- 특정한 번호(tno)가 필요 - where절
package org.zerock.jdbcex.dao;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.zerock.jdbcex.domain.TodoVO;
import lombok.Cleanup;
public class TodoDAO {
동일
/// 게시물 하나 삭제기능
// deleteOne(Long 게시물번호)
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(2, tno);
pstmt.executeUpdate();
}
/// 게시물 하나 수정기능
// updateOne(Long 게시물번호)
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();
}
}
▷TodoDAO testUpdateOne() 테스트
testUpdateOne()
package org.zerock.jdbcex;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zerock.jdbcex.dao.TodoDAO;
import org.zerock.jdbcex.domain.TodoVO;
class TodoDAOTestExApp {
private TodoDAO todoDAO;
@BeforeEach // 테스트가 시작되기 전 새로 DAO객체를 만듦
// TodoDAO객체를 초기화 하는 작업
public void ready() {
todoDAO = new TodoDAO();
}
동일
@Test
public void testUpdateOne() throws Exception {
TodoVO todoVO = TodoVO.builder()
.tno(1L)
.title("Sample Title....")
.finished(true)
.dueDate(LocalDate.of(2023, 6, 22))
.build();
todoDAO.updateOne(todoVO);
}
}