스프링은 JDBC에서 반복되는 코드들을 JdbcTemplate 클래스로 제공해준다. 이 클래스를 사용하면 중복되는 코드를 효과적으로 줄일 수 있다.
또한 스프링은 트랜잭션 관리 기능을 애노테이션으로 제공한다.(@Transactional) 커밋과 롤백은 스프링이 알아서 처리해준다.
DB 설치 후 테스트용 기본 테이블을 생성하는 과정은 넘어가도록 하겠다.
책이 조금 오래전에 나왔기 때문에 최신 버전과 호환되지 않을 수 있다.
난 의존성을 내 MySQL 버전에 맞게 다음과 같이 설정하였다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>8.5.65</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
스프링이 제공하는 DB 연동 기능은 DataSource를 사용하여 DB Connection을 구한다.
DB 연동에 사용할 DataSource를 스프링 빈으로 등록하고 DB 연동 기능을 구현한 빈 객체는 DataSource를 주입받아 사용한다.
package config;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppCtx {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
ds.setUsername("spring5");
ds.setPassword("spring5");
ds.setInitialSize(2);
ds.setMaxActive(10);
return ds;
}
}
DataSource 객체를 생성하고 MySQL DB 관련 설정을 하는 코드이다.
package dbquery;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.apache.tomcat.jdbc.pool.DataSource;
public class DbQuery {
private DataSource dataSource;
public DbQuery(DataSource dataSource) {
this.dataSource = dataSource;
}
public int count() {
Connection conn = null;
try {
conn = dataSource.getConnection();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select count(*) from MEMBER")) {
rs.next();
return rs.getInt(1);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if(conn != null)
try {
conn.close();
} catch(SQLException e) {
}
}
}
}
커넥션을 구하고 종료하는 코드이다.
커넥션을 구할 때는 커넥션 풀에서 커넥션을 가져와서 사용한다. 사용이 완료된 후에는 커넥션을 다시 반환한다. 반환된 커넥션은 유휴 상태로 대기하게 된다.
커넥션 풀을 사용하면 미리 커넥션을 생성해놓고 꺼내와서 쓸 수 있기 때문에 성능적으로 더 유리하다.
일정 시간 이상 쿼리가 실행되지 않으면 커넥션을 끊어버리는 DBMS도 있기 때문에 때때로 지속적으로 커넥션을 테스트 해야되는 경우도 존재한다. 이런 경우 다음과 같은 속성을 사용하면 된다.
스프링을 사용하면 JDBC Template을 사용하여 편리하게 쿼리를 실행할 수 있다.
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
JDBCTemplate 객체 생성.
DataSource를 주입받아 JDBCTemplate 객체를 생성하도록 했다.
JDBCTemplate의 query() 메서드는 SELECT 쿼리를 실행한 후 실행 결과를 RowMapper 인터페이스로 자바 객체로 변환해준다.
public Member selectByEmail(String email) {
List<Member> results = jdbcTemplate.query(
"select * from MEMBER where EMAIL = ?",
new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME"),
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
}
}, email);
return results.isEmpty() ? null : results.get(0);
}
임의 클래스로 이메일을 조회하는 코드이다.
결과가 한 행밖에 없을 경우에는 List보다는 Integer 같은 타입으로 결과를 받는 것이 간편할 것이다. 이럴 때는 queryForObject()를 사용하면 된다.
public int count() {
Integer count = jdbcTemplate.queryForObject(
"select count(*) from MEMBER", Integer.class);
return count;
}
queryForObject()는 실행 결과가 0개 거나 두 개 이상이면 익셉션을 발생기키기 때문에 반드시 실행 결과가 한 행이어야 한다.
INSERT, UPDATE, DELETE 쿼리는 모두 update()를 사용한다.
update()의 리턴값은 변경된 행의 개수이다.
public void update(Member member) {
jdbcTemplate.update(
"update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
member.getName(), member.getPassword(), member.getEmail());
}
PreparedStatement의 set 메서드를 사용해서 직접 인덱스 파라미터의 값을 설정해야 하는 경우에는 PreparedStatementCreator를 인자로 받는 메서드를 이용해서 직접 PreparedStatement를 생성하고 설정해야 한다.
MySQL의 AUTO_INCREMENT 컬럼은 행이 추가되면 자동으로 값이 할당되며 주요키 컬럼에 사용된다.
쿼리 실행 후에 해당 키값을 알고 싶다면 JdbcTemplate의 KeyHolder를 사용할 수 있다.
public void insert(Member member) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con)
throws SQLException {
// 파라미터로 전달받은 Connection을 이용해서 PreparedStatement 생성
PreparedStatement pstmt = con.prepareStatement(
"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
"values (?, ?, ?, ?)",
new String[] { "ID" });
// 인덱스 파라미터 값 설정
pstmt.setString(1, member.getEmail());
pstmt.setString(2, member.getPassword());
pstmt.setString(3, member.getName());
pstmt.setTimestamp(4,
Timestamp.valueOf(member.getRegisterDateTime()));
// 생성한 PreparedStatement 객체 리턴
return pstmt;
}
}, keyHolder);
Number keyValue = keyHolder.getKey();
member.setId(keyValue.longValue());
}
update() 메서드는 PreparedStatement를 실행한 후 자동 생성된 키 값을 KeyHolder에 보관한다. 이를 getKey() 메서드를 이용하여 구할 수 있다.
스프링은 호환성을 위해 SQLException을 그대로 전파하지 않고 변환 처리해서 전달한다. 이렇게 하면 연동 기술에 상관없이 동일하게 익셉션을 처리할 수 있기 때문에 개발할 때 편리하다는 장점이 있다.
여러 작업을 하나의 작업으로 묶어서 처리해야 하는 경우 트랜잭션을 사용할 수 있다.
트랜잭션 범위 내에서 실행하고 싶은 메서드에 @Transactional 애노테이션만 붙이면 간단하게 트랜잭션 처리를 할 수 있다.
@Transactional 애노테이션을 사용하려면 스프링 설정에 다음 두 가지를 추가해야 한다.
@Configuration
@EnableTransactionManagement
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
제대로 실행되는지 로그를 확인하기 위해 Logback 기능을 사용해 보도록 하겠다.
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
pom.xml을 변경한 후 resources에 logback.xml을 설정해주면 로그 기능을 사용할 수 있다.
트랜잭션도 공통 기능 중 하나이기 때문에 내부적으로 AOP를 사용한다.
@Transactional 애노테이션이 적용되어 있는 경우 스프링은 트랜잭션 기능을 적용한 프록시 객체를 생성한다. 이 프록시 객체를 @Transactional이 붙은 메서드를 호출하면 PlatformTransactionManager를 사용해서 트랜잭션을 시작하고 성공적으로 실행된 경우 트랜잭션을 커밋한다.
별다른 설정이 존재하지 않는 경우 RuntimeException이 발생했을 때 트랜잭션을 롤백한다. SQLException은 RuntimeException을 상속하고 있지 않으므로 이 경우에는 트랜잭션을 롤백하지 않는다. SQLException이 발생했을 때에도 트랜잭션을 롤백하고 싶다면 @Transactional의 rollbackFor 설정을 사용해야 한다.
Transactional(rollbackFor = SQLException.class)
이와 반대의 기능을 제공하는 noRollbackFor 속성도 존재한다.
여러 익셉션 타입을 지정하고 싶다면 배열 형태로 지정하면 된다.
JDBCTemplate은 진행 중인 트랜잭션이 존재하면 해당 트랜잭션 범위 내에서 쿼리를 실행한다.
만약 새로운 트랜잭션이 시작되게 하고싶다면 propagation 속성값이 REQUIRED_NEW가 되게 하면 된다.