Spring 데이터 접근 기술
JDBC, JdbcTemplate, 예외 추상화, 트랜잭션 경계로 본 데이터 접근 기술의 선택 기준
Contents
데이터 접근 기술은 작업마다 필요한 추상화 수준이 달라서, 통제권과 생산성 사이에서 고르는 선택입니다.
JDBC가 남기는 반복
자바 데이터베이스 연결(JDBC)은 데이터 접근의 가장 바닥 계층입니다. 커넥션 획득, SQL 실행, 결과 매핑, 자원 반환, 예외 처리가 코드에 그대로 드러납니다. 그래서 저장소 클래스마다 비슷한 try-finally 구조가 반복됩니다.
이 반복은 누락의 위험을 키웁니다. 자원 반환을 한 번이라도 빠뜨리면 커넥션 풀이 천천히 고갈됩니다. 상위 데이터 접근 기술이 무엇을 감춰 주는지는 이 바닥을 봐야 분명해집니다.
// JDBC 직접 사용 - 매 메서드마다 반복되는 구조
Connection con = dataSource.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql);
try {
pstmt.setLong(1, id);
ResultSet rs = pstmt.executeQuery();
// 결과 매핑 ...
} catch (SQLException e) {
throw new MyDbException(e);
} finally {
pstmt.close(); // 여기서 또 SQLException 가능
con.close();
}위 코드에서 핵심 로직은 SQL과 매핑뿐인데, 자원 반환과 예외 변환이 더 많은 줄을 차지합니다. 저장소가 늘어날수록 이 골격이 그대로 복제됩니다.
JdbcTemplate가 줄이는 것
JdbcTemplate은 spring-jdbc 라이브러리에 포함된 도구로, 별도 설정 없이 바로 씁니다. 템플릿 콜백 패턴으로 커넥션 획득과 종료, statement와 결과 집합 종료, 트랜잭션 동기화, 예외 번역을 대신 처리합니다. 개발자는 SQL을 쓰고, 파라미터를 정의하고, 결과를 매핑하면 됩니다.
다만 SQL 통제는 여전히 개발자 책임입니다. SQL 문자열과 RowMapper를 직접 설계해야 하므로, 복잡한 조회가 많아지면 SQL과 매핑 코드가 장황해집니다.
private final JdbcTemplate template;
public JdbcItemRepository(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
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();
}
}
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;
};
}순서 기반 ? 바인딩은 컬럼 순서가 바뀌면 조용히 어긋납니다. 그래서 스프링은 이름 기반 바인딩인 NamedParameterJdbcTemplate을 권장합니다.
private final NamedParameterJdbcTemplate template;
public Item save(Item item) {
String sql = "insert into item (item_name, price, quantity) " +
"values (:itemName, :price, :quantity)";
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
item.setId(keyHolder.getKey().longValue());
return item;
}BeanPropertySqlParameterSource는 객체의 프로퍼티명을 그대로 바인딩 이름에 맞춥니다. 단순 INSERT만 필요하면 SimpleJdbcInsert로 SQL 문자열 자체를 생략할 수도 있습니다.
예외 추상화와 예외 번역
SQLException은 JDBC 기술에 종속된 체크 예외입니다. 이걸 서비스 계층까지 그대로 올리면 상위 코드가 저장소 기술 세부사항에 묶입니다. 나중에 JDBC에서 JPA로 바꾸면 예외 타입 때문에 상위 코드까지 영향을 받습니다.
스프링은 이 문제를 예외 번역으로 풉니다. SQLExceptionTranslator가 기술 종속 예외를 DataAccessException 계층으로 바꿉니다. JdbcTemplate은 이 변환을 내부에서 자동으로 수행합니다.
DataAccessException은 런타임 예외라서 모든 메서드에 throws를 달지 않아도 됩니다. JPA로 구현을 바꿔도 스프링은 JPA 예외를 같은 계층으로 번역해 변경 폭을 줄입니다. 대신 스프링이라는 기술에는 종속됩니다.
추상화가 예외 원인까지 지우지는 않습니다. 모든 오류를 공통 예외 하나로 뭉뚱그리면 원인 추적이 어려워지므로, 제약 위반과 문법 오류는 구분해 받는 편이 낫습니다.
데이터소스와 트랜잭션 경계
커넥션을 매번 새로 만들면 TCP 연결과 DB 인증까지 거쳐야 해서 비쌉니다. 그래서 미리 만든 커넥션을 재사용하는 커넥션 풀을 씁니다. 스프링 부트는 spring.datasource.url 같은 속성을 읽어 HikariDataSource를 기본 풀로 등록합니다.
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/test
username: sa
password:풀 크기는 운영 부하에 맞춰 조정합니다. HikariCP의 기본 최대 풀 크기는 10입니다(HikariCP 공식 문서 maximumPoolSize 기준).
커넥션 위에는 트랜잭션 경계가 있습니다. 스프링 부트는 등록된 기술을 보고 트랜잭션 매니저를 자동 등록합니다. JDBC 계열이면 DataSourceTransactionManager, JPA면 JpaTransactionManager를 고릅니다. 등록되는 빈 이름은 transactionManager입니다.
트랜잭션 경계는 보통 서비스 계층에 둡니다. 한 업무가 여러 저장소 작업을 하나로 묶기 때문입니다. @Transactional은 프록시 기반 선언형 도구라, 메서드 호출을 트랜잭션으로 감싸 줍니다.
전파(propagation)는 기존 트랜잭션에 참여할지 새로 만들지를 정하는 규칙입니다. 기본값 REQUIRED는 기존 트랜잭션이 있으면 참여하고 없으면 새로 시작합니다. 감사 로그처럼 본 작업과 독립된 커밋이 꼭 필요할 때만 REQUIRES_NEW를 제한적으로 씁니다.
프록시 기반이라 두 가지를 조심해야 합니다. 같은 객체 안에서 자기 메서드를 직접 부르면 프록시를 우회해 트랜잭션이 안 걸립니다(self-invocation). 내부 참여 호출에서 예외가 나면 바깥에서 잡아도 커밋 시점에 UnexpectedRollbackException이 날 수 있습니다.
기술 선택 기준
실무 선택지는 JDBC와 그 위의 추상화에 MyBatis, JPA, Spring Data JPA, Querydsl까지 더해집니다. 아래 표는 각 기술이 잘 맞는 상황과 주의점을 비교합니다.
| 기술 | 잘 맞는 상황 | 강점 | 주의점 |
|---|---|---|---|
| JDBC | 원리 이해, 저수준 제어 | 가장 직접적 | 반복 코드와 예외 처리가 많음 |
| JdbcTemplate | 단순 CRUD, 명확한 SQL | JDBC 반복을 크게 줄임 | 복잡 조회는 금방 장황해짐 |
| MyBatis | SQL 제어권이 중요한 조직 | SQL이 명확, DB 특화 활용 쉬움 | 매퍼 관리 비용이 커질 수 있음 |
| JPA | 도메인 모델 중심 CRUD | 변경 감지, 연관관계, 생산성 | fetch 전략과 성능 해석 필수 |
| Spring Data JPA | 반복 리포지토리 감소 | 기본 CRUD와 페이징이 빠름 | JPA 원리 이해를 대신 못 함 |
| Querydsl | 동적 조건 조회, 관리자 검색 | 타입 안전한 동적 쿼리 | 성능을 자동 개선하지 않음 |
기준은 통제권과 생산성 사이의 트레이드오프로 좁혀집니다. SQL을 세밀하게 통제해야 하면 MyBatis나 JdbcTemplate이 유리합니다. 도메인 모델과 변경 감지 이점을 크게 가져가려면 JPA가 유리합니다.
흔한 조합은 단순 CRUD를 Spring Data JPA로, 복잡한 동적 조회를 Querydsl로 나누는 방식입니다. 조회 모델과 쓰기 모델의 성격이 다르면 기술을 섞는 것도 정상적인 선택입니다.
선택의 마지막 기준은 성능입니다. 병목 구간은 기술 교체보다 SQL과 인덱스 점검이 먼저입니다. ORM을 써도 결국 커넥션, 트랜잭션, 실행 계획을 이해해야 장애를 정확히 해석할 수 있습니다.
정리
스프링 데이터 접근 기술은 JDBC가 드러내는 반복과 예외 문제를 단계별로 가려 주는 추상화의 묶음입니다. JdbcTemplate은 자원 반환과 예외 번역을 모아 주지만 SQL 통제는 개발자에게 남깁니다. 예외 추상화는 기술 종속 예외를 DataAccessException 계층으로 번역해 상위 계층 결합을 줄여 줍니다. 데이터소스와 트랜잭션 매니저, 전파 규칙까지 이해해야 커넥션 고갈과 롤백 문제를 제때 잡습니다. 기술 선택은 작업의 성격에 맞춰 추상화 수준을 고르고, 필요하면 여러 기술을 섞는 판단입니다.