트랜잭션이란? 특징과 사용법에 대해 쉽게 알아보자

반응형

트랜잭션(Transaction)

트랜잭션은 DB의 상태를 변경시키기 위해 수행하는 작업 단위입니다.

여기서 DB의 상태를 변경시킨다는 SELECT, UPDATE, INSERT, DELETE 와 같은 쿼리를 날려 연산을 수행하는 것입니다.

트랜잭션을 좀 더 쉽게 풀이해서 이야기 하자면, 트랜잭션을 한국어로 번역하면 "거래"

즉, DB에서 트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것이라고 할 수 있습니다.

 

그런데 여기서 거래를 안전하게 처리하도록 보장해준다는 말이 무엇일까요?

이 얘기를 쉽게 이해하기 위해 우리가 일상속에서 자주 이용하는 계좌이체를 예시로 들 수 있습니다.

 

계좌이체 예시 ( 트랜잭션이 없을 경우 )

짱구와 철수가 있습니다.

짱구와 철수는 각각 계좌에 10000원씩 있습니다.

짱구가 철수한테 빌린돈을 갚기 위해 철수에게 2천원을 보냅니다.

그런데 여기서 문제가 발생합니다.

짱구는 철수에게 정상적으로 2천원을 보냈지만, 철수가 돈을 받는과정에서 오류가 발생해 2천원을 받지 못했습니다.

원래라면 짱구가 철수에게 2000원 계좌이체를 할경우 다음과 같은 작업이 이뤄져야합니다.

1. 짱구의 잔고를 2000원 감소

2. 철수의 잔고를 2000원 증가

 

하지만 위의 경우에서는 1번은 정상적으로 수행됬지만, 2번에서 오류가 발생해

짱구는 돈을 보냈지만 철수는 받지 못해 짱구의 돈 2000원만 사라지는 대참사가 발생했습니다.

 

트랜잭션 기능 사용

DB가 제공하는 트랜잭션 기능을 사용하면 commitrollback으로 정상적인 작업이 가능하도록 할 수 있습니다.

위 처럼 1번은 성공했지만 2번은 실패하는, 중간에 문제가 발생했을 경우 거래 전의 상태로 돌아 갈 수 있는데,

작업 중 하나라도 실패를 해서 거래 이전으로 되돌리는 것을 롤백(rollback)이라고 합니다.

롤백을 하게 되면 결과적으로 짱구의 잔고가 감소하지 않게 됩니다.

그리고 모든 작업이 정상적으로 성공하는 경우 데이터베이스에 정상 반영하는 것을 커밋(commit)이라고 합니다.

 

정리하자면

데이터 변경 쿼리를 실행하고 데이터베이스에 그 결과를 반영하려면 커밋 명령어인 commit 을 호출하고,

결과를 반영하고 싶지 않으면 롤백 명령어인 rollback 을 호출하면 됩니다.

커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이기 때문에, 해당 트랜잭션을 시작한 세션(사용자) 에게만 변경 데이터가 보이고 다른 세션(사용자)에게는 변경 데이터가 보이지 않습니다.

 ( SELECT, UPDATE, INSERT, DELETE 모두 같은 원리로 동작 )

 

DB 세션

사용자가 DB 서버에 접근하면, 사용자는 DB 서버에 연결을 요청하고 커넥션을 맺게 됩니다.

이때 DB 서버는 내부에 세션이라는 것을 만들고, 앞으로 해당 커넥션을 통한 모든 요청은 이 세션을 통해서 실행하게 됩니다.

쉽게 이야기해서 개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행합니다.

세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료합니다.

그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있습니다.

사용자가 커넥션을 닫거나, 또는 DB 관리자가 세션을 강제로 종료하면 세션은 종료됩니다.

( 커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 생성. )


자동 커밋과 수동 커밋

트랜잭션을 사용하려면 먼저 자동 커밋과 수동 커밋을 이해해야 합니다.

