Spring JDBC

후니팍·2023년 4월 14일
1
post-thumbnail

JdbcTemplate

  • 스프링에서 지원하는 jdbc를 편하게 사용하기 위한 클래스입니다.

JDBC

우테코 체스 미션에서 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 연결 정보를 직접 넣어주어야 합니다.
또, 자원 정리 또한 직접 해줘야 합니다.


Spring JDBC

JdbcTemplate을 사용하면 위의 코드처럼 직접 connection을 걸어주지 않아도 됩니다. DataSourceConnection 객체 생성을 하도록 할 수 있습니다.

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 연결을 하지 않고 쿼리를 날리는 것처럼 보입니다. 자원 정리를 하는 부분도 보이지 않습니다.
JdbcTemplateupdate()메소드에서 DB 연결과 자원을 정리 모두 해주기 때문입니다. 아래 코드는 JdbcTemplateupdate() 메소드의 구현체 입니다. 마지막의 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()를 거치게 되는데, 이 때 자원 할당과 정리를 해주고 있습니다!


RowMapper

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);
}

NamedParameterJdbcTemplate

기존 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들의 이름이 같아야합니다.


SimpleJdbcInsert

윗 부분의 코드들은 삽입 과정에서 불편한 점이 있습니다. 삽입하고 나서 바로 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 라이브러리에 있는 주요 객체들에 대해 살펴보았습니다. 처음 공부해보는 라이브러리라서 부족한 부분이 있을 것 같습니다. 잘못된 정보는 차차 수정해 나가겠습니다.

profile
영차영차

1개의 댓글

comment-user-thumbnail
2023년 4월 14일

우왕 잘읽고 갑니다

답글 달기