[JSP] 27. Java 스타일의 트랜젝션 구현하기(은행 계좌)
트랜젝션 처리에 대해서 소개하려고 한다.
이 주제는 사실 몇 가지 키워드가 있다.
ACID(원자성, 일관성, 고립성, 지속성)을 만족해야 한다.
기본적인 DML만 조작하다가 갑자기 접근하게 되면, 이 내용은 조금 난해할 수도 있다.
1. 자료처리에 있어서 4가지를 모두 충족해야 한다.
원자성(Atomicity)
트랜젝션은 분해가 불가능한 최소의 단위인 하나의 원자처럼 동작한다는 의미.
트랜젝션 내의 모든 연산들은 반드시 한꺼번에 완전하게 전체가 정상적으로 수행이 완료되거나 아니면 연산자체를 수행하지 않음.
일관성(Consistency)
트랜잭션 작업이 시작되지 전에 데이터베이스 상태가 일관된 상태였다면 트랜잭션 작업이 종료된 후에도 일관성 있는 데이터 베이스 상태를 유지해아한다.
예를 들어서 게시판에 글을 쓰는데 제목의 글자 제한이 255자라고 하자.
트랜잭션이 일어나면 이러한 조건을 만족해야하는 것이다. 만약 이를 위반하는 트랜잭션이 있다면 롤백 처리해야 한다.
(문제가 있는 작업이라면, 입력자체를 시키면 안 되는 것이다.)
고립성(Isolation)
트랜잭션 작업 수행 중에는 다른 트랜잭션에 영향을 주어서도 안 되고, 다른 트랜잭션들에 의해 간섭을 받아서도 안 된다는 것을 의미.
다른 트랜잭션의 영향을 받게 되면 영향을 주는 트랜잭션에 의해 자신의 동작이 달라 질 수 있기 때문이다.
트랜젝션 자신은 고립된 상태에서 수행되어야 한다는 것을 의미. 즉 다수의 트랜잭션이 동시에 수행중인 상황에서 하나의 트랜잭션이 완료될 때까지는 현재 실행 중인 트랜잭션의 중간 수행결과를 다른 트랜잭션에서 보거나 참조 할 수 없다.
지속성(Durablility)
일련의 데이터 조작(트렌젝션 조작)을 완료 하고 완료 통지를 사용자가 받는 시점에서 그 조작이 영구적이 되어 그 결과를 잃지 않는 것을 나타낸다. 시스템이 정상일 때 뿐 아니라 데이터베이스나 OS의 이상 종료, 즉 시스템 장애도 견딜 수 있다는 것을 말한다.
2. "은행 계좌"라는 주제
은행 계좌 구현의 문제를 놓고 보면, 돈을 이체시켰는데 시스템 장애로 돈이 빠져나가고 이체가 되지 않아버리면 입금자는 돈을 잃어버리게 된다.
물론 트랜젝션만 가지고 은행 시스템이 구축된 것은 아닐 것이다.
이런 경우를 컴퓨터 프로그래밍으로 표현해보려고 한다.
3. JSP/Servlet으로 구현해도 되고, JUnit 태스트 도구로 접근해도 상관없다.
이 프로젝트를 자습하는 데 있어서, 자유롭게 원하는 형식에서 태스트해도 무방하다.
자바 기반이면, 스프링프레임워크에서도 가능하고, Dynamic Web Project를 생성해도 무방하고 Java Project로 따라해도 괜찮다는 이야기를 하고 소개하겠다.
[태스트 환경]
IDE: Eclipse 2020-06
DB: Oracle 11g XE(Express Edition)
- Maven Projects(Spring 포함)
- JUnit 5
4. 데이터베이스 설계
-- 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;
트리거 작성과 스퀀스 번호 생성에 대해서도 적혀져 있으니깐, 잘 사용해도 무방하다.
파일명: account_tbl.sql
[첨부(Attachments)]
5. 프로젝트 구성도
아래 그림은 프로젝트 구성도이다.
HomeController.java 등은 생략해도 된다.
그림 1, 그림 2. 프로젝트 구성도
6. 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)]
6-1. 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)]
7. JDBCUtil.java(com.website.example.common)
이 프로젝트에서는 close()만 사용하였다. 예비 태스트 용도로 자바 표준의 SQL 방식도 두고 있다.
package com.website.example.common;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import javax.sql.DataSource;
public class JDBCUtil {
private static final String driverName = "oracle.jdbc.driver.OracleDriver";
private static final String jdbcUrl = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
private static final String userId = "{사용자 계정명}";
private static final String userPwd = "{비밀번호}";
public static Connection getConnection() {
try {
Class.forName(driverName);
return DriverManager.getConnection(jdbcUrl, userId, userPwd);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void close(PreparedStatement stmt, Connection conn) {
if (stmt != null) {
try {
if (!stmt.isClosed())
stmt.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
stmt = null;
}
} // end of if
if (conn != null) {
try {
if (!conn.isClosed())
conn.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
conn = null;
}
} // end of if
}
public static void close(ResultSet rs, PreparedStatement stmt, Connection conn) {
if (rs != null) {
try {
if (!rs.isClosed())
rs.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
rs = null;
}
} // end of if
if (stmt != null) {
try {
if (!stmt.isClosed())
stmt.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
stmt = null;
}
} // end of if
if (conn != null) {
try {
if (!conn.isClosed())
conn.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
conn = null;
}
} // end of if
}
}
파일명: JDBCUtil.java
[첨부(Attachments)]
8. MyDataSourceFactory.java(com.website.example.common)
DataSourceFactory이다.
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;
public MyDataSourceFactory() {
String resource = "db.properties";
is = getClass().getClassLoader().getResourceAsStream(resource);
}
public DataSource getMySQLDataSource() {
Properties props = new Properties();
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(){
Properties props = new Properties();
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)]
9. AccountDAO.java (com.website.example.dao)
AccountDAO에 대한 명세이다. DAO는 Data Access Object의 약자이다.
package com.website.example.dao;
import java.sql.SQLException;
import com.website.example.vo.AccountVO;
public interface AccountDAO {
// 계좌 생성
public void create(AccountVO vo) throws SQLException;
// 잔액 조회
public int getBalance(String name) throws SQLException;
// 플러스 계좌
public void plus(String name, int money) throws SQLException;
// 마이너스 계좌
public void minus(String name, int money) throws SQLException;
// 거래
public void transfer(String sender, String receiver, int money) throws SQLException;
}
파일명: AccountDAO.java
[첨부(Attachments)]
10. AccountDAOImpl.java (com.website.example.dao)
AccountDAO에 대한 명세이다. DAO는 Data Access Object의 약자이다.
package com.website.example.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.website.example.common.JDBCUtil;
import com.website.example.vo.AccountVO;
// Transaction - 자바
public class AccountDAOImpl implements AccountDAO {
private final String CREATE_ACCOUNT = "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 = ?";
private DataSource ds = null;
public AccountDAOImpl(DataSource ds) {
this.ds = ds;
}
// 단일 쿼리에서의 트랜젝션 방법
@Override
public void create(AccountVO vo) throws SQLException {
Connection conn = ds.getConnection();
PreparedStatement pstmt = null;
try {
conn.setAutoCommit(false); // 트랜젝션 시작
pstmt = conn.prepareStatement(CREATE_ACCOUNT);
pstmt.setString(1, vo.getName());
pstmt.setInt(2, vo.getBalance());
pstmt.setTimestamp(3, vo.getRegidate());
pstmt.executeUpdate();
conn.commit();
}catch(SQLException e) {
conn.rollback();
e.getMessage();
}finally {
JDBCUtil.close(pstmt, conn);
}
}
@Override
public int getBalance(String name) throws SQLException {
Connection conn = ds.getConnection();
PreparedStatement pstmt = null;
ResultSet rs = null;
int result = 0;
try {
conn.setAutoCommit(false); // 트랜젝션 시작
pstmt = conn.prepareStatement(SELECT_BALANCE);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if ( rs.next() ) {
result = rs.getInt(3);
}
conn.commit();
}catch(SQLException e) {
conn.rollback();
e.getMessage();
}finally {
JDBCUtil.close(pstmt, conn);
}
return result;
}
@Override
public void plus(String name, int money) throws SQLException {
Connection conn = ds.getConnection();
PreparedStatement pstmt = null;
try {
conn.setAutoCommit(false);
// plus, minus 다 확인 후에 commit처리 해야 함.
// 그래서 지금 바로 트랜젝션을 구현하면 안 됨
pstmt = conn.prepareStatement(UPDATE_PLUS);
pstmt.setString(1, name);
pstmt.setInt(2, money);
pstmt.setString(3, name);
pstmt.executeUpdate();
System.out.println(money);
conn.commit();
}catch(SQLException e) {
System.out.println(e.getMessage());
conn.rollback();
}finally {
JDBCUtil.close(pstmt, conn);
}
}
@Override
public void minus(String name, int money) throws SQLException {
Connection conn = ds.getConnection();
PreparedStatement pstmt = null;
try {
conn.setAutoCommit(false);
// 예외 발생시키기(트랜젝션 의도적 발생)
if(true){
throw new SQLException(); // 의도적 예외 발생
}
// plus, minus 다 확인 후에 commit처리 해야 함.
// 그래서 지금 바로 트랜젝션을 구현하면 안 됨
pstmt = conn.prepareStatement(UPDATE_MINUS);
pstmt.setString(1, name);
pstmt.setInt(2, money);
pstmt.setString(3, name);
pstmt.executeUpdate();
conn.commit();
}catch(SQLException e) {
conn.rollback();
System.out.println(e.getMessage());
}finally {
JDBCUtil.close(pstmt, conn);
}
}
@Override
public void transfer(String sender, String receiver, int money) throws SQLException {
Connection conn = ds.getConnection();
PreparedStatement pstmt = null;
try {
conn.setAutoCommit(false);
/*
// 예외 발생시키기(트랜젝션 의도적 발생)
if(true){
throw new SQLException(); // 의도적 예외 발생
}
*/
// plus, minus 다 확인 후에 commit처리 해야 함.
// 그래서 지금 바로 트랜젝션을 구현하면 안 됨
pstmt = conn.prepareStatement(UPDATE_MINUS);
// 주는 분
pstmt.setString(1, sender);
pstmt.setInt(2, money);
pstmt.setString(3, sender);
pstmt.executeUpdate();
// 받는 분
pstmt = conn.prepareStatement(UPDATE_PLUS);
pstmt.setString(1, receiver);
pstmt.setInt(2, money);
pstmt.setString(3, receiver);
pstmt.executeUpdate();
conn.commit();
}catch(SQLException e) {
conn.rollback();
System.out.println(e.getMessage());
}finally {
JDBCUtil.close(pstmt, conn);
}
}
}
파일명: AccountDAOImpl.java
[첨부(Attachments)]
11. AccountService.java (com.website.example.service)
AccountService에 대한 명세이다.
서비스에 대한 정의이다. DB를 정의하는 영역은 아니다.
하지만, Spring Framework로 구현할 때, Spring JDBC 등을 적용하여 트랜젝션 동기화 기능을 구현부에 구현하면, Connection을 사용할 수도 있다.
질의문을 정의하는 영역은 아니다.
package com.website.example.service;
import java.sql.SQLException;
import com.website.example.vo.AccountVO;
public interface AccountService {
public void accountCreate(AccountVO vo) throws SQLException;
public void accountTransfer(String sender, String receiver, int money) throws SQLException;
}
파일명: AccountService.java
[첨부(Attachments)]
12. AccountServiceImpl.java (com.website.example.service)
AccountServiceImpl.java는 AccountService의 구현부이다.
인터페이스의 역할을 정의해보면, 하나의 통로와 같은 역할을 한다고 볼 수 있다.
package com.website.example.service;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.website.example.dao.AccountDAO;
import com.website.example.dao.AccountDAOImpl;
import com.website.example.vo.AccountVO;
public class AccountServiceImpl implements AccountService {
private AccountDAO accountDAO;
private DataSource ds = null;
public AccountServiceImpl(DataSource ds) {
accountDAO = new AccountDAOImpl(ds);
this.ds = ds;
}
@Override
public void accountCreate(AccountVO vo) throws SQLException {
accountDAO.create(vo);
}
public void accountTransfer(String sender, String receiver, int money) throws SQLException{
int balance = accountDAO.getBalance(sender); // 보내는 사람 잔액 체크
if(balance >= money){ // 보내는 돈이 잔액보다 많으면
accountDAO.transfer(sender, receiver, money);
} else{
System.out.println("돈 없음");
//throw new NoMoneyException();
}
}
}
파일명: AccountServiceImpl.java
[첨부(Attachments)]
13. TestMain.java (com.website.example.unit)
JUnit의 태스트 영역이다.
package com.website.example.unit;
import static org.junit.jupiter.api.Assertions.*;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import com.website.example.common.MyDataSourceFactory;
import com.website.example.service.AccountService;
import com.website.example.service.AccountServiceImpl;
import com.website.example.vo.AccountVO;
class TestMain {
@Test
void test() throws SQLException {
DataSource ds = new MyDataSourceFactory().getOracleDataSource();
AccountService service = new AccountServiceImpl(ds);
// 1. 계정 생성 (계정이 없는 경우라면, 주석 풀고 작업해보면 됨.
/*
AccountVO vo = new AccountVO();
vo.setName("홍길동");
vo.setBalance(10000);
vo.setRegidate(Timestamp.valueOf("2020-10-01 10:30:00"));
service.accountCreate(vo);
vo.setName("홍길자");
vo.setBalance(0);
vo.setRegidate(Timestamp.valueOf("2020-10-04 23:20:00"));
service.accountCreate(vo);
*/
// 2. 금전 거래
service.accountTransfer("홍길동", "홍길자", 500);
}
}
파일명: TestMain.java
[첨부(Attachments)]
14. 태스트 해보기 - 결과
코드로 구현하여 태스트를 해봐도 좋을 듯 싶다.
그림 3. 태스트 하기
그림 4. 태스트 하기
트랜젝션을 한번 해보면, 무슨 일이 일어나는지 구경을 조금해보면 좋을 듯 싶다.
15. 맺음글(Conclusion)
setAutoCommit(), conn.commit(), conn.rollback() 사소해보이는 이 코드가 반응하는 것을 관찰해보면, 트랜젝션이 중요하다는 사실을 알 수 있다.
* 참고자료(References)
1. JDBC 트랜젝션 동기화, https://joont.tistory.com/158, Accessed by 2020-10-09, Last Modified 2016-07-11.
-> 추천(50점): 정말 잘 설명해주고 있다.
2. Data Access - Transaction, https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/data-access.html#transaction, Accessed by 2020-10-09, Last Modified 2020-09-15.
3.
'소프트웨어(SW) > JSP' 카테고리의 다른 글
[JSP] 29. 프로젝트 구성 방법 - Eclipse로 살펴보는 보안 프로젝트 구성 (178) | 2020.12.04 |
---|---|
[JSP] 28. Maven (Jaxb-runtime, activation, Jaxb Api, JSTL)을 활용한 XML 생성하기 (175) | 2020.10.11 |
[JSP] 26. JSP/Servlet - InvocationHandler와 Proxy를 이용하여 관점 구현하기 (157) | 2020.10.04 |
[JSP] 25. jQuery와 Ajax 멀티 파일 업로드(개선), POST전송, JSON - 구성하기(2) (157) | 2020.10.03 |
[JSP] 24. jQuery와 Ajax 멀티 파일 업로드(개선), POST전송, JSON - 구성하기(1) (119) | 2020.10.03 |