반복
되는 비핵심기능들)을 보완
jdbc는 여기를 참조
람다
를 사용하면 더 효율적인 개선 가능트랙잭션
의 관리가 쉬움
트랜잭션
은 데이터베이스 뿐 아니라,한번에
처리해야할 일의 묶음, 단위를보장
하는 의미로 사용
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>8.5.27</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
JDBC
연동에 필요한 기능을 제공DB 커넥션풀
기능을 제공MySQL 연결
에 필요한 JDBC 드라이버 제공
DB 커넥션풀은 여기를 참조
JDBC API는 DriverManager 외에 DataSource
를 이용해 DB 연결을 구하는 방법을 다음과 같이 정의
Connection conn = null;
try{
// datasource는 생성자나 설정 매서드를 이용해 주입
conn = dataSource.getConnection();
...
DataSource
를 사용해 DB Connection을 구함Bean
으로 등록하고,주입
받아 사용
// AppCtx.java
@Configuration
public class AppCtx {
/*
* datasource() 매서드를 통해 DataSource를 스프링 Bean으로 등록
* 아래의 destroyMethod = "close"를 지정함으로써,
* 커넥션 풀에 보관된 Connection들을 닫는 매서드 호출까지를 Bean의 생명주기로 지정
*/
@Bean(destroyMethod = "close")
public DataSource dataSource() {
// DataSource 객체 생성
DataSource ds = new DataSource();
// JDBC 드라이버 클래스를 지정, 여기서는 MYSQL 드라이버 클래스 사용
ds.setDriverClassName("com.mysql.jdbc.Driver");
// URL 지정
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
// 계정 & 암호 지정
ds.setUsername("spring5");
ds.setPassword("spring5");
...
return ds;
}
...
@Bean
public MemberDao memberDao() {
return new MemberDao(dataSource());
}
...
}
스프링에서는 DataSource, Connection, Statement 및 ResultSet을 직접 사용하지 않고,
JdbcTemplate
을 이용해 편리
하게 쿼리 실행 가능
DB의 데이터에 접근하는 자세한 방법은 여기를 참조
// MemberDao.java
public class MemberDao {
private JdbcTemplate jdbcTemplate;
// 필요한 dataSorce 객체를 주입받음
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
...
}
SELECT
쿼리 실행을 위한 query()
매서드를 다음과 같이 제공
인덱스 기반
파라미터를 가진 쿼리라면,args
파라미터를 이용하여 각 인덱스의 파라미터의 값을 지정
select * from member where email=?
한 행
을 읽어와 자바 객체
로 변환해주는 매퍼 기능 제공
ResultSet이란 SELECT문을 사용하여 얻어온
레코드 값
들을 테이블의 형태로 갖게 되는객체
// MemberDao.java
public class MemberDao {
...
// 조회 쿼리 기능을 구현
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);
/*
* query() 매서드는 쿼리를 실행한 결과가 존재하지 않으면 0인 list를 반환하므로
* list가 비어있는지 여부로 결과가 존재하지 않는지 확인할 수 있음
*/
return results.isEmpty() ? null : results.get(0);
}
인덱스 파라미터(물음표)
를 포함 List<Member> results = jdbcTemplate.query(
"select * from MEMBER where EMAIL = ?",
new RowMapper<Member>() {...코드생략},
email); // 물음표에 해당하는 값 전달
한 행
으로 실행되는 경우 사용
// MemberDao.java
public class MemberDao {
...
public int count() {
Integer count = jdbcTemplate.queryForObject(
"select count(*) from MEMBER", Integer.class);
return count;
}
}
실행 결과 칼럼이 두 개 이상이면 RowMapper를 파라미터로 전달해 결과 생성
queryForObject() 매서드를 사용하려면 쿼리 실행 결과는
반드시 한 행
이어야 함
만약 행이 없거나, 두 개 이상이면익셉션
이 발생
INSERT
, UPDATE
, DELETE
쿼리 실행을 위한 update()
매서드를 다음과 같이 제공
update() 매서드는 쿼리 실행 결과로 변경된 행의 개수
를 반환
// MemberDao.java
public class MemberDao {
...
public void update(Member member) {
jdbcTemplate.update(
"update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
member.getName(), member.getPassword(), member.getEmail());
}
앞선 예제 코드의 경우, 첫 번째, 두 번째 세 번째 파라미터의 값으로 각각
접근자
를 이용해 인덱스 파라미터의 값을 전달
하지만직접
인덱스 파라미터의 값을 설정해야 할 경우,설정자
를 이용해 설정 가능
- PreparedStatementCreator를 인자로 받는 매서드를 이용해 아래와 같이 사용
jdbcTemplate.update(new PreparedSatementCreator(){ @Override public PreparedStatement createPreparedStatement(Connection con) throws SQLException{ // 파라미터로 전달받은 Connection을 이용해 PreparedStatement 생성 PreparedStatement pstmt = con.prepareStatement( "insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) values (?,?,?,?)"); // 인덱스 파라미터 값 설정 pstm.setString(1, member.getEmail()); pstm.setString(2, member.getPassword()); pstm.setString(1, member.getName()); pstm.setString(1, Timestamp.valueOf(member.getRegisterDateTime())); // 생성한 PreparedStatement 객체 리턴 return pstm; } });
AUTO_INCREMENT
칼럼을 가진 테이블에 값을 삽입하면 해당 칼럼의 값이 자동
으로 생성되므로,INSERT
쿼리에 자동 증가 칼럼에 해당하는 값이 지정되지 않음
jdbcTemplate.update(
"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) values (?,?,?,?)",
member.getEmail(), member.getPassword(), member.getName(),
new Timestamp(member.getRegisterDateTime()));
KeyHolder
를 사용해 다음과 같이 구할 수 있다.
// MemberDao.java
public class MemberDao {
...
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" });
// 여기서 자동 증가하는 key값을 두 번째 파라미터로 전달
// 인덱스 파라미터 값 설정
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());
}
...
GeneratedKeyHolder
객체를 생성keyHolder
를 전달
KeyHolder keyHolder = new
GeneratedKeyHolder()
;
jdbcTemplate.update(new PreparedStatementCreator(){…생략},keyHolder
);
여기까지 진행 후, Main 실행시, The server time zone value ‘KST’ ~~~ 라는
익셉션
이 발생
MySQL 5.1.X 이후 KST타임존
을 인식하지 못하는 에러가 발생
/etc/mysql/mysql.condf.d 설정 변경(경로는 상이할 수 있음)
한 작업
으로 실행할 때 사용Commit
, Rollback
을 통해 전부 반영하거나 기존 상태로 되돌림setAutoCommit(false)
을 이용해 다음과 같이 트랜잭션을 시작하고 반영하거나 취소함
Connection conn = null;
try{
...
conn.setAutoCommit(false); // 트랜잭션 범위 시작
... 쿼리실행
conn.commit(); // 트랜잭션 범위 종료: 커밋
}
catch(SQLException ex){
if(conn != null)
// 트랜잭션 범위 종료: 롤백
try{ conn.rollback(); } catch (SQLException e){}
}
finally{
if(conn!= null)
try{ conn.close(); } catch(SQLException e){}
}
위와 같은 방식은 코드로
직접
트랜잭션의 범위를 설정하기 때문에, 개발자가 커밋이나 롤백을누락
하기 쉬움
이에 다음에 나오는 스프링이 제공하는 방식을 사용
@Transactional
어노테이션을 붙임
// ChangePasswordService.java
public class ChangePasswordService {
private MemberDao memberDao;
/* 어노테이션을 사용해 트랜잭션 범위 설정
* 따라서 memberDao.selectByEmail()에서 실행하는 쿼리와
* member.changePassword()에서 실행하는 쿼리는 한 트랜잭션에 묶임
*/
@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);
}
...
}
플랫폼 트랜잭션 매니저
(PlatformTransactionManager) Bean 설정활성화
설정
// AppCtx.java
@Configuration
// 다음 어노테이션을 통해 @Transactional 어노테이션 활성화
@EnableTransactionManagement
public class AppCtx {
....
@Bean
// 플랫폼 트랜잭션 매니저 Bean 설정
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
AOP
를 사용트랜잭션 처리는 프록시
를 통해 이루어짐
// MainForCPS.java
public class MainForCPS {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppCtx.class);
/*
* 아래 코드를 실행시, ChangePasswordService 객체 대신
* 트랜잭션 처리를 위해 생성한 프록시를 리턴함
*/
ChangePasswordService cps =
ctx.getBean("changePwdSvc", ChangePasswordService.class);
...
}
위의 코드가 실행되기까지 과정은 다음과 같다.
- MainForCPS ->
프록시
(changePasswordService) ->플랫폼 트랜잭션 매니저
-> 실제 기능(ChansgePasswordService)
프록시 객체는@Transactional
어노테이션이 붙은 매서드를 호출하면플랫폼 트랜잭션 매니저
를 사용해 트랜잭션을 시작- 결과에 따라
커밋
하거나롤백
트랜잭션 전파
: 트랜잭션이 이미 실행되고 있는데, 내부에서 또 다른 트랜잭션이 수행되는 경우Propagation.REQUIRED
로서 새로 생성하지 않음
(기존 트랜잭션 그대로 사용)
@Transactional
의 주요 속성
- value / propagation / isolation / timeout
Propagation
의 주요 속성
- REQUIRED / MANDATORY / REQUIRES_NEW / SUPPORTS / NOT_SUPPORTED / NEVER / NESTED
Isolation
의 주요 속성
- DEFAULT / READ_UNCOMMITED / READ_COMMITED / REPEATABLE_READ / SERIALIZABLE