KOSA FullStack 교육/DB

KOSA fullstack 교육(JDBC 4단계 과정)

로미로미로 2025. 5. 22. 12:36

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


}

 

메서드 별로 실행하고 싶은 부분만 주석을 풀어서 실행해보면 결과를 알 수 있다.