
Spring boot + JdbcTemplate 사용해보기 (3) 에서 작성했던 코드를 보면 sql 에 물음표(?) 여러개가 있고, 해당 물음표에 assign 할 데이터를 update 메서드의 파라미터로 순차적으로 나열해 주어야 했다.
컬럼 추가하는 유지보수 건이 접수되면, 컬럼 수가 많은 경우 상당히... 짜증이 날 것 같다.
@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);
}
NamedParameterJdbcTemplate 는 이런 문제를 해결해 주는데, 간단히 말하자면 SQL 내에서 물음표(?)를 사용하는 것이 아니라 :변수명 형태로 작성해주고, parameter 를 :변수명 -> 변수값 의 Map 형태로 넘겨주는 것이다.
이렇게 하면 파라미터 순서를 신경쓸 필요가 없고, 변수명만 잘 신경써주면 된다.
NamedParameterJdbcTemplate 클래스 소스코드를 살펴보면, SqlParameterSource 또는 Map<String, ?> 을 사용하고 있다.
@Override
public int update(String sql, SqlParameterSource paramSource) throws DataAccessException {
return getJdbcOperations().update(getPreparedStatementCreator(sql, paramSource));
}
@Override
public int update(String sql, Map<String, ?> paramMap) throws DataAccessException {
return update(sql, new MapSqlParameterSource(paramMap));
}
@Override
public int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder)
throws DataAccessException {
return update(sql, paramSource, generatedKeyHolder, null);
}
@Override
public int update(
String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder, @Nullable String[] keyColumnNames)
throws DataAccessException {
PreparedStatementCreator psc = getPreparedStatementCreator(sql, paramSource, pscf -> {
if (keyColumnNames != null) {
pscf.setGeneratedKeysColumnNames(keyColumnNames);
}
else {
pscf.setReturnGeneratedKeys(true);
}
});
return getJdbcOperations().update(psc, generatedKeyHolder);
}
SqlParameterSource 는 인터페이스고 여러 구현체들을 제공한다.
이들 중에 대표적으로 사용되는 구현체로는 MapSqlParameterSource, BeanPropertySqlParameterSource 가 있다.
NamedParameter 로 사용가능한 것은
Map<String, ?>과SqlParameterSource인터페이스가 있고,SqlParameterSource인터페이스 중MapSqlParameterSource,BeanPropertySqlParameterSource가 대표적으로 사용된다.
기본 JdbcTemplate 객체생성할 때와 방법은 동일하다.
JdbcTemplate -> NamedParameterJdbcTemplate 로만 바뀌었다.
물론, 별도의 config 클래스를 만들어서 bean 으로 등록해놓고 DI 해도 상관없다.
public class JdbcTemplateMemberRepositoryV2 implements MemberRepository {
private final NamedParameterJdbcTemplate template;
public JdbcTemplateMemberRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
}
findById 나 delete 와 같은 메서드는 파라미터를 id 하나 사용하기 때문에 비교적 간단하게 사용할 수 있다. 이 경우 아래와 같이 Map<String, ?> 을 적용할 수 있다.
@Override
public void delete(Long memberId) {
String sql = "DELETE FROM members WHERE id=:id";
Map<String, Long> param = Map.of("id", memberId);
template.update(sql, param);
}
memberId 가 Long 이기에 Map<String, Long> 로 선언했지만 Map<String, Object> 를 사용해도 무방하다.
MapSqlParameterSource 는 Map 과 같이 key-value 쌍을 저장할 수 있다.
@Override
public void update(Long memberId, MemberUpdateParam updateParam) {
String sql = "UPDATE members SET name = :name"
+ ", gender = :gender"
+ ", position = :position"
+ ", birth_date = :birthDate"
+ ", address = :address"
+ ", phone_number = :phoneNumber"
+ " WHERE id = :id";
SqlParameterSource param = new MapSqlParameterSource()
.addValue("name", updateParam.getName())
.addValue("gender", updateParam.getGender().name())
.addValue("position", updateParam.getPosition())
.addValue("birthDate",updateParam.getBirthDate() )
.addValue("address", updateParam.getAddress())
.addValue("phoneNumber", updateParam.getPhoneNumber())
.addValue("id", memberId);
template.update(sql, param);
}
여기서 주의할 점은 template.update 메서드가 실행될 때 enum 타입은 그대로 getter 를 쓰면 오류가 난다.
위 코드에서 updateParam.getGender().name() 부분을 updateParam.getGender() 로 바꾸고 실행하면 오류가 발생한다.
필자는 enum 타입의 경우, enum 명 그대로 DB 저장되는 것을 원했기에 updateParam.getGender().name() 와 같이 enum 의 name() 메서드를 한번 더 호출하여 매핑했다.
BeanPropertySqlParameterSource 는 JavaBean property 를 따르는 객체를 대상으로 작동하는데, 우리가 흔하게 사용하는 getter 와 setter 기반으로 동작한다고 이해하면 된다.
예를 들어, 아래 코드의 경우 :name 에 member.getName() 을 매핑하는 식으로 동작한다.
(위와 같은 매커니즘이라 사실 getter 만 있어서 동작할 것 같은데 테스트 해보니 잘 동작한다.)
@Override
public Member save(Member member) {
String sql = "INSERT INTO members (name, gender, position, birth_date, address, phone_number)"
+ " VALUES (:name,:gender,:position,:birthDate,:address,:phoneNumber)";
SqlParameterSource param = new BeanPropertySqlParameterSource(member);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
long key = keyHolder.getKey().longValue();
member.setId(key);
return member;
}
그런데, 위 코드를 실행하면 에러가 발생한다.
원인은 위 MapSqlParameterSource 예시 에서와 같이 enum 컬럼 때문이다.
debugger 를 이용해 template.update 메서드 로직 오류를 추적해보면, 아래와 같은 쿼리가 생성되는 것을 확인할 수 있다.

