우테코 체스 미션에서 JdbcTemplate
를 사용하지 않고 직접 jdbc를 사용했습니다.
아래의 코드와 같이 DB 연결, 자원 정리 작업을 직접했습니다.
public Connection getConnection() {
try {
return DriverManager.getConnection("jdbc:mysql://" + SERVER + "/" + DATABASE + OPTION, USERNAME, PASSWORD);
} catch (final SQLException e) {
System.err.println("DB 연결 오류:" + e.getMessage());
e.printStackTrace();
return null;
}
}
@Override
public int save(final ChessGameDto chessGameDto) {
try (final Connection connection = databaseConnector.getConnection()) {
int chessGameId = saveChessGame(chessGameDto, connection);
savePiece(chessGameDto.getBoard(), connection, chessGameId);
return chessGameId;
} catch (SQLException | IllegalStateException e) {
e.printStackTrace();
throw new RuntimeException("정상 저장되지 않았습니다.");
}
}
private int saveChessGame(final ChessGameDto chessGameDto, final Connection connection) {
final String query = "insert into chess_game(turn) values (?)";
try (final PreparedStatement ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);) {
ps.setString(1, chessGameDto.getTurn());
ps.executeUpdate();
return findChessGameId(ps);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("정상 저장되지 않았습니다.");
}
}
일부의 코드를 가져왔습니다. 코드에서와 같이 connection을 할 때 application.properties
(application.yml
)의 DB 연결 정보를 직접 넣어주어야 합니다.
또, 자원 정리 또한 직접 해줘야 합니다.
JdbcTemplate
을 사용하면 위의 코드처럼 직접 connection을 걸어주지 않아도 됩니다. DataSource
가 Connection
객체 생성을 하도록 할 수 있습니다.
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateDao(final DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public int save(final ChessGameDto chessGameDto) {
final String sql = "insert into chess_game(turn) values (?)";
jdbcTemplate.update(sql, chessGameDto.getTurn());
}
DataSource
를 생성자의 인자로 받아왔지만 getConnection()
메소드가 보이지 않습니다. DB 연결을 하지 않고 쿼리를 날리는 것처럼 보입니다. 자원 정리를 하는 부분도 보이지 않습니다.
JdbcTemplate
의 update()
메소드에서 DB 연결과 자원을 정리 모두 해주기 때문입니다. 아래 코드는 JdbcTemplate
의 update()
메소드의 구현체 입니다. 마지막의 execute()
메소드를 주목해보겠습니다.
@Override
public int update(final String sql) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL update [" + sql + "]");
}
/**
* Callback to execute the update statement.
*/
class UpdateStatementCallback implements StatementCallback<Integer>, SqlProvider {
@Override
public Integer doInStatement(Statement stmt) throws SQLException {
int rows = stmt.executeUpdate(sql);
if (logger.isTraceEnabled()) {
logger.trace("SQL update affected " + rows + " rows");
}
return rows;
}
@Override
public String getSql() {
return sql;
}
}
return updateCount(execute(new UpdateStatementCallback(), true));
}
@Nullable
private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
stmt = con.createStatement();
applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("StatementCallback", sql, ex);
}
finally {
if (closeResources) {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
}
처음에 getConnection()
으로 DB와 연결을 하고, 마지막 finally
에서 DB 연결을 해제하고 Statement
의 자원을 정리합니다. JdbcTemplate
의 모든 메소드는 execute()
를 거치게 되는데, 이 때 자원 할당과 정리를 해주고 있습니다!
JdbcTemplate
의 메소드를 이용하다 보면 원하는 도메인 클래스 타입으로 반환되게 하고 싶은 경우가 있습니다. 아래의 코드와 같이 반환 값을 도메인 클래스로 지정하게 되면 class mismatch 에러가 발생합니다.
public User getUser(Long id) {
final String sql = "select * from users where id = ?";
return jdbcTemplate.queryForObject(sql, User.class, id);
}
당연하게 DB에는 User 객체가 존재하지 않습니다. users table이 존재할 뿐이죠. 그런데 DB에서 User 타입으로 반환하게끔 코드를 작성했기 때문에 에러가 발생한 것입니다. 이런 상황을 위해 Spring JDBC는 RowMapper
를 제공합니다.
RowMapper
를 사용하면 원하는 형태의 결과값을 반환할 수 있습니다.
public User getUser(Long id) {
final String sql = "select * from users where id = ?";
return jdbcTemplate.queryForObject(sql, userRowMapper, id);
}
private final RowMapper<User> userRowMapper = (resultSet, rowNum) -> {
User user = new User(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("nickname"),
resultSet.getInt("age"),
resultSet.getString("address")
);
return user;
};
ResultSet
으로 DB 결과값을 먼저 받고 User 객체에 담아서 반환하는 방식입니다. 조금 더 개선해보자면 아래와 같이 바꿀 수 있습니다.
public User getUser(Long id) {
final String sql = "select * from users where id = ?";
return jdbcTemplate.queryForObject(
sql,
(resultSet, rowNum) -> new User(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getString("nickname"),
resultSet.getInt("age"),
resultSet.getString("address")
),
id);
}
기존 JdbcTemplate
에는 불편한 점이 있었습니다. 쿼리에 들어가는 인자를 세팅할 때 순서대로 세팅을 해주어야 했습니다. 아래와 같이 말이죠.
final String sql = "insert into users (name, nickname, age, address) values (?,?,?,?)"
template.update(sql, user.getName(), user.getNickname(), user.getAge(), user.getAddress());
인자가 많아질수록 순서는 헷갈리게 되고 굉장히 불편해지는데요, 이럴 때 사용할 수 있는 것이 NamedParameterJdbcTemplate
입니다.
NamedParameterJdbcTemplate
를 사용하면 메소드 매개변수의 순서를 고려하지 않아도 됩니다.
public int saveUser(final User user) {
final String sql = "insert into users(name, nickname, age, address) values (:name,:nickname,:age,:address)";
final SqlParameterSource param = new MapSqlParameterSource()
.addValue("name", user.getName())
.addValue("age", user.getAge())
.addValue("nickname", user.getNickname())
.addValue("address", user.getAddress());
return namedParameterJdbcTemplate.update(sql, param);
}
key-value 형태인 Map 방식으로 parameter source를 저장하기 때문에 JdbcTemplate
에 비해 더 간편하게 코드를 작성할 수 있었습니다. 위 코드에서는 더 간단하게 코드를 작성할 수도 있습니다. BeanPropertySqlParameterSource
를 이용하는 방법인데요. 코드를 우선 보겠습니다.
public int saveUser(final User user) {
final String sql = "insert into users(name, nickname, age, address) values (:name,:nickname,:age,:address)";
final SqlParameterSource param = new BeanPropertySqlParameterSource(user);
return namedParameterJdbcTemplate.update(sql, param);
}
BeanPropertySqlParameterSource
는 Java Bean 객체의 속성 이름을 key로 하고, 해당 속성의 값을 value로 하여 SqlParameterSource 구현체를 만듭니다. MapSqlParameterSource
처럼 key-value를 이용하는데, BeanPropertySqlParameterSource
는 자동으로 key-value 쌍을 연결해주는 방식입니다. 당연하게도 필드 명과 parameter source들의 이름이 같아야합니다.
윗 부분의 코드들은 삽입 과정에서 불편한 점이 있습니다. 삽입하고 나서 바로 id를 조회하고 싶을 때, 코드가 굉장히 복잡해집니다. 아래의 코드는 KeyHolder
를 이용하여 primary key를 조회하는 방식인데, 가독성이 떨어집니다.
public Long insertWithKeyHolder(User user) {
final String sql = "insert into users (name, nickname, age, address) values (?, ?, ?, ?)";
final KeyHolder generatedKeyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setString(1, user.getName());
ps.setString(2, user.getNickname());
ps.setInt(3, user.getAge());
ps.setString(4, user.getAddress());
return ps;
}, generatedKeyHolder);
return generatedKeyHolder.getKey().longValue();
}
이를 위해 스프링에서는 SimpleJdbcInsert
라는 삽입을 위한 클래스를 제공합니다.
처음에 아래와 같이 세팅을 합니다.
private SimpleJdbcInsert insertActor;
public SimpleInsertDao(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("users")
.usingGeneratedKeyColumns("id");
}
table명과 generatedKey의 칼럼명을 설정해주면 SimpleJdbcInsert
를 사용할 때 해당 table의 해당 칼럼의 값을 조회할 수 있게 됩니다.
아래의 코드처럼 간단하게 insert 할 수 있습니다.
public User insertWithSimpleJdbcInsert(User user) {
SqlParameterSource params = new BeanPropertySqlParameterSource(user);
final long id = insertActor.executeAndReturnKey(params).longValue();
return new User(id, user.getName(), user.getNickname(), user.getAge(), user.getAddress());
}
Spring JDBC 라이브러리에 있는 주요 객체들에 대해 살펴보았습니다. 처음 공부해보는 라이브러리라서 부족한 부분이 있을 것 같습니다. 잘못된 정보는 차차 수정해 나가겠습니다.
우왕 잘읽고 갑니다