[우아한테크코스 백엔드 4기] 레벨4 - "JDBC 라이브러리 구현하기" 회고

헌치·2022년 11월 8일
1

우아한테크코스

목록 보기
26/30

깃허브 레포 : jwp-dashboard-jdbc

JdbcTemplate

기존의 JdbcTemplate의 파라미터,리턴값을 참조해 나만의 JdbcTemplate을 만들어보았다.
RowMapper, PreparedStatementSetter, ResultSetCallback 등 다양한 함수형 인터페이스를 사용해 코드 중복을 줄이려 했다.

public class JdbcTemplate {

    private final JdbcExecutor jdbcExecutor;
    private final DefaultStatementSetter defaultStatementSetter = new DefaultStatementSetter();

    public JdbcTemplate(final DataSource dataSource) {
        this.jdbcExecutor = new JdbcExecutor(dataSource);
    }


    public <T> List<T> query(final String sql, final RowMapper<T> rowMapper,
                             final Object... args) {
        return query(sql, defaultStatementSetter.getSetter(args), rowMapper);
    }

    public <T> List<T> query(final String sql,
                             final PreparedStatementSetter statementSetter,
                             final RowMapper<T> rowMapper) {
        final ResultSetCallback<List<T>> resultSetCallback = rs -> {
            List<T> result = new ArrayList<>();
            while (rs.next()) {
                result.add(rowMapper.mapRow(rs));
            }
            return result;
        };
        return jdbcExecutor.find(sql, statementSetter, resultSetCallback);
    }

    public <T> T queryForObject(final String sql, final RowMapper<T> rowMapper,
                                final Object... args) {
        return queryForObject(sql, defaultStatementSetter.getSetter(args), rowMapper);
    }

    public <T> T queryForObject(final String sql,
                                final PreparedStatementSetter statementSetter,
                                final RowMapper<T> rowMapper) {
        final ResultSetCallback<T> resultSetCallback = rs -> {
            rs.next();
            return rowMapper.mapRow(rs);
        };
        return jdbcExecutor.find(sql, statementSetter, resultSetCallback);
    }

    public Integer executeUpdate(final String sql, final Object... args) {
        return executeUpdate(sql, defaultStatementSetter.getSetter(args));
    }

    public Integer executeUpdate(final String sql,
                                 final PreparedStatementSetter statementSetter) {
        return jdbcExecutor.update(sql, statementSetter);
    }

    public DataSource getDataSource() {
        return jdbcExecutor.getDataSource();
    }
}

JdbcExecutor

명령 쿼리 로직을 한 곳에서 관리하고 싶어 콜백 패턴으로 JdbcExecutor를 추출했다.

public class JdbcExecutor {

    private static final Logger log = LoggerFactory.getLogger(JdbcExecutor.class);

    private final DataSource dataSource;

    public JdbcExecutor(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public <T> T find(String sql, PreparedStatementSetter statementSetter,
                      ResultSetCallback<T> resultSetCallback) {
        return executeOrThrow(sql, statementSetter, statement -> {
            try (final ResultSet rs = statement.executeQuery()) {
                return resultSetCallback.execute(rs);
            }
        });
    }

    public Integer update(final String sql,
                          final PreparedStatementSetter statementSetter) {
        return executeOrThrow(sql, statementSetter, PreparedStatement::executeUpdate);
    }

    private <T> T executeOrThrow(final String sql, final PreparedStatementSetter setter,
                                 final PreparedStatementCallback<T> statementCallback) {
        Connection connection = DataSourceUtils.getConnection(dataSource);
        try (final PreparedStatement statement = connection.prepareStatement(sql)) {
            setter.setValues(statement);
            return statementCallback.execute(statement);
        } catch (SQLException e) {
            log.error(e.getMessage(), e);
            throw new DataAccessException(e);
        } finally {
            DataSourceUtils.releaseConnection(connection, dataSource);
        }
    }

    public DataSource getDataSource() {
        return dataSource;
    }
}

PreparedStatementSetter

PreparedStatementSetter라는 함수형 인터페이스를 활용해 람다 형태로 PreparedStatement 인자를 세팅했다.
처음엔 DefaultStatementSetter 로직을 JdbcTemplate 안에 뒀는데, 기능 단위로 분리하고 싶어 클래스로도 추출했다.
콜백 패턴을 적용하고 나니 오히려 로직이 복잡해진 것 같기도 하다.

@FunctionalInterface
public interface PreparedStatementSetter {

