본문 바로가기
프로그래밍

[JAVA] DB에 접근하는 유틸 코드 분석

by youngbamer 2021. 9. 28.
반응형

JAVA에서 DB에 접근하는 유틸 코드를 작성해보았다.

public class JDBCInsertTest {

	public static void main(String[] args) {
		Connection conn = null;
		PreparedStatement pstat = null;

		try {
			
			String url = "jdbc:mysql://127.0.0.1:3306/text_board?ServerTimeZone=UTC";
			
			Class.forName("com.mysql.cj.jdbc.Driver");
			conn = DriverManager.getConnection(url, "root", "");
			
			String sql = "INSERT INTO article";
			sql += " SET regDate = NOW()";
			sql += ", updateDate = NOW()";
			sql += ", title = CONCAT(\"제목\",RAND())";
			sql += ", `body` = CONCAT(\"내용\",RAND());";
			
			pstat = conn.prepareStatement(sql);
			int affectedRows = pstat.executeUpdate();

		} catch (ClassNotFoundException e) {
			
			
			System.out.println("드라이버 로딩 실패");
		} catch (SQLException e) {
			System.out.println("에러: " + e);
		} finally {
			try {
				if (pstat != null && !pstat.isClosed()) {
					pstat.close();
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
			
			try {
				if (conn != null && !conn.isClosed()) {
					conn.close();
				}
			}
			catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
}

 

위와 같이 짰는데 이 코드는 몇가지 문제가 있다.

일단 시스템을 실행할떄마다 DB에 접속되므로 프로그램의 부하가 있고,

SQL 문에 변수가 들어간다면 SQL Injection 이라는 해킹 공격의 위험에도 노출된다.

따라서 효율적으로 SQL에 접근할 수 있고 해킹의 공격에도 안전한 유틸 코드를 다운로드받았다.

public class DBUtil {
	public static Map<String, Object> selectRow(Connection dbConn, SecSql sql) {
		List<Map<String, Object>> rows = selectRows(dbConn, sql);

		if (rows.size() == 0) {
			return new HashMap<>();
		}

		return rows.get(0);
	}

	public static List<Map<String, Object>> selectRows(Connection dbConn, SecSql sql) throws SQLErrorException {
		List<Map<String, Object>> rows = new ArrayList<>();

		PreparedStatement stmt = null;
		ResultSet rs = null;

		try {
			stmt = sql.getPreparedStatement(dbConn);
			rs = stmt.executeQuery();
			ResultSetMetaData metaData = rs.getMetaData();
			int columnSize = metaData.getColumnCount();

			while (rs.next()) {
				Map<String, Object> row = new HashMap<>();

				for (int columnIndex = 0; columnIndex < columnSize; columnIndex++) {
					String columnName = metaData.getColumnName(columnIndex + 1);
					Object value = rs.getObject(columnName);

					if (value instanceof Long) {
						int numValue = (int) (long) value;
						row.put(columnName, numValue);
					} else if (value instanceof Timestamp) {
						String dateValue = value.toString();
						dateValue = dateValue.substring(0, dateValue.length() - 2);
						row.put(columnName, dateValue);
					} else {
						row.put(columnName, value);
					}
				}

				rows.add(row);
			}
		} catch (SQLException e) {
			throw new SQLErrorException("SQL 예외, SQL : " + sql, e);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
					throw new SQLErrorException("SQL 예외, rs 닫기, SQL : " + sql, e);
				}
			}

			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					throw new SQLErrorException("SQL 예외, stmt 닫기, SQL : " + sql, e);
				}
			}
		}

		return rows;
	}

	public static int selectRowIntValue(Connection dbConn, SecSql sql) {
		Map<String, Object> row = selectRow(dbConn, sql);

		for (String key : row.keySet()) {
			return (int) row.get(key);
		}

		return -1;
	}

	public static String selectRowStringValue(Connection dbConn, SecSql sql) {
		Map<String, Object> row = selectRow(dbConn, sql);

		for (String key : row.keySet()) {
			return (String) row.get(key);
		}

		return "";
	}

	public static boolean selectRowBooleanValue(Connection dbConn, SecSql sql) {
		Map<String, Object> row = selectRow(dbConn, sql);

		for (String key : row.keySet()) {
			return ((int) row.get(key)) == 1;
		}

		return false;
	}

