애매한 잡학사전

java에서 Database 접속 구현하기 with 인터페이스 본문

DEV/JAVA

java에서 Database 접속 구현하기 with 인터페이스

거대한 개발자 2022. 9. 13. 10:51
반응형

데이터베이스에 있는 데이터를 조회해서 AWS Athena에 저장하는 Java 배치 프로그램을 만들면서 DB manager를

구현했는데 정리해 보려고 합니다. 바로 클래스를 생성해서 구현할 수 있지만 이번에는 의존성 주입(DI : Dependency Injection) 연습을 위해 인터페이스로 생성했습니다.

 

제가 정리할 내용은 다음과 같습니다.

- DB manager 인터페이스 생성

- DB manager 클래스 생성 및 인터페이스의 abstract method 구현


DB manager 인터페이스 생성

의존성 주입을 위해 인터페이스를 생성합니다.

처음 생각했을 때는 필요한 method는 connection, close 이렇게 2개였지만 조금 더 생각을 하다 보니

기본적인 CRUD 기능을 별도의 클래스로 구현하는 것보다 인터페이스에 넣어서 DB manager 객체를 생성하고

호출하는 곳에서는 CRUD method를 호출, 호출한 method 내부에서 connection 생성 후 CRUD를 처리하면

될 것 같았습니다. 그래서 기본적인 SELECT, INSERT, UPDATE, DELETE method를 생성합니다.

SELECT method는 파라미터가 있는 것과 없는 것 2가지로 생성하였습니다. 

 

/**
 * DB connection
 * @return Connection
 */
Connection getConnection();

/**
 * close
 * @param rs resultSet
 */
void close(ResultSet rs);

/**
 * close
 * @param stmt PreparedStatement
 */
void close(PreparedStatement stmt);

/**
 * select query 실행
 * @param query select query
 * @return select 결과 리턴
 */
List<Map<String, Object>> select(String query);

/**
 * select query 실행 with Param
 * @param query select query
 * @param paramVal  parameter
 * @return 조회 내용
 */
List<Map<String, Object>> select(String query, Map<String, String> paramVal);

/**
 * insert query 실행
 * @param query insert query
 * @param paramVal value
 * @return insert count
 */
int insert(String query, Map<String, String> paramVal);

/**
 * update query 실행
 * @param query update query
 * @param paramVal value
 * @return update count
 */
int update(String query, Map<String, String> paramVal);

/**
 * delete query 실행
 * @param query delete query
 * @param paramVal value
 * @return delete count
 */
int delete(String query, Map<String, String> paramVal);

DB manager 클래스 생성 및 인터페이스의 abstract method 구현

이제 이 인터페이스를 구현한 클래스를 생성합니다.

인터페이스에 있는 모든 method를 생성해주고 method body를 작성합니다.

 

먼저 default 생성자입니다. 

private final String dbUser;
private final String dbPassword;
private final String databaseName;

public DatabaseManager(){
    this.dbUser = StringUtils.getProperty("Globals.UserName");
    this.dbPassword = StringUtils.getProperty("Globals.Password");
    this.databaseName = StringUtils.getProperty("Globals.DbName");
}

호출하는 곳에서 객체 생성을 하면 property에 있는 DB명, user명, password를 가져와서 전역 변수에 담아 놓습니다.


다음은 connection method입니다.

@Override
public Connection getConnection(){
    Connection conn = null;

    try {
        Class.forName(StringUtils.getProperty("Globals.DriverClassName"));
        String dbUrl = StringUtils.getProperty("Globals.Url")+databaseName
            +";encrypt=true;trustServerCertificate=true";
        conn = DriverManager.getConnection(dbUrl, dbUser, dbPassword);
    } catch (ClassNotFoundException classNotFoundException) {
        logger.error("ClassNotFoundException : " + classNotFoundException.getMessage());
    } catch (SQLException sqlException){
        logger.error("SQLException : " + sqlException.getMessage());
    } catch(Exception exception){
        logger.error("exception : " + exception.getMessage());
    }

    return conn;
}

