예를 들어서 A의 통장에서 10만원이 출금되어 B의 통장에 입금이 되는 과정을 SQL문으로 보면
A -> UPDATE account SET balance = balance - 100000 WHERE id = 'A';
B -> UPDATE account SET balance = balance + 100000 WHERE id = 'B';
가 될 것이다. 이 작업은 둘 다 정상 처리가 되어야만 성공을 하는 '단일 작업'인데 이렇게 두 개의 update문을 사용해 둘 다 성공해야만 의미가 있는 이러한 작업을 데이터베이스에서는 '트랜젝션'이라고 한다.
select * from account;
// id - balance / id - balance
// A - 1,000,000 / B - 2,000,000
START TRANSACTION; // 트랜젝션을 시작하겠다
UPDATE account SET balance = balance - 100000 WHERE id = 'A';
UPDATE account SET balance = balance + 100000 WHERE id = 'B';
COMMIT; // 지금까지 작업한 내용을 DB에 영구적으로 저장, transaction을 종료
select * from account;
// id - balance / id - balance
// A - 900,000 / B - 2,100,000
START TRANSACTION;
UPDATE account SET balance = balance - 400,000 WHERE id = 'A';
select * from account;
// id - balance / id - balance
// A - 500,000 / B - 2,100,000
ROLLBACK; // 지금까지 작업들을 모두 취소하고 transaction 이전 상태로 되돌린다, transaction을 종료
select * from account;
// id - balance / id - balance
// A - 900,000 / B - 2,100,000
AUTOCOMMIT
- 각각의 SQL문을 자동으로 transaction 처리 해주는 개념
- SQL문이 성공적으로 실행하면 자동으로 commit 한다
- 실행 중에 문제가 있었다면 알아서 rollback 한다
- MySQL에서는 default로 autocommit이 enabled(활성화) 되어 있다
- 다른 DBMS에서도 대부분 같은 기능을 제공한다
// AUTOCOMMIT 활성화 상태
insert into account values ('C', 1000000);
select * from account;
// id - balance / id - balance / id - balance
// A - 900,000 / B - 2,100,000 / C - 1,000,000
SET autocommit=0; // AUTOCOMMIT 비활성화
DELETE FROM account WHERE balance <= 1000000; // 잔고 100만 이하 삭제하셈
select * from account;
// id - balance
// B - 2,100,000
ROLLBACK; // AUTOCOMMIT이 비활성화된 상태에서 DELETE를 진행했기 때문에 되돌릴 수 있다
select * from account;
// id - balance / id - balance / id - balance
// A - 900,000 / B - 2,100,000 / C - 1,000,000
어? 그러면 아까 update를 사용해서 이체할 때는 왜 자동으로 commit이 안된걸까? 🤨
START TRANSACTION;
UPDATE account SET balance = balance - 100000 WHERE id = 'A';
UPDATE account SET balance = balance + 100000 WHERE id = 'B';
COMMIT;
이유는 START TRANSACTION 을 실행함과 동시에 AUTOCOMMIT이 OFF가 되기 때문이다!
COMMIT/ROLLBACK과 함께 transaction이 종료되면 원래의 autocommit 상태로 돌아간다.
(주의해야할 점은 이미 비활성화 되어있던 상태라면 비활성화로 돌아간다)
하지만 실제로 우리가 개발을 할 때 이렇게 SQL문을 직접 DB에 붙어서 입력을 하지는 않는다. 우리는 프로그래밍 언어로 개발을 하기 때문에 이런 트랜젝션 명령어 또한 프로그래밍 언어로 작성을 하게 된다.
public void transter(String fromId, String toId, int amoint) {
try {
Connection connection = ... ;
connection.setAutoCommit(false); // means START TRANSACTION
... // update at fromId
... // update at toId (이체 같은 로직)
connection.commit(); // 정상적으로 처리됐을 경우
} catch (Exception e) {
...
connection.rollback(); // 오류가 발생했을 경우
...
} finally {
connection.setAutoCommit(true); // 성공, 실패의 여부와 상관없이 무조건 실행
}
}
위의 예시에서
... // update at fromId
... // update at toId (이체 같은 로직)
을 제외한 나머지 부분들은 모두 트랜젝션과 관련된 부분들이다. 그런데 이러한 트랜젝션과 관련된 부분과 이체와 관련된 부분들이 같이 있기 때문에 보기가 힘들다.
만약에 Spring이나 Springboot를 통해서 개발을 하게 되면 @Transactional 이라는 어노테이션을 사용할 수 있는데 위의 코드가
@Transactional
public void transter(String fromId, String toId, int amoint) {
... // update at fromId
... // update at toId (이체 같은 로직)
}
이렇게 실제 이체와 관련된 로직만 안에 써주면 된다.
나머지 트랜젝션과 관련된 부분들은 @Transactional 어노테이션이 알아서 처리해준다!