JDBC의 반복 코드를 프레임워크에 위임하고 SQL과 매핑만 우리가 쓰는 얇은 래퍼.
무엇을 대신해 주나
// 커넥션 열기·닫기 · Statement 생성 · 예외 처리 · ResultSet 순회를 전부 대신한다
List<Member> list = jdbcTemplate.query(
"SELECT id, name FROM member WHERE status = ?",
(rs, rowNum) -> new Member(rs.getLong("id"), rs.getString("name")),
"ACTIVE");
// 이름 있는 파라미터를 쓰려면
namedJdbcTemplate.query("SELECT ... WHERE status = :status",
Map.of("status", "ACTIVE"), rowMapper);
예외 계층 변환
JDBC 의 SQLException 은 검사 예외이고, 오류 코드가 DB 마다 다르다
- Spring 이 DataAccessException(언체크) 계층으로 변환한다
DuplicateKeyException · DeadlockLoserDataAccessException · EmptyResultDataAccessException ...
- DB 를 바꿔도 애플리케이션 예외 처리가 그대로 동작한다
검사 예외가 언체크로 바뀐다는 점이 @Transactional의 롤백 규칙과도 맞물린다 — 기본 규칙으로도 롤백된다.
어디에 쓰나
JPA 가 어색한 곳에 쓴다
- 복잡한 통계·리포트 쿼리
- 대량 배치 (영속성 컨텍스트가 오히려 부담)
- 성능이 민감해 SQL 을 직접 통제해야 하는 구간
jOOQ — 타입 안전한 대안
// DB 스키마에서 자바 클래스를 생성해 두고, SQL 을 코드로 쓴다
dsl.select(ORDERS.USER_ID, count())
.from(ORDERS)
.where(ORDERS.CREATED_AT.ge(from)) // 컬럼명 오타·타입 불일치가 컴파일 에러
.groupBy(ORDERS.USER_ID)
.fetch();
- JdbcTemplate — SQL 이 문자열 → 오타가 런타임에 드러난다
- jOOQ — SQL 이 코드 → 컴파일 시점에 걸린다. 대신 코드 생성 단계가 붙는다
요즘의 실무 조합
| 쓰기 경로 | JPA/Hibernate | 영속성 컨텍스트·변경 감지가 값을 한다 |
|---|---|---|
| 읽기 경로 | jOOQ · JdbcTemplate | 쿼리 모양과 성능을 직접 통제한다 |
대부분의 서비스가 읽기 9 : 쓰기 1 이라 자연스러운 분리다 밑바닥은 같은 JDBC 라 커넥션 풀·트랜잭션 경계 이야기는 그대로 적용된다
면접 함정
- ❌ "JdbcTemplate은 구식" → JPA가 어색한 구간의 표준 도구로 지금도 널리 쓴다.
- ❌ "JPA와 JdbcTemplate을 섞으면 안 된다" → 같은 트랜잭션·같은 커넥션을 공유한다. 다만 JPA의 쓰기 지연 때문에 flush 시점을 의식해야 한다.
배치 처리
jdbcTemplate.batchUpdate(
"INSERT INTO member (name, email) VALUES (?, ?)",
members, 1000, // 1000건씩 묶는다
(ps, m) -> { ps.setString(1, m.name()); ps.setString(2, m.email()); });
JPA의 saveAll()보다 훨씬 빠르다 — 영속성 컨텍스트에 엔티티를 쌓지 않고 스냅샷도 만들지 않기 때문이다. 대량 적재는 JdbcTemplate이 맞는 자리다.
JPA와 섞을 때의 함정
@Transactional
void mix(Long id) {
Member m = repo.findById(id).orElseThrow();
m.setName("변경"); // 아직 UPDATE 안 나감 (쓰기 지연)
jdbcTemplate.queryForObject("SELECT name FROM member WHERE id=?", String.class, id);
// ← 옛 값을 읽는다! JdbcTemplate 은 영속성 컨텍스트를 모른다
em.flush(); // 먼저 flush 해야 한다
}
같은 커넥션·같은 트랜잭션이지만 JPA의 쓰기 지연 버퍼는 공유되지 않는다. 섞어 쓸 때는 flush 시점을 반드시 의식한다.