트랜잭션(Transaction)

기록하는 용도·2022년 9월 6일
0

트랜잭션(Transaction)

작업단위
더 이상 쪼갤 수 없는 작업 단위 -> 원자성
데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위



TCL (Transaction Control Language)

	- COMMIT : 변경된 작업 내용을 실제 데이터베이스에 반영
	- ROLLBACK : 변경된 작업 내용을 취소하고 이전 상태로 되돌림           

사례) 카드를 발급하면 포인트 지급을 약속한다고 하자.
카드 발급 트랜잭션 -> 1.카드 발급 + 2.포인트 발급으로 이루어질 것이다.
1번 2번 두 사항이 모두 정상적으로 수행되었을 때 실제 작업 내용이 DB에 저장되어야한다. -> commit
만약 1번 2번의 세부 작업에 문제가 발생할 경우에는, 예를 들면 카드는 발급되고 포인트 지급 시 문제가 발생 될 경우에는 작업 내용을 취소하고 원상태로 되돌려야한다. -> rollback



Java Application 상에서 Transaction 처리

try{
//자동 커밋에서 수동 커밋 모드로 변경 -> 수동 커밋 모드 : 직접 커밋을 명령하기 전까지는 실제 db에 반영되지않는다.
	connection.setAutoCommit(false);
	세부작업1 카드발급
	세부작업2 포인트발급
			
	모든 세부 작업이 정상적으로 실행되었을 때 실제 db에 반영하도록 
	connection.commit(); 를 실행한다.
}catch(Exception e){
	connection.rollback(); //작업 취소하고 원상 복귀시킨다.
}



예제

1. 테이블 생성
먼저 card라는 테이블과 point라는 테이블을 생성한다.
두개의 각 테이블은 필요한 컬럼이 id를 제외하고 다르다.

CREATE TABLE card(
	id VARCHAR2(100) PRIMARY KEY,
	name VARCHAR2(100) NOT NULL
)

CREATE TABLE point(
	id VARCHAR2(100) PRIMARY KEY,
	point_type VARCHAR2(100) NOT NULL,
	point NUMBER NOT NULL
)

SELECT * FROM card;
SELECT * FROM point;

SELECT 문으로 각자 내용은 현재 비어있음을 확인할 수 있다.



2. main
dao.registerCardAndPointVer1 메소드를 호출하며 두가지 경우를 나눠서 결과를 확인하려고한다.
정상수행되는 코드와 비정상 수행되는 코드이다.

import model.CardDAO;

