체스 미션에서는 데이터베이스에서 값을 가져오기 위해 DAO를 사용했다.
이 때 JDBC를 사용할 때 데이터베이스의 커넥션을 얻고, try-with-resource를 사용하는 부분이 반복되었다.
템플릿 콜백 패턴을 이용하여 나만의 JdbcTemplate을 만들어보았다.
기존 코드
- User
- UserDao
- ConnectionPool
public class User {
private final int id;
private final String name;
public User(final int id, final String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
public class UserDao {
private final ConnectionPool connectionPool;
public UserDao(final ConnectionPool connectionPool) {
this.connectionPool = connectionPool;
}
public void insert(final String name) {
final Connection connection = connectionPool.getConnection();
final String query = "INSERT INTO User (name) VALUES (?)";
try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
preparedStatement.setString(1, name);
preparedStatement.executeUpdate();
} catch (final SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
public void delete(final int userId) {
final Connection connection = connectionPool.getConnection();
final String query = "DELETE FROM user WHERE id = ?";
try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
preparedStatement.setInt(1, userId);
preparedStatement.executeUpdate();
} catch (final SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
public User findById(final int userId) {
final Connection connection = connectionPool.getConnection();
final String query = "SELECT * FROM user WHERE id = ?";
try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
preparedStatement.setInt(1, userId);
final ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
return new User(
resultSet.getInt("id"),
resultSet.getString("name")
);
}
} catch (final SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
return null;
}
public List<User> findAll() {
final Connection connection = connectionPool.getConnection();
final String query = "SELECT * FROM user";
try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
final ResultSet resultSet = preparedStatement.executeQuery();
final List<User> result = new ArrayList<>();
while (resultSet.next()) {
result.add(new User(
resultSet.getInt("id"),
resultSet.getString("name")
));
}
return result;
} catch (final SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
}
public class ConnectionPool {
private static final String SERVER = "localhost:13306";
private static final String DATABASE = "chess";
private static final String OPTION = "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
private static final String URL = "jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION;
private static final String USERNAME = "root";
private static final String PASSWORD = "root";
private final AtomicInteger index = new AtomicInteger();
private final List<Connection> connections;
public ConnectionPool(final int connectionCount) {
connections = generateConnections(connectionCount);
}
private List<Connection> generateConnections(final int connectionCount) {
return Stream.generate(this::generateConnection)
.limit(connectionCount)
.collect(toList());
}
private Connection generateConnection() {
try {
return DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (SQLException e) {
throw new IllegalStateException("데이터베이스에 연결할 수 없습니다.");
}
}
public Connection getConnection() {
int currentIndex = index.getAndIncrement();
return connections.get(currentIndex % connections.size());
}
}
SELECT, DELETE 중복 제거
변하지 않는 부분: try-with-resource, preparedStatement를 사용하는 부분, executeUpdate로 실행 등등
변하는 부분: SQL Query, 매개변수
다음과 같이 쿼리를 실행하는 부분을 분리하고 가변인수를 사용한다면 SELECT와 DELETE의 중복을 제거할 수 있다.
public void insert(final String name) {
final String query = "INSERT INTO User (name) VALUES (?)";
executeUpdate(query, name);
}
public void delete(final int userId) {
final String query = "DELETE FROM user WHERE user_id = ?";
executeUpdate(query, userId);
}
private void executeUpdate(final String query, final Object... parameters) {
final Connection connection = connectionPool.getConnection();
try (final PreparedStatement preparedStatement = connection.prepareStatement(query)) {
for (int i = 1; i <= parameters.length; i++) {
preparedStatement.setObject(i, parameters[i - 1]);
}
preparedStatement.executeUpdate();
} catch (final SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
조회 분리하기 - 1. 콜백을 위한 인터페이스 정의
조회는 INSERT, DELETE와 달리 값을 반환받아야 하기 때문에 다른 방법을 사용해야 한다.
이 때 콜백이라는 것을 사용하여 중복을 제거할 수 있다.
콜백(Callback)
프로그래밍에서 콜백은 다른 코드의 인수로 넘겨주는 실행 가능한 코드를 뜻한다.
자바에서는 람다나 익명 클래스를 넘겨서 사용할 수 있다.
데이터베이스에서 값을 조회하고, 해당 값을 객체로 매핑하여 값을 반환해야 한다.
executeQuery로 조회한 값은 ResultSet 안에 들어가있다.
이를 원하는 타입의 값으로 변환해야하니 일단 콜백을 위한 인터페이스를 만들어야 한다.
@FunctionalInterface
public interface RowMapper {
User mapRow(final ResultSet resultSet) throws SQLException;
}
조회 분리하기 - 2. 단건 조회
위에서 정의한 RowMapper를 메서드에서 어떻게 사용해야 할까?
아래와 같이 SQL 쿼리, RowMapper, 파라미터를 분리한 메서드에 넘겨주고 쿼리 실행 후 매핑한 값을 반환하도록 한다.
public User findById(final int userId) {
final String query = "SELECT * FROM user WHERE id = ?";
return queryForSingleResult(query, resultSet -> {
final int id = resultSet.getInt("id");
final String name = resultSet.getString("name");
return new User(id, name);
}, userId);
}
private User queryForSingleResult(
final String query,
final RowMapper rowMapper,
final Object... parameters
) {
final Connection connection = connectionPool.getConnection();
try (final PreparedStatement preparedStatement = connection.prepareStatement(query);
final ResultSet resultSet = executeQuery(preparedStatement, parameters)) {
if (resultSet.next()) {
return rowMapper.mapRow(resultSet);
}
return null;
} catch (SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
private ResultSet executeQuery(
final PreparedStatement preparedStatement,
final Object[] parameters) throws SQLException {
for (int i = 1; i <= parameters.length; i++) {
preparedStatement.setObject(i, parameters[i - 1]);
}
return preparedStatement.executeQuery();
}
조회 분리하기 - 3. 다건 조회
단건 조회와 유사하다.
public List<User> findAll() {
final String query = "SELECT * FROM user";
return query(query, resultSet -> {
final int id = resultSet.getInt("id");
final String name = resultSet.getString("name");
return new User(id, name);
});
}
private List<User> query(final String query, final RowMapper rowMapper, final Object... parameters) {
final Connection connection = connectionPool.getConnection();
try (final PreparedStatement preparedStatement = connection.prepareStatement(query);
final ResultSet resultSet = executeQuery(preparedStatement, parameters)) {
final List<User> result = new ArrayList<>();
while (resultSet.next()) {
result.add(rowMapper.mapRow(resultSet));
}
return result;
} catch (SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
private ResultSet executeQuery(
final PreparedStatement preparedStatement,
final Object[] parameters) throws SQLException {
for (int i = 1; i <= parameters.length; i++) {
preparedStatement.setObject(i, parameters[i - 1]);
}
return preparedStatement.executeQuery();
}