자동 커밋으로 설정하면 각각의 쿼리 실행 직후에 자동으로 커밋을 호출 해 커밋이나 롤백을 직접 호출하지 않아도 되는 편리함이 있습니다.

하지만 쿼리를 하나하나 실행할 때 마다 자동으로 커밋이 되어버리기 때문에 우리가 원하는 트랜잭션 기능을 제대로 사용할 수 없게 됩니다.

 

따라서 commit , rollback 을 직접 호출하면서 트랜잭션 기능을 제대로 수행하려면 자동 커밋을 끄고 수동 커밋을 사용해야 합니다. ( 보통은 자동 커밋이 Default 값 )

수동 커밋을 사용하면 그것을 "트랜잭션을 시작"한다라고 합니다.

set autocommit false; //수동 커밋 모드 설정
insert into member(member_id, money) values ('짱구',10000);
insert into member(member_id, money) values ('철수',10000);
commit; //수동 커밋

( 수동 커밋 모드나 자동 커밋 모드는 한번 설정하면 해당 세션에서는 계속 유지, 중간에 변경 가능 )

 

다시 보는 계좌이체 예제

위에서 설명했던 계좌이체 예제를 h2 콘솔을 이용해 각 각 다른 2개의 세션을 가지고 설명해보겠습니다.

 

우선 짱구와 철수가 각각 계좌에 10000원 씩 갖고 있다고 가정하고 쿼리문으로 세팅해줍니다.

insert into member(member_id, money) values ('짱구',10000);
insert into member(member_id, money) values ('철수',10000);

SELECT * FROM MEMBER 쿼리로 확인해보면 자동 커밋으로 두 세션에 짱구와 철수의 계좌 잔액이 반영되어있습니다.

좌 : 세션 1, 우 : 세션 2

 

여기서 짱구가 철수에게 빌린돈이 있어 돈을 값기 위해 2000원을 보냅니다.

그런데 내부 쿼리 오류로 다음과 같이 Column명이 잘못 입력이 되었습니다.

set autocommit false; //수동 커밋 모드
update member set money=10000 - 2000 where member_id = '짱구'; //성공
update member set money=10000 + 2000 where member_idddddd = '철수'; //쿼리 예외발생

이 경우 해당 컬럼을 찾을 수 없다는 오류가 뜨게 되는데 오류가 발생하지 않은 짱구의 쿼리는 정상적으로 실행되어 짱구만 돈을 보내고 철수는 받질 못한 대참사가 생기게 됩니다.

// 오류 발생
Column "MEMBER_IDDDDDD" not found; SQL statement:
update member set money=10000 + 2000 where member_idddddd = '철수' [42122-200] 42S22/42122

이 경우에 우리는 수동 커밋을 사용했기 때문에 commit을 하기전에는 세션2에는 반영되지 않습니다.

그래서 rollback; 쿼리를 날려 트랜잭션을 시작하기 전 단계로 데이터를 복구할 수 있습니다.

 

좌 : 세션 1, 우 : 세션2  (rollback 전)

 

좌 : 세션 1, 우 : 세션 2  (rollback 후)

 

이제 다시 쿼리의 오타를 수정해 수동 커밋으로 쿼리를 보내봅시다.

set autocommit false; //수동 커밋 모드
update member set money=10000 - 2000 where member_id = '짱구';
update member set money=10000 + 2000 where member_id = '철수';

세션 1에는 정상적으로 반영이 되었고 두 쿼리 모두 오류없이 정상적으로 실행되었습니다.

아직 커밋전이기 때문에 세션2에는 반영이 되지 않았습니다.

좌 : 세션 1, 우 : 세션 2  (commit 전)

쿼리가 정상적으로 실행되었으니 이제 commit으로 결과를 반영합니다.

좌 : 세션 1, 우 : 세션 2  (commit 후)

세션2에도 결과가 반영되어 두 세션의 짱구의 잔고는 - 2000, 철수의 잔고는 +2000원이 된 것을 볼 수 있습니다.

 


트랜잭션의 특징 (ACID)

트랜잭션은 "ACID"라고 하는 원자성, 일관성, 격리성, 지속성을 보장해야한다.

