[Spring-Framework] 36. Spring-JDBCTemplate - 트랜젝션 (어노테이션, Java 설정)
이번 글에서 소개할 내용은 스프링 프레임워크의 트랜젝션을 조금 더 다뤄보려고 한다.
어노테이션을 사용하는 데 있어서 방법이 두 가지가 있다.
- 1. Java Config 파일 구성 방식
- 2. (context.xml) XML 파일 구성 방식
* 참고로 스프링 프레임워크는 셋팅이 시작의 반이라고 보면 될 것 같다.
[작업 환경]
IDE: Eclipse 2020-06
Framework: Spring Framework 4.2.4 RELEASE
spring-jdbc
spring-core
spring-tx
mysql-connector-java (8.0.21)
oracle jdbc (WEB-INF/lib에 넣어줘야 함.)
OpenJDK 15 (프로젝트에 적용한 자바 버전은 1.8로 셋팅하였음.)
딱딱한 글도 중요하지만, 영상으로 이게 무엇인지 소개해주겠다. (작동 원리 시연)
영상 1. Spring Framework 4.2.4 RELEASES - Transactions (시연)
1. 프로젝트 구성
프로젝트 구성에 관한 것이다.
그림 1. 프로젝트 구성도
2. POM.xml 구성하기
<properties>
<java-version>1.8</java-version>
<org.springframework-version>4.2.4.RELEASE</org.springframework-version>
<org.aspectj-version>1.6.10</org.aspectj-version>
<org.slf4j-version>1.6.6</org.slf4j-version>
</properties>
(중략)
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency>
(중략)
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
3. Build Path, Project Factes, Java compiler 설정하기
* Build Path -> JRE System Library 1.8, JUnit 5
* Java Compiler -> Compiler compliance level: 1.8
* Project Factes -> Java : 1.8
* 오라클 JDBC: WEB-INF/lib폴더에 ojdbc.jar 파일 넣어주기
4. AccountTbl.sql
-- Transaction 실습 DB (은행 - Account)
-- Oracle 11 - 자동번호 생성 테이블 정의
-- Table 생성 (FOODMENU_TBL)
-- NEW.ID (Table의 id를 가리킴)
CREATE TABLE account_tbl
(
idx NUMBER PRIMARY KEY,
name VARCHAR2(30),
balance NUMBER,
regidate TIMESTAMP
);
-- Sequence 정의
CREATE SEQUENCE account_sequence
START WITH 1
INCREMENT BY 1;
-- Trigger 생성
-- BEFORE INSERT on '테이블명'
CREATE OR REPLACE TRIGGER account_trigger
BEFORE INSERT
ON account_tbl
REFERENCING NEW AS NEW
FOR EACH ROW
BEGIN
SELECT account_sequence.nextval INTO :NEW.IDX FROM dual;
END;
파일명: AccountTBL.sql
[첨부(Attachments)]
AccountTbl.zip
4. db.properties (/src/main/resources)
MYSQL_DB_URL=jdbc:mysql://localhost:3306/dbanme?useUnicode=true&characterEncoding=utf8
MYSQL_DB_USERNAME=
MYSQL_DB_PASSWORD=
ORACLE_DB_URL=jdbc:oracle:thin:@127.0.0.1:1521:xe
ORACLE_DB_USERNAME=HR
ORACLE_DB_PASSWORD=1234
파일명: db.properties
[첨부(Attachments)]
db.zip
5. AccountVO.java (com.website.example.vo)
package com.website.example.vo;
import java.sql.Timestamp;;
public class AccountVO {
private int idx;
private String name;
private int balance;
private Timestamp regidate;
public int getIdx() {
return idx;
}
public void setIdx(int idx) {
this.idx = idx;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public Timestamp getRegidate() {
return regidate;
}
public void setRegidate(Timestamp regidate) {
this.regidate = regidate;
}
}
파일명: AccountVO.java
[첨부(Attachments)]
AccountVO.zip
6. DBConfig.java (com.website.example.common)
package com.website.example.common;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.website.example.dao.AccountDAO;
import com.website.example.dao.AccountDAOImpl;
import com.website.example.service.AccountService;
import com.website.example.service.AccountServiceImpl;
@Configuration
@EnableTransactionManagement
public class DBConfig {
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
public DataSource dataSource() {
DataSource dataSource = new MyDataSourceFactory().getOracleDataSource();
/* Apache DBCP
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/testDb?useUnicode=true&characterEncoding=utf8");
dataSource.setUsername("test");
dataSource.setPassword("test123!@#");
*/
return dataSource;
}
@Bean
public AccountDAO accountDAOImpl() {
AccountDAO dao = new AccountDAOImpl(dataSource());
return dao;
}
@Bean
public AccountService accountServiceImpl() {
AccountService service = new AccountServiceImpl(dataSource());
return service;
}
}
파일명: DBConfig.java
[첨부(Attachments)]
DBConfig.zip
7. RootConfig.java (com.website.example.common)
package com.website.example.common;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@Import({DBConfig.class})
@ComponentScan(basePackages = {"com.website.example"})
//@ComponentScan(basePackages = {"com.local.example.beans", "com.local.example.advisor"})
public class RootConfig {
}
파일명: RootConfig.java
[첨부(Attachments)]
RootConfig.zip
8. MyDataSourceFactory.java (com.website.example.common)
package com.website.example.common;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import com.mysql.cj.jdbc.MysqlDataSource;
import oracle.jdbc.pool.OracleDataSource;
public class MyDataSourceFactory {
private InputStream is = null;
private Properties props = null;
public MyDataSourceFactory() {
String resource = "db.properties";
this.is = getClass().getClassLoader().getResourceAsStream(resource);
this.props = new Properties();
}
public DataSource getMySQLDataSource() {
MysqlDataSource mysqlDS = null;
try {
props.load(is);
mysqlDS = new MysqlDataSource();
mysqlDS.setURL(props.getProperty("MYSQL_DB_URL"));
mysqlDS.setUser(props.getProperty("MYSQL_DB_USERNAME"));
mysqlDS.setPassword(props.getProperty("MYSQL_DB_PASSWORD"));
} catch (IOException e) {
e.printStackTrace();
}
return mysqlDS;
}
public DataSource getOracleDataSource(){
OracleDataSource oracleDS = null;
try {
props.load(is);
oracleDS = new OracleDataSource();
oracleDS.setURL(props.getProperty("ORACLE_DB_URL"));
oracleDS.setUser(props.getProperty("ORACLE_DB_USERNAME"));
oracleDS.setPassword(props.getProperty("ORACLE_DB_PASSWORD"));
} catch (IOException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return oracleDS;
}
}
파일명: MyDataSourceFactory.java
[첨부(Attachments)]
MyDataSourceFactory.zip
9. AccountDAO.java (com.website.example.dao)
package com.website.example.dao;
import java.sql.SQLException;
import com.website.example.vo.AccountVO;
public interface AccountDAO {
void createAccount(AccountVO vo) throws SQLException;
int getBalance(String name);
void minus(String name, int money) throws SQLException;
void plus(String name, int money) throws SQLException;
}
파일명: AccountDAO.java
[첨부(Attachments)]
AccountDAO.zip
MyDataSourceFactory.zip
10. AccountRowMapper.java (com.website.example.dao)
package com.website.example.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import com.website.example.vo.AccountVO;
@Service
public class AccountRowMapper implements RowMapper<AccountVO> {
@Override
public AccountVO mapRow(ResultSet rs, int rowNum) throws SQLException {
AccountVO vo = new AccountVO();
vo.setIdx(rs.getInt(1));
vo.setName(rs.getString(2));
vo.setBalance(rs.getInt(3));
vo.setRegidate(rs.getTimestamp(4));
return vo;
}
}
파일명: AccountRowMapper.java
[첨부(Attachments)]
AccountRowMapper.zip
11. AccountDAOImpl.java (com.website.example.dao)
package com.website.example.dao;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import com.website.example.vo.AccountVO;
public class AccountDAOImpl implements AccountDAO {
// Spring Framework - JDBC
private JdbcTemplate jdbcTemplate = null;
private DataSource ds = null;
private final String INSERT = "insert into account_tbl(name, balance, regidate) values(?, ?, ?)";
private final String SELECT_BALANCE = "select * from account_tbl where name = ?";
private final String UPDATE_MINUS = "update account_tbl set balance = (select balance from account_tbl where name = ?) - ? " +
" where name = ?";
private final String UPDATE_PLUS = "update account_tbl set balance = (select balance from account_tbl where name = ?) + ? " +
" where name = ?";
public AccountDAOImpl(DataSource ds) {
this.jdbcTemplate = new JdbcTemplate(ds);
this.ds = ds;
}
public void createAccount(AccountVO vo) throws SQLException {
this.jdbcTemplate.update(INSERT, vo.getName(), vo.getBalance(), vo.getRegidate());
}
public int getBalance(String name){
Object[] args = {name};
AccountVO vo = this.jdbcTemplate.queryForObject(SELECT_BALANCE, args, new AccountRowMapper());
int result = vo.getBalance();
return result;
}
public void minus(String name, int money) throws SQLException{
this.jdbcTemplate.update(UPDATE_MINUS, name, money, name);
}
public void plus(String name, int money) throws SQLException{
/*
// 예외 발생시키기
if(true){
throw new RuntimeException("런타임 에러");
}
*/
this.jdbcTemplate.update(UPDATE_PLUS, name, money, name);
}
}
파일명: AccountDAOImpl.java
[첨부(Attachments)]
AccountDAOImpl.zip
12. AccountService.java (com.website.example.service)
package com.website.example.service;
import java.sql.SQLException;
import com.website.example.vo.AccountVO;
public interface AccountService {
void accountCreate(AccountVO vo) throws SQLException;
void accountTransfer(String sender, String receiver, int money) throws SQLException;
}
파일명: AccountService.java
[첨부(Attachments)]
AccountService.zip
13. AccountServiceImpl.java (com.website.example.service)
package com.website.example.service;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.website.example.dao.AccountDAO;
import com.website.example.dao.AccountDAOImpl;
import com.website.example.vo.AccountVO;
@Repository
@Transactional
public class AccountServiceImpl implements AccountService{
private AccountDAO accountDAO;
private DataSource ds = null;
public AccountServiceImpl(DataSource ds) {
this.accountDAO = new AccountDAOImpl(ds);
this.ds = ds;
}
@Override
public void accountCreate(AccountVO vo) throws SQLException {
this.accountDAO.createAccount(vo);
}
@Override
public void accountTransfer(String sender, String receiver, int money) throws SQLException {
int balance = accountDAO.getBalance(sender); // 보내는 사람 잔액 체크
if(balance >= money){ // 보내는 돈이 잔액보다 많으면
accountDAO.minus(sender, money);
accountDAO.plus(receiver, money);
} else{
System.out.println("돈 없음");
//throw new NoMoneyException();
}
}
}
파일명: AccountServiceImpl.java
[첨부(Attachments)]
AccountServiceImpl.zip
14. MainTest.java (com.website.example.unit)
package com.website.example.unit;
import java.sql.SQLException;
import java.sql.Timestamp;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.website.example.common.RootConfig;
import com.website.example.service.AccountService;
import com.website.example.vo.AccountVO;
class MainTest {
@Test
void test() throws SQLException {
@SuppressWarnings("resource")
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(RootConfig.class);
AccountService service = (AccountService) applicationContext.getBean("accountServiceImpl");
AccountVO vo = new AccountVO();
/*
// 1. 계정 생성
vo.setName("홍길동");
vo.setBalance(10000);
vo.setRegidate(Timestamp.valueOf("2020-01-20 10:05:20"));
service.accountCreate(vo);
// 2. 계정 생성
vo.setName("홍길자");
vo.setBalance(0);
vo.setRegidate(Timestamp.valueOf("2020-01-20 22:05:20"));
service.accountCreate(vo);
*/
// 3. 거래 처리
service.accountTransfer("홍길동", "홍길자", 500);
}
}
파일명: MainTest.java
[첨부(Attachments)]
MainTest.zip
* 맺음글(Conclusion)
트랜젝션을 어노테이션 방식으로 Java Config 환경설정으로 구성하여 사용하는 방법에 대해서 소개하였다.
[Spring-Framework] 37. Spring-JDBCTemplate - 트랜젝션 (어노테이션, Java - AOP)
- https://yyman.tistory.com/1462
* 참고 자료(References)
1. Spring JDBC, https://velog.io/@koseungbin/Spring-JDBC, Accessed by 2020-10-10, Last Modified 2020-07-08.
2. Spring JDBC using Annotation based configuration, https://www.topjavatutorial.com/frameworks/spring/spring-jdbc/spring-jdbc-using-annotation-based-configuration/, Accessed by 2020-10-10, Last Modified 2016-02-14.