🧩 JdbcTemplate: JDBC의 반복 작업을 대신 처리하는 템플릿
순수 JDBC는 커넥션을 열고 닫는 과정, 예외 처리 등 개발자가 직접 처리해야 할 반복적인 코드가 너무 많다는 단점이 있다.
스프링의 JdbcTemplate은 템플릿 콜백 패턴을 사용하여 JDBC 프로그래밍의 반복적인 작업을 대신 처리해주는 클래스다.
JdbcTemplate이 대신 처리해주는 작업들:
- 커넥션 획득 및 종료
- Statement 준비 및 실행
- 결과 조회를 위한 반복문 처리
- 트랜잭션 처리를 위한 커넥션 동기화
- SQLException 발생 시 스프링 예외 변환기 실행
👉 이 덕분에 개발자는 SQL 작성과 결과 매핑이라는 핵심 작업에 더 집중할 수 있다.
JdbcTemplate의 자세한 사용법은 스프링의 공식 메뉴얼을 참고하자.
📜 JdbcTemplate 사용 예시
우선 build.gradle에 다음 의존성을 추가하자.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
예를 들어, 다음과 같은 H2 데이터베이스 테이블에 접근하는 리포지토리를 작성한다고 가정하자.
create table item (
id bigint primary key generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
);
JdbcTemplate은 DataSource를 필요로 하며, 보통 리포지토리 생성자에서 DataSource를 주입받아 JdbcTemplate 객체를 생성하는 방식으로 사용한다.
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {
private final JdbcTemplate template;
// JdbcTemplate은 DataSource 필요 (DataSource는 스프링 부트가 자동 등록함)
public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item (item_name, price, quantity) values (?, ?, ?)";
// DB에서 자동 생성한 키를 담기 위한 홀더
KeyHolder keyHolder = new GeneratedKeyHolder();
// 쿼리 실행
template.update(con -> {
PreparedStatement ps = con.prepareStatement(sql, new String[]{"id"});
ps.setString(1, item.getItemName());
ps.setInt(2, item.getPrice());
ps.setInt(3, item.getQuantity());
return ps;
}, keyHolder);
// DB에서 자동 생성한 id 값
long id = Objects.requireNonNull(keyHolder.getKey()).longValue();
item.setId(id);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name = ?, price = ?, quantity = ? where id = ?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId
);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = ?";
try {
// 단 하나의 행을 조회
Item item = template.queryForObject(sql, itemRowMapper(), id);
return Optional.of(item);
} catch (EmptyResultDataAccessException e) { // 결과가 없는 경우
return Optional.empty();
}
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
String sql = "select id, item_name, price, quantity from item";
// 동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) sql += " where";
List<Object> params = new ArrayList<>();
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%', ?, '%')";
params.add(itemName);
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) sql += " and";
sql += " price <= ?";
params.add(maxPrice);
}
// 여러 행을 조회하여 List 형태로 반환 (결과가 없으면 빈 컬렉션 반환)
return template.query(sql, itemRowMapper(), params.toArray());
}
// 조회 결과(ResultSet)의 각 행 데이터를 자바 객체로 변환 (반복 처리는 JdbcTemplate이 담당)
private RowMapper<Item> itemRowMapper() {
return ((rs, rowNum) -> {
Item item = new Item();
item.setId(rs.getLong("id"));
item.setItemName(rs.getString("item_name"));
item.setPrice(rs.getInt("price"));
item.setQuantity(rs.getInt("quantity"));
return item;
});
}
}
📌 데이터 변경 (INSERT, UPDATE, DELETE):
- template.update() 메서드를 사용한다.
- SQL과 함께 ?에 바인딩할 파라미터를 순서대로 전달하면 된다.
- 반환 값은 쿼리의 영향을 받은 행(row)의 수다.
📌 데이터 조회 (SELECT):
- template.queryForObject(): 단 하나의 행을 조회할 때 사용한다.
- 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
- 결과가 두 개 이상이면 IncorrectResultSizeDataAccessException 예외가 발생한다.
- template.query(): 여러 행을 조회하여 List 형태로 반환할 때 사용한다.
- 결과가 없으면 빈 컬렉션을 반환한다.
📌 RowMapper (조회 결과를 객체로)
- RowMapper는 데이터베이스 조회 결과인 ResultSet을 자바 객체로 변환하는 역할을 한다.
- JdbcTemplate이 ResultSet의 반복 처리를 대신해주므로, 개발자는 한 행의 데이터를 어떻게 객체로 매핑할지만 정의하면 된다.
- 특히 BeanPropertyRowMapper를 사용하면, 데이터베이스의 snake_case 컬럼명(예: item_name)을 자바 객체의 camelCase 프로퍼티명(예: itemName)으로 자동으로 변환하여 매핑해주므로 매우 편리하다.
데이터 저장 시 PK인 id는 identity(auto increment) 방식을 사용하기 때문에 데이터베이스가 대신 생성해 준다.
즉, id는 개발자가 직접 지정하는 것이 아니라, INSERT 쿼리가 실행된 이후에 자동 생성된 값으로 지정해야 한다.
자동 생성된 PK 값은 스프링의 KeyHolder를 통해 받아올 수 있다.
JdbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties에 다음을 추가하면 된다.
logging.level.org.springframework.jdbc=debug
🚀 NamedParameterJdbcTemplate: ? 대신 이름을 지정하는 JdbcTemplate
?를 사용하는 기존의 순서 기반 파라미터 바인딩은 파라미터가 많아질 경우 순서가 꼬여 버그를 유발할 위험이 있다.
String sql = "update item set item_name = ?, price = ?, quantity = ? where id = ?";
// 파라미터 순서를 틀리면 버그 발생! 🚨
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId
);
이를 해결하기 위해 스프링은 ? 대신 :itemName처럼 이름으로 파라미터를 바인딩하는 NamedParameterJdbcTemplate을 제공한다.
이때 NamedParameterJdbcTemplate의 파라미터는 다음을 통해 전달할 수 있다.
- SqlParameterSource (BeanPropertySqlParameterSource, MapSqlParameterSource)
- Map
NamedParameterJdbcTemplate 적용 예시:
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
// ✅ JdbcTemplate 대신 NamedParameterJdbcTemplate 사용
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item (item_name, price, quantity) values (:itemName, :price, :quantity)";
KeyHolder keyHolder = new GeneratedKeyHolder();
// ✅ 1. BeanPropertySqlParameterSource를 통해 파라미터 바인딩
SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(item);
template.update(sql, parameterSource, keyHolder);
long id = Objects.requireNonNull(keyHolder.getKey()).longValue();
item.setId(id);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name = :itemName, price = :price, quantity = :quantity where id = :id";
// ✅ 2. MapSqlParameterSource를 통해 파라미터 바인딩
SqlParameterSource parameterSource = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
template.update(sql, parameterSource);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = :id";
try {
// ✅ 3. Map을 통해 파라미터 바인딩
Map<String, Long> paramMap = Map.of("id", id);
Item item = template.queryForObject(sql, paramMap, itemRowMapper());
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(cond);
String sql = "select id, item_name, price, quantity from item";
// 동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) sql += " where";
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%', :itemName, '%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) sql += " and";
sql += " price <= :maxPrice";
}
return template.query(sql, parameterSource, itemRowMapper());
}
// BeanPropertyRowMapper 사용
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class);
}
}
- BeanPropertyRowMapper:
- ResultSet의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환한다.
- 데이터베이스의 snake_case 컬럼명을 자바 객체의 camelCase 프로퍼티명으로 자동으로 변환하여 매핑해준다.
컬럼명과 프로퍼티명이 완전히 다른 경우에는 SQL에서 별칭(alias)을 사용하자.
🚀 SimpleJdbcInsert: INSERT 쿼리를 대신 작성
INSERT 쿼리를 직접 작성할 필요 없이, 테이블명과 컬럼 정보만으로 간단하게 INSERT 문을 실행할 수 있는 기능을 제공한다.
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
private final SimpleJdbcInsert jdbcInsert;
public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("item") // 테이블명
.usingGeneratedKeyColumns("id"); // PK 컬럼명
// .usingColumns("item_name,", "price", "quantity"); // 쿼리에 사용할 컬럼명 (생략 가능)
}
@Override
public Item save(Item item) {
SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(item);
// INSERT 쿼리를 실행하고, 자동 생성된 PK 값을 반환함
Number key = jdbcInsert.executeAndReturnKey(parameterSource);
item.setId(key.longValue());
return item;
}
...
}
- jdbcInsert.executeAndReturnKey() 메서드를 통해 자동 생성된 PK 값을 매우 편리하게 조회할 수 있다.
SimpleJdbcInsert는 생성 시점에 테이블의 메타 데이터를 조회하기 때문에 어떤 컬럼이 있는지 확인할 수 있다.
따라서 usingColumns를 생략할 수 있으며, 특정 컬럼만 지정해서 저장하고 싶을 때만 사용하면 된다.
❌ JdbcTemplate의 한계: 동적 쿼리
JdbcTemplate의 가장 큰 단점은 동적 쿼리 작성이 어렵다는 점이다. 검색 조건에 따라 WHERE 절이나 AND 조건이 동적으로 변경되어야 할 때, 자바 코드에서 if 문을 사용하여 SQL 문자열을 조립해야 하므로, 코드가 지저분해지고 복잡해진다.
이러한 문제는 MyBatis와 같은 SQL Mapper 기술을 사용하면 효과적으로 해결할 수 있다.
'Spring DB' 카테고리의 다른 글
| [Spring DB] JPA (Java Persistence API) (0) | 2025.10.03 |
|---|---|
| [Spring DB] MyBatis (0) | 2025.10.03 |
| [Spring DB] 스프링의 예외 추상화 (DataAccessException), 예외 변환기 (SQLExceptionTranslator) (0) | 2025.09.23 |
| [Spring DB] 스프링의 트랜잭션 추상화 – 트랜잭션 매니저, 동기화 매니저, 트랜잭션 템플릿, @Transactional (0) | 2025.09.23 |
| [Spring DB] 애플리케이션에 트랜잭션 적용 – 세션, 락, 자동 커밋, 커넥션 동기화 (0) | 2025.09.23 |