    void setValues(PreparedStatement p) throws SQLException;
}

public class DefaultStatementSetter {

    PreparedStatementSetter getSetter(Object... args) {
        return stmt -> {
            try {
                for (int i = 0; i < args.length; i++) {
                    stmt.setObject(i + 1, args[i]);
                }
            } catch (NullPointerException e) {
                throw new SQLException(e);
            }
        };
    }
}

이외 함수형 인터페이스

@FunctionalInterface
public interface PreparedStatementCallback<T> {

    T execute(PreparedStatement statement) throws SQLException;
}

@FunctionalInterface
public interface ResultSetCallback<T> {

    T execute(ResultSet resultSet) throws SQLException;
}

@FunctionalInterface
public interface RowMapper<T> {
    T mapRow(ResultSet rs) throws SQLException;
}

테스트

테스트에서는 리턴값을 검증하는 방법이 까다로워, 우선은 Mock 형태로 객체들이 close() 되었는지 여부만 확인했습니다.


class JdbcTemplateTest {
    private final DataSource dataSource = mock(DataSource.class);
    private JdbcTemplate jdbcTemplate;

    @BeforeEach
    void setUp() {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @DisplayName("queryForObject 호출 시 결과 반환 후 connection, statement, resultSet이 close된다.")
    @Test
    void queryForObject() throws SQLException {
        //given
        Connection connection = mock(Connection.class);
        PreparedStatement statement = mock(PreparedStatement.class);
        ResultSet resultSet = mock(ResultSet.class);

        when(dataSource.getConnection()).thenReturn(connection);
        when(connection.prepareStatement(anyString())).thenReturn(statement);
        when(statement.executeQuery()).thenReturn(resultSet);

        //when
        final var select = "select id, account, password, email from users where id = ?";
        jdbcTemplate.queryForObject(select, mock(RowMapper.class), 1L);

        //then
        verify(statement).close();
        verify(resultSet).close();
        verify(connection).close();
    }

    @DisplayName("query 호출 시 결과 반환 후 connection, statement, resultSet이 close된다.")
    @Test
    void query() throws SQLException {
        //given
        Connection connection = mock(Connection.class);
        PreparedStatement statement = mock(PreparedStatement.class);
        ResultSet resultSet = mock(ResultSet.class);

        when(dataSource.getConnection()).thenReturn(connection);
        when(connection.prepareStatement(anyString())).thenReturn(statement);
        when(statement.executeQuery()).thenReturn(resultSet);

        //when
        final var select = "select id, account, password, email from users where account = ?";
        jdbcTemplate.query(select, mock(RowMapper.class), "hunch");

        //then
        verify(statement).close();
        verify(resultSet).close();
        verify(connection).close();
    }

    @DisplayName("executeUpdate 호출 시 결과 반환 후 connection, statement이 close된다.")
    @Test
    void executeUpdate() throws SQLException {
        //given
        Connection connection = mock(Connection.class);
        PreparedStatement statement = mock(PreparedStatement.class);

        when(dataSource.getConnection()).thenReturn(connection);
        when(connection.prepareStatement(anyString())).thenReturn(statement);
        when(statement.executeUpdate()).thenReturn(5);

        //when
        final var select = "update users set account = ?";
        Integer integer = jdbcTemplate.executeUpdate(select, mock(RowMapper.class), "hunch");

        //then
        assertThat(integer).isEqualTo(5);
        verify(statement).close();
        verify(connection).close();
    }
}

후기

  • 여러 방식으로 중복된 코드를 줄일 수 있구나...

  • 콜백/템플릿 패턴을 통해 중복로직을 분리 하는 것이 재밌었다.

    • 직접 인터페이스를 통해 템플릿/콜백을 구현해본 적이 없었는데 리팩토링 하면서 적용해볼 수 있어 좋았다.
    • 예전에 미니 프로젝트 구현 도중, 중복된 try-catch 문을 어떻게 줄일 수 있을지 고민했지만 해답을 못찾았던 적 있었다. 템플릿/콜백 패턴을 이용하면 가능하다! 다시 제네릭을 포함한 람다나 익명클래스로 리팩토링 해보고 싶다.
    • 관련 내용 : [우아한테크코스 백엔드 4기]프리코스 3주차 "자판기" 회고
profile
🌱 함께 자라는 중입니다 🚀 rerub0831@gmail.com

0개의 댓글