	public static int insert(Connection dbConn, SecSql sql) {
		int id = -1;

		PreparedStatement stmt = null;
		ResultSet rs = null;

		try {
			stmt = sql.getPreparedStatement(dbConn);
			stmt.executeUpdate();
			rs = stmt.getGeneratedKeys();

			if (rs.next()) {
				id = rs.getInt(1);
			}

		} catch (SQLException e) {
			throw new SQLErrorException("SQL 예외, SQL : " + sql, e);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
					throw new SQLErrorException("SQL 예외, rs 닫기, SQL : " + sql, e);
				}
			}

			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					throw new SQLErrorException("SQL 예외, stmt 닫기, SQL : " + sql, e);
				}
			}

		}

		return id;
	}

	public static int update(Connection dbConn, SecSql sql) {
		int affectedRows = 0;

		PreparedStatement stmt = null;

		try {
			stmt = sql.getPreparedStatement(dbConn);
			affectedRows = stmt.executeUpdate();
		} catch (SQLException e) {
			throw new SQLErrorException("SQL 예외, SQL : " + sql, e);
		} finally {
			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					throw new SQLErrorException("SQL 예외, stmt 닫기, SQL : " + sql, e);
				}
			}
		}

		return affectedRows;
	}

	public static int delete(Connection dbConn, SecSql sql) {
		return update(dbConn, sql);
	}
}

 

 

위 코드를 가지고 코드 스터디 바로 시작!

SelectRow - SELECT 쿼리를 SQL로 받으며 DB에서 해당하는 데이터 한 줄을 가져온다.

SelectRows - SELECT 쿼리를 SQL로 받으며 DB에서 해당하는 데이터 전체를 가져온다.

selectRowIntValue - SelectRow에서 정수로 데이터를 가져온다. 단, 정수를 출력하는 SQL 쿼리를 작성해야 한다.

selectRowStringValue - SelectRow에서 문자열로 데이터를 가져온다. 단, 문자열을 출력하는 SQL 쿼리를 작성해야 한다.

selectRowBooleanValue - SelectRow에서 True/False로 데이터를 가져온다.

insert - INSERT 쿼리를 SQL로 받으며 DB에 데이터를 추가한다.

update - UPDATE 쿼리를 SQL로 받으며 DB에 데이터를 수정한다.

delete - DELETE 쿼리를 SQL로 받으며 DB에서 데이터를 삭제한다. 기능은 update와 동일하다.

 

오늘은 가장 중요한 SelectRow 함수를 공부해보려고 한다.

예제 getArticles() 코드 리뷰
public List<Article> getArticles() {
		
		List<Article> articles = new ArrayList<>();
		
		SecSql sql = new SecSql();

		sql.append("SELECT *");
		sql.append("FROM article");
		sql.append("ORDER BY id DESC");

		List<Map<String, Object>> articlesListMap = DBUtil.selectRows(Container.conn, sql);

		for (Map<String, Object> articleMap : articlesListMap) {
			articles.add(new Article(articleMap));
		}
		
		return articles;
	}

 

SecSql 객체를 이용해 sql 변수를 만들고 select 문을 append 해준다.

 

public class SecSql {
	private StringBuilder sqlBuilder;
	private List<Object> datas;

	@Override
	public String toString() {
		return "sql=" + getFormat() + ", data=" + datas;
	}

	public SecSql() {
		sqlBuilder = new StringBuilder();
		datas = new ArrayList<>();
	}

	public boolean isInsert() {
		return getFormat().startsWith("INSERT");
	}

	public SecSql append(Object... args) {
		if (args.length > 0) {
			String sqlBit = (String) args[0];
			sqlBuilder.append(sqlBit + " ");
		}

		for (int i = 1; i < args.length; i++) {
			datas.add(args[i]);
		}

		return this;
	}

	public PreparedStatement getPreparedStatement(Connection dbConn) throws SQLException {
		PreparedStatement stmt = null;

		if (isInsert()) {
			stmt = dbConn.prepareStatement(getFormat(), Statement.RETURN_GENERATED_KEYS);
		} else {
			stmt = dbConn.prepareStatement(getFormat());
			// getFormat은 StringBuilder의 toString();
		}

		for (int i = 0; i < datas.size(); i++) {
			Object data = datas.get(i);
			int parameterIndex = i + 1;

			if (data instanceof Integer) {
				stmt.setInt(parameterIndex, (int) data);
			} else if (data instanceof String) {
				stmt.setString(parameterIndex, (String) data);
			}
		}

		return stmt;
	}

	public String getFormat() {
		return sqlBuilder.toString();
	}

	public static SecSql from(String sql) {
		return new SecSql().append(sql);
	}
}

 

SecSql 클래스 코드는 위와 같다.

차근차근 코드를 분석해보자.

 

일단 가장 먼저 SecSql 인스턴스 sql 을 생성하면 하는 행위가 append이다.

