우아한테크코스/레벨4, 레벨5

[jdbc 미션] jdbc 라이브러리: 공통된 부분을 추상화

nauni 2021. 12. 1. 09:04

공통된 부분과 아닌 부분을 분리

어떤 부분을 공통으로 사용하여 추상화 할 것인지 아닌지를 결정해야 한다. 아래와 같은 부분들은 공통으로 추상화할 수 있다.

  • Connection 생성
  • Statement 준비 및 실행
  • ResultSet 생성
  • 예외 처리
  • Connection, Statement, ResultSet 객체 close

이 미션을 하면서는 기존에 사용하던 jdbcTemplate과 미션 가이드의 도움을 많이 받았다. 실제로 추상화 할 부분과 아닌 부분을 결정하는 것은 추상화 단계의 1단계라는 생각이 들었다.

사용한 자원은 닫는다

Connection, PreparedStatement, ResultSet 등의 사용한 자원은 닫아주어야 한다. 자원이 닫히지 않고 메모리를 계속 차지한다면 메모리 누수가 발생할 수 있고 예상치 못한 문제가 발생할 수 있기 때문이라고 한다. 뭐든지 파일도, 커넥션도 열었던 것은 닫아주자! 기존에는 try~catch~finally 구문으로 자원을 처리하였으나 java7 이상부터는 try with resource 문법이 추가되어 finally 대신 자원을 처리해줄 수 있다.

    public void update(String sql, Object... args) {
        try (Connection conn = dataSource.getConnection();
                PreparedStatement pstmt = createPreparedStatement(conn, sql, args)) {
            log.debug("query : {}", sql);

            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }

   private PreparedStatement createPreparedStatement(Connection conn, String sql, Object... args) throws SQLException {
        PreparedStatement pstmt = conn.prepareStatement(sql);
        setPreparedStatement(pstmt, args);
        return pstmt;
    }

  private void setPreparedStatement(PreparedStatement pstmt, Object[] args) throws SQLException {
        int index = 1;
        for (Object arg : args) {
            pstmt.setObject(index, arg);
            index += 1;
        }
    }

PreparedStatement에서 셋팅을 해주어야하는 경우 메소드로 분리하여 처리할 수 있다.

가변인자, 제네릭

2~3월에 객체지향 자바 수업에서 가변인자를 처음 봤다. ...으로 여러개의 인자를 받을 수 있다. 같은 타입의 인자가 몇개가 들어올지 모르는 경우 가변인자를 사용해서 다양하게 함수를 사용할 수 있다. 위에 코드 예시에도 있듯이 가변인자로 받는 경우 배열로 받아진다.

아래의 코드 예시에서 처럼 제네릭 타입을 사용하여 원하는 객체타입을 반환할 수 있다.

템플릿메소드 패턴

해당 메소드를 오버라이드하여 사용하거나 메소드가 1개인 인터페이스를 사용하여 람다식을 사용하여 함수형의 방식으로 인자로 전달해 줄 수 있다.

public interface StateCallBack<T> {
    T doInStatement(Statement stmt, ResultSet rs) throws SQLException;

}

// JdbcTemplate Class
    private <T> T execute(StateCallBack<T> action, String sql, Object... args) {
        try (Connection conn = dataSource.getConnection();
                PreparedStatement pstmt = createPreparedStatement(conn, sql, args);
                ResultSet rs = pstmt.executeQuery()
        ) {
            return action.doInStatement(pstmt, rs);
        } catch (SQLException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }

    public <T> T query(String sql, RowMapper<T> rowMapper, Object... args) {
        return execute(((stmt, rs) -> {
            log.debug("query : {}", sql);

            if (rs.next()) {
                return rowMapper.mapRow(rs);
            }
            return null;
        }), sql, args);
    }

실제로 JdbcTemplate 메소드 안을 살펴보면 아래와 같은 코드로 작성되어 있다.

//실제 jdbcTemplate 라이브러리에서 사용되는 메소드
@FunctionalInterface
public interface ConnectionCallback<T> {
    @Nullable
    T doInConnection(Connection con) throws SQLException, DataAccessException;

}

결과

기존에는 메소드마다 중복되어 작성되었다. 부분들을 공통된 부분과 아닌 부분으로 분리하여 추상화를 하여 라이브러리화 하여 `후`와 같이 좀 더 변화하는 로직에 집중할 수 있는 코드가 되었다.

// 전
    public void insert(User user) {
        final String sql = "insert into users (account, password, email) values (?, ?, ?)";

        Connection conn = null;
        PreparedStatement pstmt = null;
        try {
            conn = dataSource.getConnection();
            pstmt = conn.prepareStatement(sql);

            log.debug("query : {}", sql);

            pstmt.setString(1, user.getAccount());
            pstmt.setString(2, user.getPassword());
            pstmt.setString(3, user.getEmail());
            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        } finally {
            try {
                if (pstmt != null) {
                    pstmt.close();
                }
            } catch (SQLException ignored) {}

            try {
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException ignored) {}
        }
    }



// 후
    public void insert(User user) {
        final String sql = "insert into users (account, password, email) values (?, ?, ?)";

        jdbcTemplate.insert(sql, user.getAccount(), user.getPassword(), user.getEmail());
    }