Spring JDBC
는 스프링 프레임워크가 제공하는 JDBC 추상화 계층으로 기존의 JDBC가 가진 불편한 요소들을 해결했다. 즉 Spring JDBC는 기존 JDBC 위에 추가적인 기능과 추상화 계층을 덧붙여서 개발자가 더 쉽게 데이터베이스 작업을 할 수 있도록 만들어졌다. JdbcTemplate
같은 클래스를 통해 JDBC의 번거로운 작업들을 줄여주고, 개발자가 핵심 로직에 집중할 수 있도록 도와준다. Spring JDBC의 특징은 다음과 같다.
JDBC API 위에 추상화 계층 제공 : 기본 JDBC API를 감싸서, 직접 Connection, Statement, ResultSet 관리 같은 반복되는 코드를 줄여준다. JdbcTemplate 같은 클래스로 필요한 기능만 간단하게 사용할 수 있다.
자동 자원 관리 : DB 연결, Statement, ResultSet 등 리소스를 자동으로 열고 닫아줘서, try-catch-finally 구문을 직접 작성할 필요가 없다. 이 덕분에 자원 누수 걱정을 덜 수 있다.
일관된 예외 처리 : JDBC의 SQLException을 스프링의 DataAccessException 계층으로 변환해준다. DBMS에 따라 달라질 수 있는 예외 정보를 공통된 런타임 예외로 처리해서, 코드가 깔끔하고 일관성 있게 에러 처리를 할 수 있다.
간결한 쿼리 실행 메서드 : update(), query(), queryForObject(), batchUpdate() 등의 메서드를 제공해, CRUD 작업을 간단하게 처리할 수 있다. 복잡한 SQL 실행 로직을 단순화시켜준다.
RowMapper를 통한 결과 매핑 : SQL 쿼리 결과인 ResultSet의 각 행을 쉽게 객체로 매핑할 수 있도록 RowMapper 인터페이스를 사용한다. 람다식으로 간단하게 구현 가능해서, 코드가 더 깔끔해진다.
JdbcTemplate
은 스프링에서 제공하는 JDBC 추상화 클래스이다. 기본적으로 JDBC를 사용할 때 번거로운 반복 작업 (커넥션 열고 닫기, 예외 처리 등)을 스프링이 알아서 처리해줘서, 개발자는 SQL 쿼리와 결과 매핑 같은 핵심 로직에만 집중할 수 있게 된다.
주요 특징은 다음과 같다.
자원 관리 자동화
JDBC에서 Connection, Statement, ResultSet 같은 자원 관리를 직접 해야 하는데, JdbcTemplate은 내부적으로 try-catch-finally 구조를 사용해서 이 부분을 대신 처리한다. 덕분에 자원 누수나 예외 처리를 신경 쓰지 않아도 된다.
예외 변환
JDBC에서 발생하는 SQLException을 스프링의 DataAccessException 계층으로 변환해준다. 이렇게 하면 특정 데이터베이스에 종속되지 않고, 공통된 예외 처리 로직을 만들 수 있다.
1. update()
✍️ 예제 코드
// 데이터 삽입 예제
jdbcTemplate.update("INSERT INTO users (name, email) VALUES (?, ?)", "kim", "kim@example.com");
// 데이터 업데이트 예제
jdbcTemplate.update("UPDATE users SET email = ? WHERE name = ?", "new.kim@example.com", "kim");
// 데이터 삭제 예제
jdbcTemplate.update("DELETE FROM users WHERE name = ?", "kim");
2. query()
✍️ 예제 코드
RowMapper<User> rowMapper = (ResultSet rs, int rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
);
List<User> users = jdbcTemplate.query("SELECT * FROM users", rowMapper);
users.forEach(user -> System.out.println(user.getName() + " - " + user.getEmail()));
해당 코드에서 query
메서드는 "SELECT * FROM users" 쿼리를 실행한 후, 반환된 ResultSet의 각 행마다 rowMapper를 적용한다.
즉, 각 행을 User 객체로 변환하고, 그 결과들을 List에 담아서 반환한다.
3. queryForObject()
✍️ 예제 코드
// 특정 사용자 이름으로 이메일 조회 (단일 값)
String email = jdbcTemplate.queryForObject(
"SELECT email FROM users WHERE name = ?", // 실행할 SQL 쿼리문. '?'는 나중에 값으로 대체됨.
new Object[] {"kim"}, // 쿼리문의 '?'에 들어갈 값들. 여기서는 "kim"이 들어감.
String.class // 쿼리 결과로 반환받을 타입.
);
System.out.println("Email: " + email);
4. batchUpdate()
✍️ 예제 코드
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
List<Object[]> batchArgs = new ArrayList<>();
batchArgs.add(new Object[]{"user1", "user1@example.com"});
batchArgs.add(new Object[]{"user2", "user2@example.com"});
batchArgs.add(new Object[]{"user3", "user3@example.com"});
int[] updateCounts = jdbcTemplate.batchUpdate(sql, batchArgs);
System.out.println("Inserted rows count: " + Arrays.toString(updateCounts));
혹은 이렇게 사용 할 수도 있다.
✍️ 예제 코드
int[] counts = jdbcTemplate.batchUpdate(
// 1) 실행할 SQL (파라미터 자리표시자 ? 사용)
"INSERT INTO users(name, email) VALUES(?, ?)",
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
// 2) 배치 인덱스 i에 해당하는 User 객체 가져오기
User u = users.get(i);
// 3) 첫 번째 ? 에 사용자 이름 바인딩
ps.setString(1, u.getName());
// 4) 두 번째 ? 에 사용자 이메일 바인딩
ps.setString(2, u.getEmail());
}
@Override
public int getBatchSize() {
// 5) 총 배치 크기 지정 (users 리스트 크기 만큼 반복)
return users.size();
}
}
);
// 6) 반환된 counts 배열: 각 배치 실행마다 영향을 받은 행 수 (보통 1)
System.out.println("Batch update completed. Number of rows affected: " + Arrays.toString(counts));
RowMapper
는 JdbcTemplate에서 SELECT 쿼리 결과로 나온 ResultSet의 각 행 (row)을 원하는 객체로 변환 (mapping)하는 역할을 하는 함수형 인터페이스이다.
RowMapper<T>
는 제네릭 인터페이스로, T 타입의 객체를 반환한다..✍️ RowMapper 인터페이스 정의
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
여기서 mapRow() 메서드는 ResultSet의 현재 행을 T 타입의 객체로 변환하는 로직을 구현하도록 한다.
JDBC로 SELECT 쿼리를 실행하면 ResultSet이라는 표 형태의 데이터를 받는다. RowMapper를 사용하면 이 ResultSet의 각 행을 원하는 POJO* (Plain Old Java Object)로 간단하게 매핑할 수 있다. 즉, DB에서 가져온 데이터를 자바 객체로 변환해서, 이후 비즈니스 로직에서 편리하게 사용할 수 있게 해준다.
※ POJO (Plain Old Java Object) 란?
간단히 말해, 특정 프레임워크나 라이브러리에 종속되지 않고, 순수하게 자바 문법만을 사용해 만든 객체를 의미한다.
예를 들어, 다음과 같은 User 객체가 있다고 가정하면,
✍️ 예시 코드 작성
public class User {
private Long id;
private String name;
private String email;
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// getter, setter 생략
}
JdbcTemplate을 사용해 SELECT 쿼리를 실행하고, 각 행을 User 객체로 매핑하는 RowMapper의 예시는 이렇게 작성할 수 있다.
✍️ 예시 코드 작성
RowMapper<User> rowMapper = (ResultSet rs, int rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
);
List<User> users = jdbcTemplate.query("SELECT * FROM users", rowMapper);
위 코드에서 람다식으로 구현한 rowMapper는 ResultSet의 각 행을 User 객체로 변환하는 역할을 수행한다. JdbcTemplate의 query() 메서드는 이 rowMapper를 이용해, 쿼리 결과의 모든 행을 List로 만들어준다.
여기서 rowNum은 ResultSet안에서 현재 몇 번째 row (행)인지를 의미한다.
✍️ 예시 코드 작성
@SpringBootApplication
public class JdbcTemplateExampleApplication {
public static void main(String[] args) {
SpringApplication.run(JdbcTemplateExampleApplication.class, args);
}
// CommandLineRunner를 통해 애플리케이션 구동 후 아래 코드를 실행
@Bean
public CommandLineRunner demo(JdbcTemplate jdbcTemplate) {
return args -> {
// 1. update() 메서드 사용 예제
// 데이터 삽입 (INSERT)
int inserted = jdbcTemplate.update(
"INSERT INTO users (name, email) VALUES (?, ?)",
"kim", "kim@example.com"
);
System.out.println("Inserted rows: " + inserted);
// 2. query() 메서드 사용 예제
// RowMapper를 람다식으로 구현해서, ResultSet의 각 행을 User 객체로 변환
RowMapper<User> userRowMapper = (ResultSet rs, int rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
);
// SELECT 쿼리 실행, 결과를 List<User>로 반환
List<User> users = jdbcTemplate.query("SELECT * FROM users", userRowMapper);
// 각 사용자 정보를 출력
users.forEach(user -> System.out.println(user.getName() + " - " + user.getEmail()));
// 3. queryForObject() 메서드 사용 예제
// 단일 값 조회: 특정 사용자의 이메일을 조회
String email = jdbcTemplate.queryForObject(
"SELECT email FROM users WHERE name = ?",
new Object[] {"kim"}, // 쿼리 파라미터 배열
String.class // 반환 타입 지정
);
System.out.println("Email for 'kim': " + email);
// 4. batchUpdate() 메서드 사용 예제
// 여러 건의 데이터를 한 번에 삽입하는 배치 작업
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
List<Object[]> batchArgs = new ArrayList<>();
batchArgs.add(new Object[]{"user1", "user1@example.com"});
batchArgs.add(new Object[]{"user2", "user2@example.com"});
batchArgs.add(new Object[]{"user3", "user3@example.com"});
// 배치 업데이트 실행, 각 쿼리의 결과(영향받은 행 수)를 배열로 반환
int[] updateCounts = jdbcTemplate.batchUpdate(sql, batchArgs);
System.out.println("Batch inserted rows: " + Arrays.toString(updateCounts));
// 추가적으로 update() 메서드를 사용한 UPDATE 예제
int updated = jdbcTemplate.update(
"UPDATE users SET email = ? WHERE name = ?",
"new.kim@example.com", "kim"
);
System.out.println("Updated rows: " + updated);
// update() 메서드를 사용한 DELETE 예제
int deleted = jdbcTemplate.update(
"DELETE FROM users WHERE name = ?",
"kim"
);
System.out.println("Deleted rows: " + deleted);
};
}
}
// 간단한 User POJO 클래스 예제
class User {
private Long id;
private String name;
private String email;
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getter 메서드들
public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}
BeanPropertyRowMapper
는 스프링 JDBC에서 ResultSet의 컬럼 (열) 값을 자바 빈 (POJO) 객체의 프로퍼티에 자동으로 매핑해 주는 편리한 클래스라고 할 수 있다.
자동 매핑 규칙은 다음과 같다.
✍️ 예시 코드 작성
String sql = "SELECT id, name, email FROM users WHERE id = :id";
Map<String, Object> params = Map.of("id", userId);
User user = jdbcTemplate.queryForObject(
sql,
params,
new BeanPropertyRowMapper<>(User.class)
);
위 코드에서 BeanPropertyRowMapper<>(User.class)를 사용하면,
이렇게 자동으로 호출해서 User 객체를 만들어 준다.
장점
주의점
ResultSetExtractor
는 Spring JDBC에서 쿼리 실행 후 결과인 ResultSet 전체를 한 번에 처리해서 원하는 형태의 객체로 만들어주는 역할을 한다. 쉽게 말하면, RowMapper가 한 행씩 매핑하는 반면에 ResultSetExtractor는 ResultSet 전체를 보고 복잡한 가공이나 집계가 필요할 때 쓴다.
예를 들어, 단순한 경우엔 RowMapper로 충분해서 한 행마다 User 객체로 변환하지만,
만약 여러 행의 데이터를 조합해서 하나의 복합 객체를 만들거나, 데이터 집계를 진행해야 한다면 ResultSetExtractor를 사용하게 된다.
✍️ 예시 코드 작성
ResultSetExtractor<List<User>> extractor = rs -> {
List<User> userList = new ArrayList<>();
while(rs.next()){
User user = new User(
rs.getLong("id"),
rs.getString("name"),
rs.getString("email")
);
userList.add(user);
}
return userList;
};
이 코드는 ResultSet 전체를 순회하면서 각 행의 데이터를 User 객체로 만들어 리스트로 반환하는 예시이다.
이처럼 ResultSetExtractor를 사용하면 결과 집합을 자유롭게 가공할 수 있어서, 단순 매핑 이상으로 복잡한 로직을 구현할 때 유용하다.
멋쟁이사자처럼 강의자료