property에서 DriverClassName과 DB url 정보를 가져와서 connection을 생성하는 소스코드를 작성했습니다. 


이제 close method를 구현하겠습니다.

@Override
public void close(ResultSet rs){
    try{ if(rs != null) rs.close(); } catch(Exception ignored){}
}

@Override
public void close(PreparedStatement stmt){
    try{ if(stmt != null) stmt.close(); } catch(Exception ignored){}
}

close method는 try-with-resource를 활용하려고 합니다. 

connection은 try-with-resource를 100퍼센트 활용할 수 있는데 preparedStatement와 ResultSet은 완전히 적용시킬 수 없어서 별도의 close method를 생성하고 close 로직을 작성하였습니다. 

왜 100% 활용할 수 없는지에 대해서는 아래에서 CRUD method 구현 시 알아보도록 하겠습니다.


다음은 CRUD method를 구현합니다.

/**
 * DB 조회 내용 값 세팅
 * @param resultList 반환 list
 * @param rs result set
 * @param metaData result meta data
 * @throws SQLException sql exception
 */
private void getResultList(List<Map<String, Object>> resultList, ResultSet rs) throws SQLException {
    Map<String, Object> row;
    
    ResultSetMetaData metaData = rs.getMetaData();
    int columnCount = metaData.getColumnCount();

    while(rs.next()){
        row = new HashMap<>();
        for(int i=1; i <= columnCount; i++){
            String keyName = metaData.getColumnName(i);
            Object rsVal = rs.getObject(i);
            row.put(keyName, rsVal);
        }
        resultList.add(row);
    }
}

select 할 때 파라미터가 없을 경우와 있을 경우의 결과 값을 List에 세팅하는 로직은 같기 때문에 별도의 method로 분리하고 resultSet에 있는 컬럼명을 key로 Map 데이터를 생성하고 최종적으로 List에 담아서 사용하기 위해 소스를 작성합니다.


/**
 * select query 실행
 * @param query - select query
 * @return select 결과 리턴
 */
@Override
public List<Map<String, Object>> select(String query){
    List<Map<String, Object>> resultList = new ArrayList<>();

    try(Connection conn = getConnection();
        PreparedStatement stmt = conn.prepareStatement(query);
        ResultSet rs = stmt.executeQuery()){
        getResultList(resultList, rs);
    } catch (SQLException sqlException) {
        logger.error("select sqlException : " + sqlException.getMessage());
    } catch (Exception ex){
        logger.error("select Exception : " + ex.getMessage());
    }

    return resultList;
}

쿼리에 파라미터가 없는 select method입니다. 

try-with-resource를 적용시켜 try 내부에서만 Connection, PreparedStatement, ResultSet을 사용할 수 있게 하고, 결괏값을 담을 List 객체를 생성하여 getResultList method로 resultSet과 함께 넘겨줍니다.


/**
 * select query 실행 with Param
 * @param query select query
 * @param paramVal parameter
 * @return 조회 내용
 */
@Override
public List<Map<String, Object>> select(String query, Map<String, String> paramVal){

    List<Map<String, Object>> resultList = new ArrayList<>();

    ResultSet rs = null;

    try(Connection conn = getConnection();
        PreparedStatement stmt = conn.prepareStatement(query)){

        for(int cnt = 1; cnt <= paramVal.size(); cnt++){
            stmt.setString(cnt, paramVal.get(String.valueOf(cnt)));
        }

        rs = stmt.executeQuery();

        ResultSetMetaData metaData = rs.getMetaData();
        getResultList(resultList, rs, metaData);

    } catch (SQLException sqlException) {
        logger.error("select sqlException : " + sqlException.getMessage() 
		    + ", paramVal=" + paramVal.toString());
    } catch (Exception ex){
        logger.error("select Exception : " + ex.getMessage());
    } finally {
        close(rs);
    }

    return resultList;
}

쿼리에 파라미터가 있는 select method 입니다. 