public class TestTransaction1 {
	public static void main(String[] args) {
		CardDAO dao = new CardDAO();
		try {
			//정상 수행
			dao.registerCardAndPointVer1("spring", "성시경","스벅",10000);
            
			//비정상 수행
			//dao.registerCardAndPointVer1("java", "아이유",null,90000);
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}

CardDAO 객체를 생성한다.



2-1. 정상 수행이라고 주석 처리 된 부분의 코드를 보자.
registerCardAndPointVer1메소드를 통해서 매개변수로 "spring", "성시경", "스벅", 10000을 넣어준다.




3. CardDAO
registerCardAndPointVer1메소드를

dao.registerCardAndPointVer1("spring", "성시경","스벅",10000)

코드로 매개변수를 넣어주며 호출했다.
그리고 이 호출된 메소드를 통해 sql문으로 각 테이블에게 정보를 insert 하는 것이다.

insertCardSql문에 sql문을 넣어준다.

String insertCardSql = "insert into card(id,name) values(?,?)";

card 테이블에 대한 정보를 insertCardSql에 선언했다.

pstmt = con.prepareStatement(insertCardSql);
pstmt.setString(1, id);
pstmt.setString(2, name);

위에 선언한 insertCardSql을 pstmt 변수에 명시한 후 첫번째 물음표, 두번째 물음표에 들어갈 값을 setString 해준다.
여기서 id와 name은 매개변수로 받은 id, name이다.

int cardResult = pstmt.executeUpdate();
System.out.println("카드 등록 OK " + cardResult);

insert문이기때문에 executeUpdate() 메소드로 결과값을 int로 반환하고, 반환된게있다면 1, 없다면 0을 반환할 cardResult변수를 선언해 할당한다. 그리고 그 값을 넘겨준다.

그리고 넘겨받은 매개변수 중 id와 name 이외에도 point_type, point를 처리해줘야한다.
이 때 하나의 컨넥션 상에서 여러개의 sql을 실행하기위해 pstmt를 close한다.

pstmt.close();

이렇게 pstmt의 연결을 끊어주고 Connection은 그대로 유지할 수 있기에 역순으로 pstmt를 close()한다.
말하는 객체인 pstmt를 만들어 실행하고 (update)
이 때 Connection은 유지가 되고 말하는 것만 바꾸는 것이다.(PreparedStatement)
Connection은 살아있고 그대로 유지하며 말만 새롭게 만들어 다시 실행시키는것이다.

String insertPointSql = "insert into point(id, point_type, point) values(?,?,?)";
pstmt = con.prepareStatement(insertPointSql);
pstmt.setString(1, id);
pstmt.setString(2, pointType);
pstmt.setInt(3, point);

int pointResult = pstmt.executeUpdate();
System.out.println("포인트 등록 ok : " + pointResult);

이어서 point 테이블에 필요한 point_type, point를 받아온 매개변수에서 setString으로 순서에 맞게 처리한다.

그리고 executeUpdate() 메소드를 실행한다.

실행 결과

카드 등록 OK 1
포인트 등록 ok : 1

id "spring"값을 갖는 데이터가 card 테이블과 point 테이블에 등록되었다.

card 테이블

point 테이블

전체 코드는 아래와 같다.

public class CardDAO {
	public void registerCardAndPointVer1(String id, String name, String pointType, int point) {
		Connection con = null;
		PreparedStatement pstmt = null;
		try {
			con = DriverManager.getConnection(DbConfig.URL, DbConfig.USER, DbConfig.PASS);
			String insertCardSql = "insert into card(id,name) values(?,?)";
			pstmt = con.prepareStatement(insertCardSql);
			pstmt.setString(1, id);
			pstmt.setString(2, name);
			int cardResult = pstmt.executeUpdate();
			System.out.println("카드 등록 OK " + cardResult);

			// 하나의 컨넥션 상에서 여러개의 sql을 실행하기위해 pstmt를 close한다.
			pstmt.close();
			// 컨넥션 끊고(그림판) 새로 안만들어줘도돼
			String insertPointSql = "insert into point(id, point_type, point) values(?,?,?)";
			pstmt = con.prepareStatement(insertPointSql);
			pstmt.setString(1, id);
			pstmt.setString(2, pointType);
			pstmt.setInt(3, point);

			int pointResult = pstmt.executeUpdate();
			System.out.println("포인트 등록 ok : " + pointResult);
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {

		}
	}

	public void closeAll(PreparedStatement pstmt, Connection con) throws SQLException {
		if (pstmt != null)
			pstmt.close();
		if (con != null)
			con.close();
	}

실행 결과, card 테이블과 point 테이블에 데이터가 들어간 것을 확인할 수 있다.



2-2

dao.registerCardAndPointVer1("java", "아이유",null,90000);

비정상 수행의 예제를 보자.
table의 제약조건중 point_type에 not null이라는 조건에 위배되었다.
이 코드를 실행하게 된다면 순차적으로 card 테이블에는 정보가 들어갈 것이고 point테이블에는 정보가 들어가지않을것이다.


하지만 이와같은 문제는 일어나서는 안될 상황이다.
카드 등록이 되더라도 포인트 등록 과정에서 오류가 발생한다면 카드 등록 조차도 없었던 일로 만들어야하는것이 정상이다.

카드 등록은 가능하지만, null값으로 인해 카드 테이블에 대한 정보가 처리된 후 point 테이블에 대해 정보에 대해 예외처리가 발생할 수 있다.

이처럼 누군가에게 송금을 하는 과정에서 문제가 발생한다면, (송금은 안되었지만 내 통장에 돈이 빠져나가는 일어나서는 안되는일) 아예 작업하려던 과정 자체를 되돌리는것이 ROLLBACK이다.

예외처리를 통해 이 문제를 해결해보자.




3.

JDBC 기본인 자동 커밋(AutoCommit) 모드를 수동커밋(MannualCoomit) 모드로 변경한다.

con = DriverManager.getConnection(DbConfig.URL, DbConfig.USER, DbConfig.PASS);
con.setAutoCommit(false); 

default는 true이고 connection 한다음 바로 자동 커밋 false로 만든다.

int pointResult = pstmt.executeUpdate();
System.out.println("포인트 등록 ok : " + pointResult);

con.commit(); 

이외에 코드는 같고, try 영역의 마지막 부분에서 commit을 해줘야한다.
출력문까지 코드가 수행된다면, 모든 세부 작업이 정상적으로 수행되었음을 의미하므로 이 때 실제 db에 작업 결과를 반영하도록 명시적으로 commit을 실행한다.

catch (Exception e) {
	con.rollback();
	System.out.println("카드 트랜잭션 내에서 문제 발생하여 rollback 즉 작업 취소하고 원상태로 돌림");
	throw e;

위의 대처방안을 실행한 후 다시 호출한 측으로 예외를 전달하고싶다면 이처럼한다.

public void registerCardAndPointVer2(String id, String name, String pointType, int point) throws SQLException {
		Connection con = null;
		PreparedStatement pstmt = null;
		try {
			
			
			String insertCardSql = "insert into card(id,name) values(?,?)";
			pstmt = con.prepareStatement(insertCardSql);
			pstmt.setString(1, id);
			pstmt.setString(2, name);
			int cardResult = pstmt.executeUpdate();
			System.out.println("카드 등록 OK " + cardResult);

			// 하나의 컨넥션 상에서 여러개의 sql을 실행하기위해 pstmt를 close한다.
			pstmt.close();
			// 컨넥션 끊고(그림판) 새로 안만들어줘도돼
			String insertPointSql = "insert into point(id, point_type, point) values(?,?,?)";
			pstmt = con.prepareStatement(insertPointSql);
			pstmt.setString(1, id);
			pstmt.setString(2, pointType);
			pstmt.setInt(3, point);

			int pointResult = pstmt.executeUpdate();
			System.out.println("포인트 등록 ok : " + pointResult);
			//try 영역의 마지막 부분 : 모든 세부 작업이 정상적으로 수행되었음을 의미
			//이 때 실제 db에 작업 결과를 반영하도록 명시적으로 commit을 실행한다.
			con.commit(); //메모리에 있는걸 실제 db에
			System.out.println("카드와 포인트 발급 작업이 정상적으로 수행되어 db에 commit");
		} catch (Exception e) {
			con.rollback();
			System.out.println("카드 트랜잭션 내에서 문제 발생하여 rollback 즉 작업 취소하고 원상태로 돌림");
			//위의 대처방안을 실행한 후 다시 호출한 측으로 예외를 전달하고싶다면 아래처럼한다.
			throw e;
		} finally {

		}

	}
dao.registerCardAndPointVer2("spring", "성시경","스벅",10000);

카드 등록 OK 1
포인트 등록 ok : 1
카드와 포인트 발급 작업이 정상적으로 수행되어 db에 commit

이처럼 정상수행될때에는 데이터가 db에 반영될 수 있도록 마지막에 commit을하고

dao.registerCardAndPointVer2("java", "아이유",null,90000);

이와 같이 비정상 수행일때에는 카드 등록은 되지만, 문제가 발생했을때 rollback으로 원상태로 돌릴 수 있도록 한다.
이때 실제 database에는 card테이블에서조차 이 데이터는 존재하지않는다.

0개의 댓글