각 특징을 자세히 알아보자.

 

원자성 (Atomicity)

트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.

즉, 일부만 성공하는 상태는 존재해서는 안된다.

(예시 : 계좌 이체의 경우 송금/입금 두 작업이 반드시 모두 성공하거나 모두 실패해야 하고, 중간에 실패하면 전체 거래가 취소되어야 한다.)

 

MySQL의 InnoDB의 경우 MVCC (Multi-Version Concurrency Control)를 통해 원자성을 보장한다.

MVCC는 레코드 레벨의 잠금을 지원하고 언두 로그로 하나의 레코드에 대한 여러 버전을 동시에 관리할 수 있는 기술이다.

Lock 없이 일관된 읽기를 보장하고 변경 관리와 롤백도 지원한다.

롤백의 경우에는 언두 로그에 있는 데이터를 InnoDB 버퍼 풀로 복구하고, 언두 영역의 내용을 삭제한다.

이 언두 영역은 커밋된다고 항상 삭제되는 것은 아니고 필요로 하는 트랜잭션이 더는 없을 때 삭제된다.

(언두 로그 : 데이터 변경 전의 이전 값을 보관. 롤백 시 언두 로그를 참조하여 이전 상태로 복원.)

 

일관성 (Consistency)

트랜잭션이 완료된 후에는 DB가 항상 일관된 상태를 유지해야 한다.

(예시 : "계좌 잔액이 마이너스가 되면 안 된다" 같은 제약조건들이 항상 지켜져야 한다.)

 

MySQL의 InnoDB의 경우 외래키(참조 무결성 체크), 유니크 인덱스(중복 데이터 방지) 등의 제약조건으로 일관성을 보장한다.

 

격리성 (Isolation)

동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리해야한다.

즉, 실행 중인 트랜잭션이 다른 트랜잭션에 영향을 받아선 안된다

(예시 : 동시에 여러 거래가 발생할 때도 각 거래는 서로 영향을 주지 않아야 한다.)

 

MySQL의 InnoDB의 경우  트랜잭션 격리 수준으로 격리성을 보장한다.

격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택할 수 있다.

  • 트랜잭션 격리 수준
  1. READ UNCOMMITED (커밋되지 않은 읽기)
  2. READ COMMITTED (커밋된 읽기)
  3. REPEATABLE READ (반복 가능한 읽기) 
  4. SERIALIZABLE (직렬화 가능)

 

지속성 (Durability)

트랜잭션을 성공적으로 끝내면결과가 항상 기록되어야 한다.

즉, 트랜잭션이 커밋되면 결과는 DB에 영구적으로 저장되어 보존되어야한다.

(예시 : 거래가 완료됐다면 거래가 성공/실패했더라도 거래 내역은 영구히 보존되어야 한다.)

 

MySQL의 InnoDB의 경우 리두 로그로 지속성을 보장한다.

리두 로그는 랜잭션의 영구 보존을 위한 로그이다.

WAL(Write-Ahead Logging) 방식으로 DML, DDL, TCL 작업 등 데이터 변경이 일어나는 모든 것을 기록하고

장애 발생 시 리두 로그로 데이터 복구를 보장한다.

(모든 변경 사항을 자세하게 기록하는 일지와 같음)


스프링이 제공하는 트랜잭션 AOP

스프링은 트랜잭션 AOP 를 처리하기 위한 모든 기능을 제공합니다.

스프링 부트를 사용하면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해주고,

우리는 트랜잭션 처리가 필요한 곳에 @Transactional 어노테이션만 붙여주면 됩니다.

스프링의 트랜잭션 AOP는 이 @Transactional 어노테이션을 인식해서 트랜잭션을 처리하는 프록시를 적용해줍니다.

 

@Transactional org.springframework.transaction.annotation.Transactional

 

DB 락(Lock)에 대해 알아보기

 

락(Lock)이란? Lock의 종류와 교착상태(DeadLock)

 

hstory0208.tistory.com