자바 백엔드 입문 42편. 20줄 JDBC 보일러플레이트를 1줄로 줄이는 JdbcTemplate. queryForObject·query·update와 RowMapper 패턴을 짤막한 SQL 작성 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 42편이에요. 41편에서 JDBC 직접 쓰면 20줄 보일러플레이트가 박힌다고 했죠. 이번 42편의 주인공 JdbcTemplate 이 그 20줄을 1줄로 줄여줘요. Spring의 가장 오래된 데이터 접근 추상화이고, JPA 가기 전 한 단계 거쳐야 할 도구예요.
JdbcTemplate이 헷갈리는 이유
처음 JdbcTemplate 코드를 보면 — queryForObject·query·update·RowMapper 같은 메서드가 한꺼번에 등장해요. 또 "PreparedStatement는 어디 갔지?" 가 안 잡혀요.
이 글에서는 JdbcTemplate을 "짧은 SQL 작성 도구" 비유로 풀어요. JDBC 직접 = "매번 종이·연필·지우개·자 다 준비하고 SQL 한 줄 쓰기", JdbcTemplate = "SQL 한 줄만 적으면 나머지 다 알아서". 끝까지 따라오시면 5개 핵심 메서드 + RowMapper 패턴이 한 그림에 들어와요.
Before — JDBC 직접 (20줄)
비교를 위해 41편 의 예제를 다시 봐요.
public Order findById(Long id) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
ps = conn.prepareStatement("SELECT id, amount FROM orders WHERE id = ?");
ps.setLong(1, id);
rs = ps.executeQuery();
if (rs.next()) {
return new Order(rs.getLong("id"), rs.getInt("amount"));
}
return null;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (rs != null) try { rs.close(); } catch (Exception ignore) {}
if (ps != null) try { ps.close(); } catch (Exception ignore) {}
if (conn != null) try { conn.close(); } catch (Exception ignore) {}
}
}
20줄. 핵심은 SELECT ... WHERE id = ? 한 줄인데 나머지 19줄이 잡음.
After — JdbcTemplate (3줄)
같은 일을 JdbcTemplate으로 짜면.
public Order findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT id, amount FROM orders WHERE id = ?",
(rs, rowNum) -> new Order(rs.getLong("id"), rs.getInt("amount")),
id
);
}
3줄로 끝. 커넥션 열기·예외 처리·자원 정리 — JdbcTemplate이 다 처리해줘요. 우리가 짜는 건 "SQL + 결과를 객체로 변환하는 람다 + 파라미터" 셋뿐.
JdbcTemplate 핵심 메서드 5종
자주 만나는 메서드 다섯 개.
| 메서드 | 용도 | 반환 |
|---|---|---|
queryForObject(sql, mapper, params) |
단일 결과 조회 | 객체 한 개 (없으면 예외) |
query(sql, mapper, params) |
여러 결과 조회 | List<T> |
update(sql, params) |
INSERT·UPDATE·DELETE | 변경된 행 수 |
queryForList(sql) |
Map 리스트로 조회 | List<Map<String,Object>> |
batchUpdate(sql, paramsList) |
대량 INSERT·UPDATE | 행 수 배열 |
5개로 90% 시나리오 처리.
조회 — query·queryForObject
// 단일 객체
Order order = jdbc.queryForObject(
"SELECT * FROM orders WHERE id = ?",
orderRowMapper,
orderId);
// 여러 객체
List<Order> orders = jdbc.query(
"SELECT * FROM orders WHERE status = ?",
orderRowMapper,
"ACTIVE");
// 단일 값 (count·sum 등)
int count = jdbc.queryForObject(
"SELECT COUNT(*) FROM orders WHERE status = ?",
Integer.class,
"ACTIVE");
변경 — update
// INSERT
int rows = jdbc.update(
"INSERT INTO orders (product_id, amount) VALUES (?, ?)",
productId, amount);
// UPDATE
jdbc.update(
"UPDATE orders SET status = ? WHERE id = ?",
"COMPLETED", orderId);
// DELETE
jdbc.update("DELETE FROM orders WHERE id = ?", orderId);
update() 반환값은 "몇 행이 영향받았는가". INSERT는 보통 1, UPDATE·DELETE는 조건에 매칭된 행 수.
RowMapper — ResultSet을 자바 객체로
query()·queryForObject() 의 두 번째 인자가 RowMapper. "DB 한 행을 자바 객체 하나로 어떻게 변환하나" 를 정의.
// 1. 람다 표현 (간단)
RowMapper<Order> mapper = (rs, rowNum) ->
new Order(rs.getLong("id"), rs.getInt("amount"));
// 2. 별도 클래스 (재사용)
public class OrderRowMapper implements RowMapper<Order> {
@Override
public Order mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Order(
rs.getLong("id"),
rs.getInt("amount"),
rs.getString("status"),
rs.getTimestamp("created_at").toLocalDateTime()
);
}
}
같은 RowMapper를 여러 메서드에서 재사용하려면 별도 클래스가 깔끔. 한 번만 쓰면 람다가 짧음.
BeanPropertyRowMapper — 자동 매핑
매번 RowMapper 짜기 귀찮을 때 BeanPropertyRowMapper 가 답. "컬럼 이름과 객체 필드 이름이 같으면 자동 매핑".
public class Order {
private Long id;
private int amount;
private String status;
private LocalDateTime createdAt;
// getter/setter
}
// 자동 매핑
List<Order> orders = jdbc.query(
"SELECT id, amount, status, created_at FROM orders",
new BeanPropertyRowMapper<>(Order.class));
created_at (snake_case) → createdAt (camelCase) 자동 변환도 지원. 컬럼명 컨벤션만 맞추면 RowMapper 한 줄로 끝.
여기서 시험 함정 자주 나와요. "BeanPropertyRowMapper가 setter를 호출한다" 는 X. 사실 — public setter가 있어야 동작. 기본 생성자도 필요. Lombok @Setter + @NoArgsConstructor 박아두기.
NamedParameterJdbcTemplate — ? 대신 이름
물음표 ? 가 여러 개 있으면 순서가 헷갈리잖아요. NamedParameterJdbcTemplate 이 이름 기반 파라미터를 지원.
// JdbcTemplate — 순서 매개변수
jdbc.update(
"UPDATE orders SET amount = ?, status = ? WHERE id = ?",
15000, "COMPLETED", 123L);
// NamedParameterJdbcTemplate — 이름 매개변수
Map<String, Object> params = new HashMap<>();
params.put("amount", 15000);
params.put("status", "COMPLETED");
params.put("id", 123L);
namedJdbc.update(
"UPDATE orders SET amount = :amount, status = :status WHERE id = :id",
params);
매개변수가 5개 이상이면 NamedParameterJdbcTemplate이 훨씬 깔끔. Spring Boot가 자동으로 두 Bean 다 등록해줘요.
Bean 주입 패턴
JdbcTemplate을 서비스에 주입하는 표준 패턴.
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final JdbcTemplate jdbcTemplate; // 자동 주입
private final NamedParameterJdbcTemplate namedJdbcTemplate; // 자동 주입
public Order findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM orders WHERE id = ?",
new BeanPropertyRowMapper<>(Order.class),
id);
}
public List<Order> findByStatus(String status) {
return namedJdbcTemplate.query(
"SELECT * FROM orders WHERE status = :status",
Map.of("status", status),
new BeanPropertyRowMapper<>(Order.class));
}
}
18편 의 @Repository + 생성자 주입 패턴. Spring Boot가 JdbcTemplate Bean을 자동 등록하니까 그저 주입만 받으면 끝.
예외 변환 — Spring의 표준 DataAccessException
JDBC 표준 예외 SQLException 은 체크드 예외라 매번 try-catch 박아야 해요. Spring이 이걸 자동으로 런타임 예외 DataAccessException 로 변환해줘요.
// JdbcTemplate은 SQLException을 자동으로 DataAccessException으로 변환
try {
jdbc.queryForObject(...);
} catch (EmptyResultDataAccessException e) {
// 결과 없음 (queryForObject가 0건일 때)
} catch (DuplicateKeyException e) {
// 유니크 키 중복
} catch (DataAccessException e) {
// 그 외 DB 오류
}
DataAccessException 은 RuntimeException 상속이라 try-catch 강제 X. 그리고 DB별 예외(PostgreSQL·MySQL·Oracle)가 다 통일된 Spring 예외로 변환 — DB 바꿔도 catch 코드 그대로.
SimpleJdbcInsert — INSERT 한 줄 더 짧게
INSERT 자주 쓰면 SimpleJdbcInsert 가 한 단계 더 추상화.
public class OrderRepository {
private final SimpleJdbcInsert insert;
public OrderRepository(DataSource dataSource) {
this.insert = new SimpleJdbcInsert(dataSource)
.withTableName("orders")
.usingGeneratedKeyColumns("id");
}
public Long save(Order order) {
Map<String, Object> params = Map.of(
"amount", order.getAmount(),
"status", order.getStatus());
Number key = insert.executeAndReturnKey(params);
return key.longValue();
}
}
SQL 안 짜고 — 테이블 이름 + 컬럼 Map만 주면 INSERT 처리 + auto-generated PK 반환. 단순 INSERT에 자주 활용.
JdbcTemplate = "SQL을 내가 직접 짠다". 세밀한 제어, 복잡한 쿼리·튜닝에 유리. JPA(29~32편) = "SQL을 거의 안 짜고 객체 그래프로 다룬다". 빠른 개발, 단순 CRUD에 유리. 한국 회사 백엔드는 보통 JPA 기본 + 복잡한 쿼리에만 JdbcTemplate 조합.
한국 회사 표준 — Repository 패턴
JdbcTemplate으로 짠 회사 시스템 표준 골격.
// 1. 도메인 객체
public class Order {
private Long id;
private Long productId;
private int amount;
private String status;
private LocalDateTime createdAt;
// getter/setter
}
// 2. Repository 인터페이스 (옵션)
public interface OrderRepository {
Optional<Order> findById(Long id);
List<Order> findByStatus(String status);
Long save(Order order);
int updateStatus(Long id, String status);
}
// 3. JdbcTemplate 구현체
@Repository
@RequiredArgsConstructor
public class JdbcOrderRepository implements OrderRepository {
private final JdbcTemplate jdbcTemplate;
private static final RowMapper<Order> MAPPER = new BeanPropertyRowMapper<>(Order.class);
@Override
public Optional<Order> findById(Long id) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(
"SELECT * FROM orders WHERE id = ?", MAPPER, id));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public List<Order> findByStatus(String status) {
return jdbcTemplate.query(
"SELECT * FROM orders WHERE status = ?", MAPPER, status);
}
// ... 나머지 구현
}
인터페이스로 추상화하면 — 나중에 JPA·MyBatis 같은 다른 구현으로 교체 쉬워요. 옵션이지만 회사 시스템 표준 패턴.
한 줄 정리 — JdbcTemplate = JDBC 보일러플레이트 20줄을 1~3줄로 압축. queryForObject·query·update 세 메서드 + RowMapper(또는 BeanPropertyRowMapper) 패턴. NamedParameterJdbcTemplate은 매개변수 이름 기반.
시험 직전 한 번 더 — JdbcTemplate 입문자가 매번 헷갈리는 것
- JdbcTemplate = JDBC 보일러플레이트 자동 처리 (Spring 가장 오래된 데이터 접근)
- 커넥션·예외·자원 정리 = Spring이 자동
- 핵심 메서드 =
queryForObject·query·update·queryForList·batchUpdate queryForObject= 단일 결과 (없으면 EmptyResultDataAccessException)query=List<T>반환update= INSERT·UPDATE·DELETE, 반환은 영향받은 행 수- RowMapper = ResultSet → 자바 객체 변환 람다
- 별도 클래스 RowMapper도 가능 — 재사용 시 유리
BeanPropertyRowMapper= 컬럼명·필드명 자동 매핑- snake_case → camelCase 자동 변환
- BeanPropertyRowMapper 동작 = setter + 기본 생성자 필요
NamedParameterJdbcTemplate=:name이름 매개변수- 매개변수 5개+ 면 NamedParameter 사용 표준
- Spring Boot = 두 Template Bean 자동 등록
SQLException자동 →DataAccessException(RuntimeException)- DB별 예외도 통일된 Spring 예외 변환 (
DuplicateKeyException등) SimpleJdbcInsert= INSERT 한 단계 더 추상화. auto-generated PK 자동 반환@Repository+ 생성자 주입 표준- 인터페이스 분리 = JPA·MyBatis 교체 쉬움
- JdbcTemplate vs JPA = SQL 직접 vs 객체 그래프
- 회사 백엔드 = JPA 기본 + 복잡한 쿼리에 JdbcTemplate 조합
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 37편 — Spring Security 기초
- 38편 — Spring ApplicationEvent @EventListener
- 39편 — Spring @Async CompletableFuture 비동기
- 40편 — Spring WebClient RestClient HTTP 클라이언트
- 41편 — JDBC와 DataSource
다음 글: