KOSA fullstack 교육(JDBC 4단계 과정)
JDBC 4단계
DAO클래스에서 CRUD를 구현해보자.
public class CustomDAO {
public CustomDAO() throws SQLException{
//디비서버 연결
Connection connection = DriverManager.getConnection(ServerInfo.URL, ServerInfo.USER, ServerInfo.PASS);
System.out.println("서버 연결");
// PreparedStatement 생성
String queryString = "INSERT INTO custom(id,name,address) VALUES(?,?,?)";
PreparedStatement ps1 = connection.prepareStatement(queryString);
//쿼리문 실행
ps1.setInt(1, 6);
ps1.setString(2, "쿠로미");
ps1.setString(3, "제주도");
System.out.println(ps1.executeUpdate() + "row 등록");
//DELETE
String queryDelete = "DELETE FROM custom WHERE id = ?";
PreparedStatement ps2 = connection.prepareStatement(queryDelete);
ps2.setInt(1, 4);
System.out.println(ps2.executeUpdate()+ "row 삭제 성공");
//UPDATE
String queryUpdate = "UPDATE custom SET name = ?, address = ? WHERE id = ?";
PreparedStatement ps3 = connection.prepareStatement(queryUpdate);
ps3.setString(1, "쿠로민");
ps3.setString(2, "방배동");
ps3.setInt(3, 6);
System.out.println(ps3.executeUpdate()+"row 업데이트 성공");
//SELECT
String querySelect = "SELECT id, name, address FROM custom WHERE id = ?";
PreparedStatement ps4 = connection.prepareStatement(querySelect);
ps4.setInt(1, 6);
//여기서 중요한 점. SELECT 문은 실행 시 executeQuery를 사용
ResultSet rs = ps4.executeQuery();
//next는 아래 열로 순차적으로, get은 해당하는 행을 뽑아냄.
if(rs.next())
System.out.println(rs.getInt(1)+
" , "+rs.getString(2)+
" , "+rs.getString(3));
String queryAllSelect = "SELECT id, name, address FROM custom";
PreparedStatement ps5 = connection.prepareStatement(queryAllSelect);
ResultSet rs1 = ps5.executeQuery();
while(rs1.next())
System.out.println(rs1.getInt(1)+
" , "+rs1.getString(2)+
" , "+rs1.getString(3));
}
}
개발자들이 가장 오류가 많이 나는 부분이 쿼리문 부분이다.
무조건 쿼리문 하나 생성 시 테스트를 해야한다. 이것이 단위테스트 개념이다.
SELECT 문은 DML문 과는 다른 로직으로 돌아간다.
쿼리문 종류 | 메서드 | 반환값 |
SELECT | executeQuery | ResultSet(조회 결과 집합) |
UPDATE/DELETE/INSERT | executeUpdate | int (조작 완료된 행의 수) |
ResultSet 구조
[ BeforOfElement ]
[ 6, 쿠로민, 방배동 ]
...데이터가 더 있다면 여기에 쭉 담김
[ EndOfElement ]
-> 마우스 커서가 데이터의 하나 전 열을 가리키면서 시작한다는 것이 중요하다
ResultSet 다루는 방법
- rs.next() 다음 행이 존재하는 지 확인하면서 이동 (rs는 ResultSet 객체)
- rs.getXXX("컬럼명") 또는 rs.getXXX(열 번호) 해당 데이터의 컬럼을 꺼냄
이제 본격적으로 JDBC 4단계를 구현한다.
다음과같이 폴더 구조를 생성하고, db.properties에 DB서버 정보를 담는다.
왜 확장자가 properties일까?
Java에서 설정 파일 형식으로 표준화된 확장자이기 때문,
.properties는 key = value 형태의 설정을 저장하는 전통적인 포맷이다.
properties의 환경 변수들을 가져와서 사용해보자.
다음은 Test 코드이다.
public class CustomDAOTest {
public static String DRIVER_NAME;
public static String USER;
public static String URL;
public static String PASS;
public static void main(String[] args) {
try {
new CustomDAO(DRIVER_NAME, URL, USER, PASS);
}catch(Exception e) {
System.out.println(e.getMessage());
}
}
//properties 모든 값을 로드하고 자바 코드가 본격적으로 실행되도록 한다.
static {
try {
Properties p = new Properties();
p.load(new FileInputStream("src/config/db.properties"));
DRIVER_NAME = p.getProperty("jdbc.mysql.driver");
URL = p.getProperty("jdbc.mysql.url");
USER = p.getProperty("jdbc.mysql.user");
PASS = p.getProperty("jdbc.mysql.pass");
}catch(Exception e) {
System.out.println(e.getMessage());
}
}
}
FileInputStream 사용 시 왜 src 경로를 넣어줬나 ?
파일 경로(예: /file/path/...)는 Java 클래스가 인식할 수 있는 경로가 아니다.
그러니까 src 경로(= 클래스패스) 하위에 넣고 불러와야 Java에서 쉽게 읽을 수 있다.
이제 DAO 클래스를 작성한다.
public class CustomDAO {
public CustomDAO(String driver_name, String url, String user, String pass) throws Exception {
//1. 드라이버 로딩
Class.forName(driver_name);
System.out.println("드라이버 로딩 성공");
//2. 디비서버 연결
Connection connection = DriverManager.getConnection(url, user, pass);
System.out.println("드라이버 연결 성공");
//3. PreparedStatement 생성
String querySelect = "SELECT id,name,address FROM custom";
PreparedStatement ps = connection.prepareStatement(querySelect);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
System.out.println(rs.getInt("id") + "\t" +
rs.getString("name") + "\t" +
rs.getString("address"));
}
}
}
하지만 여기서 불편한 점이 보여야 된다.
우리는 실제값을 properties파일에 넣는 습관을 들여야 한다.
쿼리문을 모두 properties 파일에 넣어보자.
custom.properties
###### sql query #####
jdbc.sql.selectAll = SELECT id, name, address FROM custom
jdbc.sql.select = SELECT id, name, address FROM custom where id = ?
jdbc.sql.insert = INSERT INTO custom(id,name,address) VALUES(?,?,?)
jdbc.sql.delete = DELETE FROM custom WHERE id = ?
jdbc.sql.update = UPDATE custom SET name = ?, address = ?, WHERE id = ?
이제 4단계 구현은 마무리 되었는데 한 가지가 추가로 시행되어야 한다.
자원 반납!!
디비 서버를 연결할 때 Connection을 반환 받았고, PreparedStatement 객체를 생성했고, ResultSet 객체도 생성했다.
이것을 안닫아주면 간단하게 회사가 망한다고 볼 수 있다.
닫아주어야 하는 이유는 구체적으로 다음과 같다.
1. 메모리 누수 (Memory Leak)
- JDBC 객체들은 네이티브 자원(DB 커넥션, 포트, 소켓 등)을 사용한다
- 안 닫으면 GC(Garbage Collector)도 회수하지 못하고 계속 메모리에 쌓임
2. DB 커넥션 고갈 (Connection Leak)
- DB는 동시에 연결 가능한 Connection 수에 **제한(Connection Pool 제한)**이 있다.
- 닫지 않으면 계속 열려 있어서 다른 사용자들이 DB 연결을 못 하게 됨
3. 애플리케이션 성능 저하 및 다운
- 자원들이 회수되지 않으면 시스템이 점점 느려지고
- 서버 전체가 중단되거나 DB가 먹통이 될 수 있음
DAO 클래스에 자원 반납 코드를 추가해서 다시 완성시켜 보자.
public class CustomDAO {
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
public CustomDAO(String driver_name, String url, String user, String pass) throws Exception {
try {
// 1. 드라이버 로딩
Class.forName(driver_name);
System.out.println("드라이버 로딩 성공");
// 2. 디비서버 연결
connection = DriverManager.getConnection(url, user, pass);
System.out.println("드라이버 연결 성공");
// 3. PreparedStatement 생성
String querySelect = "SELECT id, name, address FROM custom";
ps = connection.prepareStatement(querySelect);
rs = ps.executeQuery();
while (rs.next()) {
System.out.println(rs.getInt("id") + "\t" + rs.getString("name") + "\t" + rs.getString("address"));
}
} finally {
rs.close();
ps.close();
connection.close();
}
}
}
CustomDAO
위의 메모 중 중요한 점
엔터티 = VO 클래스 = 레코드클래스
단위 테스트 = 메서드 단위 테스트 = 서비스 단위 테스트 = 커넥션 단위 테스트
작업의 순서대로 작업해보자 !!
우선 config 먼저 생성해주고
package config;
public interface ServerInfo {
//public static final
public static final String DRIVER_NAME="com.mysql.cj.jdbc.Driver";
public static final String URL = "jdbc:mysql://127.0.0.1:3306/kosa?serverTimezone=Asia/Seoul&useSSL=false&useUnicode=true&characterEncoding=UTF-8";
public static final String USER="root";
public static final String PASS="12341234";
}
Custom Class 생성 해주자
이 때 컬럼명과 필드명이 다를 때는 무조건 주석 처리 해줘야 한다.
package com.jdbc.vo;
public class Custom {
private int id;
private String name; //컬럼명은 cust_name!!!
private String address;
public Custom() {}
public Custom(int id, String name, String address) {
super();
this.id = id;
this.name = name;
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Custom [id=" + id + ", name=" + name + ", address=" + address + "]";
}
}
그 다음, 기능 템플릿 역할인 DAO 인터페이스를 생성한다
public interface CustomDAO {
//DML
void addCustom(Custom custom) throws SQLException;
void removeCustom(int id) throws SQLException;
void updateCustom(Custom custom) throws SQLException;
//SELECT(overloading)
Custom getCustom(int id) throws SQLException;
List<Custom> getCustom() throws SQLException;
}
DAO인터페이스와 DAOImpl 클래스는 왔다갔다 하면서 작업하면 된다.
DAOImpl 클래스를 작성하기 전에, 주석으로 메서드안에서 해야하는 로직을 적어보자.
다음과 같이 모든 메서드가 비슷한 로직으로 돌아가는 것을 볼 수 있다.
그렇다면 메서드로 빼는 작업을 해보자
1. 싱글톤 작업
2. 공통 로직 작성
private static CustomDAOImpl daoImpl = new CustomDAOImpl();
private CustomDAOImpl() {
System.out.println("singletone,..Creating...");
}
public static CustomDAOImpl getInstanse() {
return daoImpl;
}
//////////// 공통로직//////////////
public Connection getConnect() throws SQLException {
// 여기선 Exception 보다 명료하게 SQLException이 더 좋다
return DriverManager.getConnection(ServerInfo.URL, ServerInfo.USER, ServerInfo.PASS);
}
public void closeAll(PreparedStatement ps, Connection conn) throws SQLException {
if (ps != null)
ps.close();
if (conn != null)
conn.close();
}
public void closeAll(ResultSet rs, PreparedStatement ps, Connection conn) throws SQLException {
if (rs != null)
rs.close();
closeAll(ps, conn);
}
완료가 되었다면 이제 메서드를 작성해야하는데, addCustom 메서드를 작성하다가 null 값을 던져주어야 하는 상황에 부딪혔다.
null을 던져주는 곳이 많기 때문에, closeAll()을 오버로딩 한다 ~
그러므로 완료된 DAOImpl 클래스 코드는 다음과 같다.
public class CustomDAOImpl implements CustomDAO {
private static CustomDAOImpl daoImpl = new CustomDAOImpl();
private CustomDAOImpl() {
System.out.println("singletone,..Creating...");
}
public static CustomDAOImpl getInstanse() {
return daoImpl;
}
//////////// 공통로직//////////////
public Connection getConnect() throws SQLException {
// 여기선 Exception 보다 명료하게 SQLException이 더 좋다
return DriverManager.getConnection(ServerInfo.URL, ServerInfo.USER, ServerInfo.PASS);
}
public void closeAll(PreparedStatement ps, Connection conn) throws SQLException {
if (ps != null)
ps.close();
if (conn != null)
conn.close();
}
public void closeAll(ResultSet rs, PreparedStatement ps, Connection conn) throws SQLException {
if (rs != null)
rs.close();
closeAll(ps, conn);
}
///////////// 비지니스로직///////////
@Override
public void addCustom(Custom custom) throws SQLException { // 회원가입
Connection connection = null;
PreparedStatement ps = null;
// 1. 디비 연결... Connection 반환
try {
connection = getConnect();
// 2. PreparedStatement 생성... SQL문 인자값
String queryString = "INSERT INTO custom (id, name, address) VALUES(?,?,?)";
ps = connection.prepareStatement(queryString);
// 3. 값 바인딩 및 쿼리문 실행
ps.setInt(1, custom.getId());
ps.setString(2, custom.getName());
ps.setString(3, custom.getAddress());
System.out.println(ps.executeUpdate() + "명 등록 성공");
} finally {// 4. 자원 반납
closeAll(null, ps, connection);
}
}
@Override
public void removeCustom(int id) throws SQLException { // 회원 탈퇴
Connection connection = null;
PreparedStatement ps = null;
try {
connection = getConnect();
String queryString = "DELETE FROM custom WHERE id = ?";
ps = connection.prepareStatement(queryString);
ps.setInt(1, id);
System.out.println(ps.executeUpdate()==1 ? "삭제 성공" : "삭제 실패");
} finally {
closeAll(ps, connection);
}
}
@Override
public void updateCustom(Custom custom) throws SQLException{
Connection connection = null;
PreparedStatement ps = null;
try {
connection = getConnect();
String queryString = "UPDATE custom SET name = ?, address = ? WHERE id = ?";
ps = connection.prepareStatement(queryString);
ps.setString(1, custom.getName());
ps.setString(2, custom.getAddress());
ps.setInt(3, custom.getId());
System.out.println(ps.executeUpdate() + "명 수정 성공");
} finally {
closeAll(ps, connection);
}
}
@Override
public Custom getCustom(int id) throws SQLException{
Custom customer = null;
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
connection = getConnect();
String queryString = "SELECT id, name, address FROM custom WHERE id = ?";
ps = connection.prepareStatement(queryString);
ps.setInt(1, id);
rs = ps.executeQuery();
if(rs.next()) {
customer = new Custom(rs.getInt("id"), rs.getString("name"), rs.getString("address"));
}
} finally {
closeAll(rs ,ps, connection);
}
return customer;
}
@Override
public List<Custom> getCustom() throws SQLException{
List<Custom> customers = new ArrayList<>();
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
connection = getConnect();
String queryString = "SELECT id, name, address FROM custom";
ps = connection.prepareStatement(queryString);
rs = ps.executeQuery();
while (rs.next()) {
customers.add(new Custom(rs.getInt("id"), rs.getString("name"), rs.getString("address")));
}
} finally {
closeAll(rs ,ps, connection);
}
return customers;
}
}
Test 코드는 한 쿼리문이 끝날 때 마다,
즉 한 단위(기능), 서비스, 커넥션이 끝날 때 마다
테스트에서 돌려봐야 한다.
CustomDAOTest.java
public class CustomDAOTest {
public static void main(String[] args) {
CustomDAOImpl dao = CustomDAOImpl.getInstanse();
// try {
// dao.addCustom(new Custom(4, "채니","사당"));
// } catch (SQLException e) {
// System.out.println(e.getMessage());
// }
// try {
// System.out.print(dao.getCustom());
// } catch (SQLException e) {
// System.out.println(e.getMessage());
// }
// try {
// dao.removeCustom(4);
// }catch(SQLException e) {
// System.out.println(e.getMessage());
// }
// try {
// dao.updateCustom(new Custom(1, "시나모롤", "이수"));
// }catch(SQLException e) {
// System.out.println(e.getMessage());
// }
}
static {
try {
Class.forName(ServerInfo.DRIVER_NAME);
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
메서드 별로 실행하고 싶은 부분만 주석을 풀어서 실행해보면 결과를 알 수 있다.