SQLException이 DataAccessException으로 바뀌는 순간 — 데이터 접근 추상화
try {
jdbcTemplate.queryForObject("SELECT name FROM users WHERE id = ?", String.class, id);
} catch (SQLException e) {
// 이 catch 블록은 실행되지 않는다
}
JdbcTemplate은 SQLException을 던지지 않는다. Spring은 모든 데이터 접근 예외를 unchecked DataAccessException 계층으로 변환한다 — 벤더별 에러 코드(MySQL 1062, PostgreSQL 23505, Oracle 1)를 통일된 타입(DuplicateKeyException 등)으로 매핑한다.
이 글은 JdbcTemplate, 예외 변환(exception translation), Spring Data의 공통 추상화를 풀어간다. (Spring Framework Reference - Data Access)
JDBC의 문제 — Spring이 해결한 것
순수 JDBC의 문제점:
- checked exception:
SQLException을 try-catch하거나 throws로 전파해야 함 - 벤더별 에러 코드: MySQL, PostgreSQL, Oracle의 중복 키 에러 코드가 모두 다름
- 보일러플레이트: Connection → PreparedStatement → ResultSet → close → close → close
Spring의 해결:
- unchecked exception:
DataAccessException(RuntimeException 하위)으로 변환 - 통일된 예외 계층: 벤더 무관하게
DuplicateKeyException하나로 처리 JdbcTemplate: 반복 코드를 프레임워크가 처리
JdbcTemplate — JDBC의 보일러플레이트 제거
// Spring 7 / Java 25 — JdbcTemplate
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
private final JdbcTemplate jdbc;
public UserRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }
// 조회 — RowMapper로 ResultSet → 객체
public Optional<User> findById(Long id) {
try {
User user = jdbc.queryForObject(
"SELECT id, name, email FROM users WHERE id = ?",
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
),
id
);
return Optional.ofNullable(user);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
// 목록 조회
public List<User> findAll() {
return jdbc.query(
"SELECT id, name, email FROM users",
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
)
);
}
// 삽입
public void save(User user) {
jdbc.update(
"INSERT INTO users (name, email) VALUES (?, ?)",
user.name(), user.email()
);
}
// 업데이트
public int updateEmail(Long id, String email) {
return jdbc.update(
"UPDATE users SET email = ? WHERE id = ?",
email, id
);
}
}
JdbcTemplate이 Connection, PreparedStatement, ResultSet의 생성/해제를 모두 처리한다. 개발자는 SQL과 매핑 로직만 작성한다.SQLException은 발생하지 않는다 — Spring이DataAccessException으로 변환한다.
SQLException이 어떻게 DataAccessException으로 바뀌는가 — 내부 메커니즘: JdbcTemplate이 SQL을 실행하고 SQLException이 발생하면 → SQLErrorCodeSQLExceptionTranslator가 이를 가로챈다 → 데이터베이스의 SQLExceptionTranslator가 JDBC 드라이버에서 제공하는 에러 코드(MySQL: 1062, PostgreSQL: SQLSTATE 23505)를 읽는다 → 이 코드를 Spring의 통일된 예외 타입(DuplicateKeyException)으로 매핑한다. 개발자는 "MySQL인지 PostgreSQL인지" 모른 채 DuplicateKeyException 하나로 처리할 수 있다.
비유: 각 나라 공항 세관이 "마약 소지"를 각자 다른 법률 조항으로 기소한다 — 미국은 "21 USC 841", 한국은 "마약류관리법 제60조". Spring은 이것을 "국제 표준 마약 소지죄"(DuplicateKeyException) 하나로 번역한다. 개발자는 각국 법률을 외울 필요 없이, 표준 용어 하나로 대응할 수 있다.
JdbcTemplate이 처리하는 것
| 항목 | 순수 JDBC | JdbcTemplate |
|---|---|---|
| Connection 관리 | 수동(open/close) | 자동 |
| Statement 관리 | 수동 | 자동 |
| ResultSet 관리 | 수동 | 자동 |
| 예외 변환 | SQLException(checked) |
DataAccessException(unchecked) |
| 트랜잭션 통합 | 수동 | @Transactional과 자동 통합 |
예외 변환(Exception Translation)
flowchart LR
subgraph db["데이터베이스별 에러"]
M["MySQL: error 1062"]
P["PostgreSQL: SQLSTATE 23505"]
O["Oracle: ORA-00001"]
end
M --> S["SQLException"]
P --> S
O --> S
S --> T["Spring 예외 변환기<br/>(DataAccessException)"]
T --> D["DuplicateKeyException<br/>(통일된 타입)"]
// Spring 7 / Java 25 — 통일된 예외 처리
import org.springframework.dao.DuplicateKeyException;
@Repository
public class UserRepository {
public void createUser(User user) {
try {
jdbc.update("INSERT INTO users ...", ...);
} catch (DuplicateKeyException e) {
// MySQL, PostgreSQL, Oracle 모두 이 타입으로 잡힘
throw new BusinessException("이미 존재하는 사용자", e);
}
// SQLException catch 불필요!
}
}
DataAccessException 계층
DataAccessException (RuntimeException)
├── DuplicateKeyException // 중복 키
├── DataIntegrityViolationException // 데이터 무결성 위반
├── EmptyResultDataAccessException // 결과 없음
├── IncorrectResultSizeDataAccessException // 결과 수 불일치
└── ...
Spring은
SQLExceptionTranslator가 벤더별 에러 코드와 SQLState를DataAccessException하위 타입으로 매핑한다. 애플리케이션 코드는 벤더를 알 필요 없이DuplicateKeyException하나로 처리한다. (Spring Framework Reference - DAO Exception Handling)
Spring Data 공통 추상화 — Repository 인터페이스
Spring Data(JPA, MongoDB, Redis 등)는 공통된 Repository 인터페이스 계층을 제공한다:
// Spring 7 / Java 25 — Spring Data 공통 인터페이스 (개념)
public interface Repository<T, ID> { } // 마커 인터페이스
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
Optional<T> findById(ID id);
Iterable<T> findAll();
long count();
void deleteById(ID id);
}
// Spring Data JPA로 구현체 자동 생성
public interface UserRepository extends CrudRepository<User, Long> {
// 구현체 작성 불필요 — Spring Data JPA가 런타임에 생성
Optional<User> findByEmail(String email);
List<User> findByNameContaining(String name);
}
Spring Data JPA, Spring Data MongoDB 등이 각각
CrudRepository를 확장하여 구현체를 자동 생성한다.findByEmail같은 메서드 이름만 정의하면 쿼리가 자동 생성된다 (Spring Boot 장 05 - Spring Data JPA 상세).
JdbcTemplate vs Spring Data JPA — 언제 무엇을?
| 항목 | JdbcTemplate |
Spring Data JPA |
|---|---|---|
| SQL | 직접 작성 | 자동 생성 + JPQL |
| 매핑 | RowMapper 수동 | 엔티티 자동 매핑 |
| 학습 곡선 | 낮음 | 중간 ~ 높음 |
| 성능 제어 | 높음 (SQL 최적화) | 보통 (N+1 주의) |
| 적합 | 복잡한 SQL, 최적화 필요 | CRUD 중심, 도메인 모델링 |
간단한 CRUD + 복잡한 조인 쿼리가 섞인 프로젝트에서는 두 가지를 혼용한다 — 기본 CRUD는 Spring Data JPA, 복잡한 분석 쿼리는
JdbcTemplate으로 작성한다.
실습 — JdbcTemplate 패턴
// Spring 7 / Java 25 — JdbcPatternDemo.java
import java.util.*;
public class JdbcPatternDemo {
record User(Long id, String name, String email) {}
public static void main(String[] args) {
// DB 없이 패턴 시뮬레이션 — RowMapper 역할
List<Map<String, Object>> fakeResults = List.of(
Map.of("id", 1, "name", "Alice", "email", "alice@test.com"),
Map.of("id", 2, "name", "Bob", "email", "bob@test.com")
);
// RowMapper 시뮬레이션: Map → User
List<User> users = fakeResults.stream()
.map(row -> new User(
((Number) row.get("id")).longValue(),
(String) row.get("name"),
(String) row.get("email")
))
.toList();
users.forEach(u -> System.out.println(u.id() + ": " + u.name() + " <" + u.email() + ">"));
}
}
java JdbcPatternDemo.java
1: Alice <alice@test.com>
2: Bob <bob@test.com>
확인할 것: RowMapper가 데이터베이스 행(row)을 객체로 변환한다. JdbcTemplate이 Connection/Statement/ResultSet 관리를 처리하고, 개발자는 매핑 로직만 작성한다.
NamedParameterJdbcTemplate — 이름 기반 매개변수
JdbcTemplate은 ? 자리표시자를 쓰지만, 매개변수 순서가 헷갈린다. NamedParameterJdbcTemplate은 이름 기반 매개변수를 제공한다:
// Spring 7 / Java 25 — NamedParameterJdbcTemplate
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
@Repository
public class UserRepository {
private final NamedParameterJdbcTemplate npjdbc;
public UserRepository(NamedParameterJdbcTemplate npjdbc) { this.npjdbc = npjdbc; }
public List<User> findByCityAndAge(String city, int minAge) {
var params = new MapSqlParameterSource()
.addValue("city", city)
.addValue("minAge", minAge);
return npjdbc.query(
"SELECT * FROM users WHERE city = :city AND age >= :minAge",
params,
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"))
);
}
}
:city,:minAge로 이름을 붙이면 순서에 신경 쓸 필요 없다. 복잡한 쿼리에서 가독성이 크게 향상된다.
SimpleJdbcInsert — INSERT 간소화 + 자동 키 생성
// Spring 7 / Java 25 — SimpleJdbcInsert로 INSERT + KeyHolder
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
@Repository
public class UserRepository {
private final SimpleJdbcInsert insert;
public UserRepository(DataSource dataSource) {
this.insert = new SimpleJdbcInsert(dataSource)
.withTableName("users")
.usingGeneratedKeyColumns("id"); // 자동 생성 키
}
public long save(User user) {
Map<String, Object> params = Map.of(
"name", user.name(),
"email", user.email()
);
Number key = insert.executeAndReturnKey(params);
return key.longValue(); // 생성된 PK 반환
}
}
배치(Batch) 처리
// Spring 7 / Java 25 — 배치 INSERT
public void batchInsert(List<User> users) {
npjdbc.batchUpdate(
"INSERT INTO users (name, email) VALUES (:name, :email)",
users.stream().map(u -> {
var params = new MapSqlParameterSource()
.addValue("name", u.name())
.addValue("email", u.email());
return params;
}).toArray(MapSqlParameterSource[]::new)
);
// 수천 건을 한 번에 INSERT — 개별 INSERT보다 수십 배 빠름
}
요약 — 이 글의 결론
JdbcTemplate이 JDBC 보일러플레이트를 제거한다. Connection, Statement, ResultSet 관리를 프레임워크가 처리한다. 개발자는 SQL과 RowMapper만 작성.- Spring이 예외를 변환한다. 벤더별
SQLException을 통일된DataAccessException(unchecked) 계층으로 변환.DuplicateKeyException하나로 모든 DB의 중복 키 에러를 처리. DataAccessException은 unchecked다. try-catch를 강제하지 않아 코드가 간결해진다. 필요한 곳에서만 catch.- Spring Data는 공통
Repository추상화를 제공한다. Spring Data JPA, MongoDB 등이 같은 인터페이스 계층 위에 구축된다. JdbcTemplate과 Spring Data JPA는 혼용할 수 있다. 복잡한 SQL은JdbcTemplate, CRUD 중심은 Spring Data JPA.
생각해 볼 문제
SQLExceptionTranslator가 벤더별 에러 코드를 어떻게 매핑하는가?SQLErrorCodeSQLExceptionTranslator의 동작 원리는?JdbcTemplate에서PreparedStatement재사용(prepared statement pooling)은 어떻게 이루어지는가?DataAccessException의 하위 타입 중TransientDataAccessException과NonTransientDataAccessException의 차이는?- Spring Data의
@Repository어노테이션이 예외 변환에 어떤 역할을 하는가? - Spring 7에서 JPA 3.2(Hibernate 7)로 올라가면서 변경된 점은?
참고
- Spring Framework 7 - Data Access - 접근 2026-07-10
- Spring Framework 7 - DAO Exceptions - 접근 2026-07-10
- Spring Framework 7 - JDBC - 접근 2026-07-10
- Spring Framework 7 - DAO Exception Handling - 접근 2026-07-10
'Develop Artifacts > Spring' 카테고리의 다른 글
| Spring - 09. transaction (0) | 2026.07.12 |
|---|---|
| Spring - 08. validation exception (0) | 2026.07.12 |
| Spring - 07. spring mvc (0) | 2026.07.12 |
| Spring - 06. aop (0) | 2026.07.12 |
| Spring - 05. bean scope profile (0) | 2026.07.12 |
