반복되는 비핵심기능들)을 보완
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