JDBC를 사용해봤다면 DB에 연결하기 위해 반복적인 코드가 너무 많은 경험이 있을 것이다.
구조적인 반복을 줄이기 위해 JdbcTemplate 클래스를 사용할 수 있다.
JdbcTemplate를 사용하기 위해서는 pom.xml에 다음 dependency를 추가한다.
//JdbcTemplate 등 JDBC 연동에 필요한 기능을 제공한다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
//DB 커넥션 풀 기능을 제공한다.
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>8.5.27</version>
</dependency>
//Mysql 연결에 필요한 JDBC 드라이버 제공
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
커넥션 풀 한줄 요약
DB에 접근하는 일정 연결 객체 생성해 놓고 빌려주고 반납하게 해서 자원 절약!
DataSource
DataSource는 연결을 위한 팩토리라고 보면 될 것 같다.
스프링이 제공하는 DB 연동 기능은 DataSource를 사용해 DB Connection을 구한다.
DB연동에 사용할 DataSource를 스프링 빈으로 등록하고 연동 기능을 구현한 빈 객체는 DataSource를 주입받아 사용한다.
@Configuration
public class DbConfig {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
//DataSource 객체 생성 = 연결 객체 생성
DataSource ds = new DataSource();
//드라이버 클래스 지정
ds.setDriverClassName("com.mysql.jdbc.Driver");
//연결할 URL 지정.
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
// id, password 입력
ds.setUsername("spring5");
ds.setPassword("spring5");
// 초기 연결 크기 지정
ds.setInitialSize(2);
// 초기 연결 객체 10개 생성
ds.setMaxActive(10);
ds.setTestWhileIdle(true);
ds.setMinEvictableIdleTimeMillis(60000 * 3);
ds.setTimeBetweenEvictionRunsMillis(10 * 1000);
return ds;
}
}
물론 위에 DB설정은 내께 아니다.
Tomcat JDBC의 주요 프로퍼티
Connection Pool 은 연결을 생성하고 유지한다.
사용자가 연결을 요청하면 해당 커넥션은 활성화 상태가 되고 , 반환하면 유휴(Idle) 상태가 된다.
연결 요청 = Connection conn = dataSource.getConnection() ;
반환 = conn.close();
설정 메서드 | 설명 |
setInitialSize( 숫자 ) | 커넥션 풀 생성시 초기 커넥션 갯수. 10개가 기본 값. |
setMaxActive( 숫자 ) | 최대 커넥션 갯수. 기본값 : 100개 |
setMaxIdle( 숫자 ) | 유지할 수 있는 최대 커넥션 갯수. |
setMinIdle( 숫자 ) | 최소 커넥션 갯수. 기본값은 initialSize에서 가져옴. |
setMaxWait( 숫자 ) | 대기할 최대시간. 기본값 : 30초 |
setMaxAge( 숫자 ) | 최초 연결 후 커넥션의 유효시간. 기본값 : 0 ( 무한 ) |
setValidationQuery( 문자 ) | 커넥션 유효한지 검사할 때 쓸 쿼리. 기본값 : null (검사 안함) |
setValidationQueryTimeout ( 숫자 ) | 검사 쿼리 최대 실행시간. 기본값 : -1 ( 비활성화 ) |
setTestOnBorrow( boolean ) | 커넥션 반환할 때 검사 여부 . 기본 : false |
setTestWhileIdle( boolean ) | 커넥션이 풀에 있을 때 검사할지 여부. 기본 : false |
setMinEvictableIdleTimeMillis(int) | 커넥션 풀에 유휴 상태로 유지할 최소시간. 기본값 : 60000 Millisecond ( 1분 ) |
setTimeBetweenEvictionRunsMillis( int ) | 커넥션 풀의 유휴 커넥션을 검사할 주기. 기본 : 5000 Millisecond ( 5초 ) . 1초 이하로 설정하면 안됨. |
JdbcTemplate 생성과 사용
1. JdbcTemplate 생성하기
public class MemberDao {
//JdbcTemplate 선언
private JdbcTemplate jdbcTemplate;
//DataSource 의존성 주입 ( DI )
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
2. JdbcTemplate를 쓰는 MemberDao 빈 등록
@Bean
public MemberDao memberDao() {
return new MemberDao(dataSource());
}
1. 조회 쿼리 실행
List<T> query( String sql , RowMapper<T> rowMapper)
List<T> query( String sql , Object[] args , RowMapper<T> rowMapper)
List<T> query( String sql , RowMapper<T> rowMapper , Object.. args)
sql = 전달 받은 쿼리 .
RowMapper<T> = ResultSet의 결과 자바 객체로 변환.
args = 인덱스 기반 파라미터 가진 쿼리면 args 파라미터로 각 인덱스 파라미터의 값 지정.
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,name); // 인덱스 파라미터가 여러개인 경우. 콤마로 구분한다.
return results.isEmpty() ? null : results.get(0);
}
인덱스 파라미터가 여러개인 경우란 ?
위에서는 파라미터로 String email만 받았지만 , email과 name을 받는다고 쳤을 때 ,
"select * from member where email = ? and name= ? " 이렇게 위치홀더( 물음표 : ? )가 있을 때
해당하는 값을 순서대로 넣어주기 위한 것 .
그리고 위처럼 RowMapper를 일회용으로 선언해서 작성해도 된다.
반대로 , 여러 곳에서 동일한 RowMapper를 사용할 때에는 따로 RowMapper를 구현한 클래스를
만들어서 사용하면 코드의 중복을 막을 수 있다.
결과가 한개일 때
public int count() {
Integer count = jdbcTemplate.queryForObject(
//첫번째 인자 : sql 문 , 두번째 인자 : 칼럼을 읽을 때 사용할 타입
"select count(*) from MEMBER", Integer.class);
return count;
}
위와 같이 결과 값이 select의 결과 값이 회원의 숫자의 갯수인 경우 queryForObject를 사용해서
RowMapper 대신 Integer 클래스를 사용할 수 있다.
2. JdbcTemplate을 이용한 변경 쿼리 실행
조회를 제외한 Insert , Update , Delete 쿼리는 update() 메서드를 사용한다.
int update( String sql )
int update( String sql , Object... args )
매개 변수는 위와 비슷하게 질의문과 , ?에 넣을 인자들을 넣어주면 된다.
PrepareStatement를 사용해서 ? 에 넣을 인자들을 넣어 줄 수 있는데 ,
PrepareStatementCreator 를 인자로 받는 메서드를 이용해서 직접 PrepareStatement를 생성, 설정해야 한다.
jdbcTemplate.update( new PreparedStatementCreator(){
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException{
//파라미터로 전달받은 Connection을 이용해 PreparedStatement 생성
PreparedStatement ps = con.prepareStatement(
" insert into Member ( email , password , name , regdate ) values( ? , ? , ? , ?)");
//인덱스 파라미터 선택
ps.setString(1,member.getEmail());
ps.setString(2,member.getPassword());
ps.setString(3,member.getName());
ps.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
return ps ;
}
});
KeyHolder 이용해서 자동 생성 ( Auto_Increment ) 값 구하기
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" });
// 후략 ... 바로 위에 있는 코드가 들어가 있음.
}, keyHolder);
Number keyValue = keyHolder.getKey();
member.setId(keyValue.longValue());
}
GeneratedKeyHolder : 자동 생성된 키 값을 구해주는 KeyHolder 구현 클래스
Number keyValue : createPreparedStatement 두번째 인자로 자동 생성된 키 값 객체를 전달 받음.
update 메서드는 PrepareStatement를 실행 후 자동 생성된 키 값을 KeyHolder에 보관함.
.getKey() 메서드로 구하고 intValue , LongValue() 메서드로 변환해서 사용.
트랜잭션 처리
트랜잭션 : 0 or 100 . 완전히 안되거나 , 완전히 되거나 둘 중 하나. 은행 등에서 이체를 생각하면 쉬움.
JDBC에서의 처리
Connection conn = null;
try{
conn = DriverManager.getConnection(jdbcUrl , user , pw );
conn.setAutoCommit(false); // 자동 커밋 취소 . ( 트랜잭션 범위 시작 )
// ..... 트랜잭션 구문 있다고 상상
conn.commit(); // 트랜잭선 범위 끝. 커밋함.
}catch(SQLException ex){
if(conn==null)
// 롤백.
try{conn.rollback(); } catch( SQLException ex ){}
}
자동 커밋을 지우고 , 트랜잭션 범위를 돌리고 , 이상없다면 커밋 , 이상있다면 롤백을 통해서 처리했었다.
Spring에서 @Transactional 을 이용한 트랜잭션 처리
설정
- 플랫폼 트랜잭션 매니저 빈 설정
- @Transactional 어노테이션 달기.
@Configuration
@EnableTransactionManagement
public class AppCtx {
//.. 중략
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
//.. 후략
PlatformTransactionManager는 스프링이 제공하는 트랜잭션 매니저 인터페이스다.
DataSourceTransactionManager 는 PlatfromTransactionManager 인터페이스를 상속받은 것.
트랜잭션을 사용할 객체를 만들었다면
@EnableTransactionManagement 를 사용하여 @Transactional 어노테이션이 붙은 메서드를 트랜잭션으로 관리한다.
사용 .
public class ChangePasswordService {
private MemberDao memberDao;
@Transactional
public void changePassword(String email, String oldPwd, String newPwd) {
Member member = memberDao.selectByEmail(email);
if (member == null)
throw new MemberNotFoundException();
member.changePassword(oldPwd, newPwd);
memberDao.update(member);
}
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
}
트랜잭션 처리할 구문에 @Transactional 어노테이션만 붙이면 됨.
selectByEmail 구문과 changePassword 구문은 한 트랜잭션 안에 묶이게 된다.