Map으로 파라미터를 받아서 PreparedStatement에 값을 세팅해주는 로직을 추가하였습니다. 

위에서 이야기했던 try-with-resource를 100% 적용할 수 없는 이유가 있는데, 파라미터가 없는 select method의 경우에는 100% 활용이 가능하지만 파라미터가 있는 select method의 경우에는 preparedStatment의 파라미터가 있어서 쿼리를 실행하기 전에 for문으로 파라미터를 생성해야 하기 때문에 try-with-resource를 100% 적용할 수 없어 ResultSet만 별도로 close 할 수 있는 method를 추가했습니다.


/**
 * insert query 실행
 * @param query - insert query
 * @param paramVal - value
 * @return insert count
 */
@Override
public int insert(String query, Map<String, String> paramVal){
    int intInsertCnt = 0;

    try(Connection conn = getConnection();
        PreparedStatement stmt = conn.prepareStatement(query)){

        for(int cnt = 1; cnt <= paramVal.size(); cnt++){
            stmt.setString(cnt, paramVal.get(String.valueOf(cnt)));
        }
        intInsertCnt = stmt.executeUpdate();
    } catch (SQLException sqlException) {
        logger.error("insert sqlException : " + sqlException.getMessage());
    } catch (Exception ex){
        logger.error("insert Exception : " + ex.getMessage());
    }

    return intInsertCnt;
}

insert를 실행할 method 입니다. 

파라미터를 받아서 preparedStatement에 값을 세팅하고 실행하는 로직을 추가하였습니다. 

insert는 ResultSet이 없기 때문에 try-with-resource를 활용하여 별도의 close method 호출은 없습니다. 


/**
 * update query 실행
 * @param query - update query
 * @param paramVal - value
 * @return update count
 */
@Override
public int update(String query, Map<String, String> paramVal){

    int intUpdateCnt = 0;

    try(Connection conn = getConnection();
        PreparedStatement stmt = conn.prepareStatement(query)){
        for(int cnt = 1; cnt <= paramVal.size(); cnt++){
            stmt.setString(cnt, paramVal.get(String.valueOf(cnt)));
        }
        intUpdateCnt =  stmt.executeUpdate();

    } catch (SQLException sqlException) {
        logger.error("update sqlException : " + sqlException.getMessage());
    } catch (Exception ex){
        logger.error("update Exception : " + ex.getMessage());
    }

    return intUpdateCnt;
}

update mehtod도 insert method와 같은 로직이지만 method name으로 호출 시에 어떤 로직인지를 구분하기 위해 별도의 method로 구현하였습니다.


/**
 * delete query 실행
 * @param query - delete query
 * @param paramVal - value
 * @return delete count
 */
@Override
public int delete(String query, Map<String, String> paramVal){
    int intDeleteCnt = 0;

    try(Connection conn = getConnection();
        PreparedStatement stmt = conn.prepareStatement(query)){

        for(int cnt = 1; cnt <= paramVal.size(); cnt++){
            stmt.setString(cnt, paramVal.get(String.valueOf(cnt)));
        }

        intDeleteCnt =  stmt.executeUpdate();

    } catch (SQLException sqlException) {
        logger.error("delete sqlException : " + sqlException.getMessage());
    } catch (Exception ex){
        logger.error("delete Exception : " + ex.getMessage());
    }

    return intDeleteCnt;
}

delete method도 insert, update와 같은 로직인데 구분을 위해서 별도의 method로 구현하였습니다. 


이상으로 DB에 접속해서 데이터 처리를 할 수 있는 기본적인 소스코드를 구현해 보았습니다. 

이 method 이외에 insert를 여러 번 하는 경우와 페이징 처리를 위해 전체 count를 조회하는 method 이렇게 2개의

method가 더 필요했었는데 일단은 기본적인 내용만 정리하려고 합니다. 

궁금한 사항이 있으시면 댓글 남겨주시면 최대한 답변드리겠습니다.

감사합니다.

Comments