
JdbcTemplate 의 의존성 주입의 경우, 아래와 같이 constuctor 에 DataSource 를 받아서 JdbcTemplate 인스턴스를 생성해주는 방식이 관례라고 한다.
물론 별도의 설정파일에서 JdbcTemplate 을 Bean 으로 등록해 놓고 사용해도 된다.
@Repository
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate template;
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
// 다른 코드 생략
}
JdbcTemplate 을 이용하면 원래 더 간단하게 코드를 작성할 수 있는 데, 아래 코드는 기대했던 것 보다 장황하다.
코드가 장황하게 된 이유는 keyHolder 때문이다.
member 를 insert 하고 난 뒤 member 에 할당된 id 를 가져오기 위해 keyHolder 가 사용되었다.
@Override
public Member save(Member member) {
String sql = "INSERT INTO members (name, gender, position, birth_date, address, phone_number)"
+ " VALUES (?,?,?,?,?,?)";
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
template.update(connection -> {
// 자동 증가 키
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setString(1, member.getName());
ps.setString(2, member.getGender().name());
ps.setString(3, member.getPosition());
ps.setDate(4, Date.valueOf(member.getBirthDate()));
ps.setString(5, member.getAddress());
ps.setString(6, member.getPhoneNumber());
return ps;
}, keyHolder);
long key = keyHolder.getKey().longValue();
member.setId(key);
return member;
}

save 에 비해 update 는 비교적 심플하다.
JdbcTemplate 의 update 메서드를 호출하면 되고,
첫번째 파라미터로 sql 을 넘겨주고 그 이후의 파라미터들을 sql 에 표시된 ? 순서대로 넘겨주면 된다.
(순서대로 파라미터를 넘겨줘야 한다는 것이 단점이다. 컬럼추가되면.. 순서 때문에 실수할 확률이 매우 높다.. ㅠㅠ)
@Override
public void update(Long memberId, MemberUpdateParam updateParam) {
String sql = "UPDATE members SET name = ?"
+ ", gender = ?"
+ ", position = ?"
+ ", birth_date = ?"
+ ", address = ?"
+ ", phone_number = ?"
+ " WHERE id = ?";
template.update(sql, updateParam.getName(), updateParam.getGender().name(), updateParam.getPosition(),
updateParam.getBirthDate(), updateParam.getAddress(), updateParam.getPhoneNumber(), memberId);
}
select 의 경우, JdbcTemplate 메서드 중 queryXXX 형태로 정의된 것들을 사용하면 된다.
여기서는 queryForObject 메서드가 사용되었다.
쿼리 결과를 특정 객체로 mapping 해주도록 할 수 있는 데, 그 역할을 하는 것이 memberRowMapper() 이다.
@Override
public Optional<Member> findById(Long id) {
String sql = "SELECT id, name, gender, position, birth_date, address, phone_number FROM members WHERE id=?";
try {
Member member = template.queryForObject(sql, memberRowMapper(), id);
return Optional.ofNullable(member);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
private RowMapper<Member> memberRowMapper() {
return ((rs, rowNum) -> Member.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.gender(Gender.valueOf(rs.getString("gender")))
.position(rs.getString("position"))
.birthDate(rs.getDate("birth_date").toLocalDate())
.address(rs.getString("address"))
.phoneNumber(rs.getString("phone_number"))
.build());
}
queryForObject 메서드는 아래와 같이 정의되어 있다.
sql 이랑 sql 에 사용된 ? 에 대한 파라미터들을 args 로 받고 있고, rowMapper 도 파라미터로 받고 있다.

위 메서드에서 사용된 nullableSingleResult 메서드는 아래와 같이 정의되어 있다.

쿼리 결과가 row 가 1개가 아니면 (즉, 없거나 2개 이상이면) 예외를 throw 하는 것을 확인 할 수 있다.
물론 우리가 작성한 로직의 경우, id 가 PK 이기 때문에 조회 결과가 없거나 1개일 것이라서 EmptyResultDataAccessException 에 대한 예외 처리만 해줬다.
RowMapper<T> 의 경우, (ResultSet rs, int rowNum) -> T 함수에 대한 FunctionalInterface 이다.
memberRowMapper() 코드는 결국, ResultSet 에 담긴 데이터를 Member 객체의 프로퍼티로 매핑해서 Member 객체를 생성하여 리턴해주는 역할을 한다.
findAll 의 경우도 아래 코드는 비교적 간단하다.
하지만, 검색 파라미터가 1개 라서 간단한 것이지 검색 파라미터가 늘어날 수록 코드의 복잡도는 기하급수적으로 복잡해질 것이다.
JdbcTemplate 이 동적 쿼리 작성하기에는 확실히 난이도가 높은 것 같다.
@Override
public List<Member> findAll(MemberSearchParam cond) {
String name = cond.getName();
String sql = "SELECT id, name, gender, position, birth_date, address, phone_number FROM members";
List<Object> param = new ArrayList<>();
if (StringUtils.hasText(name)) {
sql += " WHERE name Like CONCAT('%',?,'%')";
param.add(name);
}
return template.query(sql, memberRowMapper(), param.toArray());
}
제일 간단하다. (자세한 설명은 생략한다)
@Override
public void delete(Long memberId) {
String sql = "DELETE FROM members WHERE id=?";
template.update(sql, memberId);
}