따라서 appen 함수를 살펴보자.

 

public SecSql append(Object... args) {
		if (args.length > 0) {
			String sqlBit = (String) args[0];
			sqlBuilder.append(sqlBit + " ");
		}

		for (int i = 1; i < args.length; i++) {
			datas.add(args[i]);
		}

		return this;
	}

appen 함수를 살펴보면 Object... args 는 함수의 인자를 받을때마다 args 배열로 받는다는 의미이다.

예를 들어 인자를 한개만 받으면 args[0] 으로, 2개 이상 받으면 args[0], args[1] ... 식으로 늘어난다.

 

따라서 sql.append("SELECT * FROM article")이라면 args[0] = "SELECT * FROM article"

그리고 sql.append("SELECT * FROM article WHERE id = ?", id) 라면

 

args[0] = SELECT * FROM article WHERE id = ?

args[1] = id

 

두번째 sql을 사례로 보면

if (args.length > 0) {
			String sqlBit = (String) args[0];
			sqlBuilder.append(sqlBit + " ");
		}

이므로 sqlBuilder에 첫번째 값인 SELECT * FROM article WHERE id = ? 가 String으로 sqlBuilder에 들어간다.

그리고 공백이 추가된다.

 

그 다음 args[1]은 

for (int i = 1; i < args.length; i++) {
			datas.add(args[i]);
		}

로 들어가서 datas 리스트에 추가되게 된다.

 

즉 sql은 sqlBuilder와 datas를 갖게 된다.

if (isInsert()) {
			stmt = dbConn.prepareStatement(getFormat(), Statement.RETURN_GENERATED_KEYS);
		} else {
			stmt = dbConn.prepareStatement(getFormat());
			// getFormat은 StringBuilder의 toString();
		}

그리고 prepareStatement에 getFormat() 인자를 넘겨준다.

public String getFormat() {
		return sqlBuilder.toString();
	}

getFormat은 sqlBuilder를 String으로 변환한 값을 리턴한다.

그러면 preparStatment에 의해 ? 인자는 set을 통해 값을 매핑시킬 수 있다.

 

for (int i = 0; i < datas.size(); i++) {
			Object data = datas.get(i);
			int parameterIndex = i + 1;

			if (data instanceof Integer) {
				stmt.setInt(parameterIndex, (int) data);
			} else if (data instanceof String) {
				stmt.setString(parameterIndex, (String) data);
			}
		}

그 코드가 바로 위와 같다.

stmt.set 을 통해 데이터를 매핑시켜준 후 최종적으로 prepareStatement 값을 리턴한다.

 

SelectRows 코드 리뷰
public static List<Map<String, Object>> selectRows(Connection dbConn, SecSql sql) throws SQLErrorException {
		List<Map<String, Object>> rows = new ArrayList<>();

		PreparedStatement stmt = null;
		ResultSet rs = null;

		try {
			stmt = sql.getPreparedStatement(dbConn);
			rs = stmt.executeQuery();
			ResultSetMetaData metaData = rs.getMetaData();
			int columnSize = metaData.getColumnCount();

			while (rs.next()) {
				Map<String, Object> row = new HashMap<>();

				for (int columnIndex = 0; columnIndex < columnSize; columnIndex++) {
					String columnName = metaData.getColumnName(columnIndex + 1);
					Object value = rs.getObject(columnName);

					if (value instanceof Long) {
						int numValue = (int) (long) value;
						row.put(columnName, numValue);
					} else if (value instanceof Timestamp) {
						String dateValue = value.toString();
						dateValue = dateValue.substring(0, dateValue.length() - 2);
						row.put(columnName, dateValue);
					} else {
						row.put(columnName, value);
					}
				}

				rows.add(row);
			}
		} catch (SQLException e) {
			throw new SQLErrorException("SQL 예외, SQL : " + sql, e);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
					throw new SQLErrorException("SQL 예외, rs 닫기, SQL : " + sql, e);
				}
			}

			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					throw new SQLErrorException("SQL 예외, stmt 닫기, SQL : " + sql, e);
				}
			}
		}

		return rows;
	}

 

해석을 해보면

1. re에 쿼리문을 실행하여 데이터를 가져오면 re.next()를 이용해 rs값이 있을때마다 while문을 실행한다.

2. Map으로 선언된 row에 각각 칼럼과 값을 메타데이터별로 나누어 put으로 매핑시킨 뒤 List로 선언된 row에 row를 add한다.

 

DB 유틸 코드는 앞으로 계속해서 유용하게 사용할 수 있을것 같다.

이렇게 해서 코드 스터디 끝!

반응형