쿼리 내용을 보면 gender value에 '** STREAM DATA **' 라는 문자열이 들어가 있는 것을 확인할 수 있다.
gender value 에는 'MALE', 'FEMALE' 만 올 수 있기 때문에 Data truncated for column 'gender' SQL 오류가 발생한다.
위 문제를 해결하기 위해서는 그냥 MapSqlParameterSource 를 사용하거나 아래와 같이 enum 에 대한 처리를 별도로 해주면 된다.
@Override
public Member save(Member member) {
String sql = "INSERT INTO members (name, gender, position, birth_date, address, phone_number)"
+ " VALUES (:name,:gender,:position,:birthDate,:address,:phoneNumber)";
SqlParameterSource param = new BeanPropertySqlParameterSource(member) {
@Override
public Object getValue(String paramName) throws IllegalArgumentException {
Object value = super.getValue(paramName);
if(value instanceof Enum enumValue) {
return enumValue.name();
}
return value;
}
};
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
long key = keyHolder.getKey().longValue();
member.setId(key);
return member;
}
BeanPropertySqlParameterSource 클래스를 상속한 익명 클래스를 생성한 뒤, getValue 메서드만 override 해서 enum 인 경우만 enumValue.name() 을 리턴하도록 적용하였다.
이제, 오류가 발생하지 않고 정상적으로 동작한다.
프로젝트 전반에서 enum 을 위와 같이 처리한다면 BeanPropertySqlParameterSource 를 상속받은 클래스를 별도로 정의해서 재활용하면 좋을 것 같다.
JdbcTemplate 를 사용하는 것보다 NamedParameterJdbcTemplate 를 사용하는 것이 유지보수 성이 좋아보이니, NamedParameterJdbcTemplate 를 적극적으로 사용하자.findById 와 같이 단일 파라미터의 경우 Map<String, ?> 을 사용하자.BeanPropertySqlParameterSource 를 사용하자.MapSqlParameterSource 를 사용하자.