Transaction


Transaction

1. 배경지식

(1) 트랜잭션이란?

트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다.
작업을 쪼개서 작은 단위로 만들 수 없다는 것은 트랜잭션의 핵심 속성인 원자성을 의미한다.

SQL 명령이 DB에서 실행되는 것을 트랜잭션이라고도 한다.
별도로 지정하지 않으면 SQL 명령 하나가 실행되는 것이 하나의 트랜잭션이 된다.

SQL 문장 여러개를 묶어서 하나의 트랜잭션을 만들수도 있다.
예를 들어서 운영체제에서 앱이 실행되는 것을 프로세스(process)라고 부르는 것처럼
DMBS에서 SQL 명령이 실행되는 것은 트랜잭션(transaction)이라고 부른다.

운영체제 프로세스에서 병렬성 문제, 데드락 문제가 발생하는 것과 유사하게
DB 트랜잭션에서도 병렬성 문제, 데드락 문제가 발생한다.


(2) 트랜잭션의 속성

트랜잭션 내부 작업들이 부분 성공 부분 실패하는 일 없이, 전체 성공하거나 전체 실패하는 것이 보장된다.
예를들어, 자금 이체는 보내는 쪽에서 돈을 빼는 작업과 받는 쪽에 돈을 넣는 작업 중 하나라도 실패하면 안된다.


트랜잭션 실행이 성공적으로 완료되면, 데이터베이스 상태는 모순이 없는 상태가 (consistency) 유지됨이 보장된다.
즉 트랜잭션은 데이터베이스 상태를 모순이 있는 (inconsistent) 상태로 변경할 수 없다.


DB에서 여러 트랜잭션이 동시에 실행되지만, 트랜잭션이 서로 충돌하지 않고, 마치 혼자 실행되는 것과
같은 환경이 보장된다.

예를들어, 두 트랜잭션이 어떤 레코드를 동시에 쓰려고 할 때.
어떤 트랜잭션이 수정하고 있는 레코드를 다른 트랜잭션이 읽으려고 할 때
충돌 없이 여러 트랜잭션이 동시에 잘 실행될 수 있음이 보장된다.


성공적으로 수행 종료된 트랜잭션이 저장한 데이터는 시스템 장애 등의 이유로 날아가는 일이 없이
계속 유지됨이 보장된다. 혹시 시스템에 장애가 발생하더라도 DBMS의 백업과 복구 기능을 활용하여,
장애 직전의 상태로 데이터를 전부 복구할 수 있음이 보장된다.


2. 구현

spring 기능을 활용하여 트랜잭션을 구현

(1) application.properties

spring.aop.proxy-target-class=true        

스프링 트랜잭션 기능을 구현하기 위해서
@Transactional 어노테이션을 메소드나 클래스에 붙여줄 경우
그리고 어노테이션이 사용된 클래스가 부모 interface가 없을 때
위 설정이 필요하다.


Transaction Level

트랜잭션 전파 (propagation)

트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법을 결정하는 속성이다.
기존 트랜잭션에 참여한다는 것은 현재 트랜잭션에서 다른 트랜잭션으로 이동할 때를 이야기 한다.

예를들어 AccountService에 트랜잭션이 걸려 있는데 OrderService 에서도 트랜잭션이 걸려 있는 것을 말한다.
같은 클래스는 해당 사항이 없다.

트랜잭션 경계의 시작 지점에서 트랜잭션 전파 속성을 참조해서 해당 범위의 트랜잭션을 어떤 식으로 진행시킬지 정할 수 있다.

다음과 같은 속성으로 설정이 가능하다.


디폴트 속성이며 모든 트랜잭션 매니저가 지원한다.
미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.
자연스럽고 간단한 트랜잭션 전파 방식이지만 사용해보면 매우 강력하고 유용하다는 사실을 알 수 있다.
하나의 트랜잭션이 시작된 후에 다른 트랜잭션 경계가 설정된 메소드를 호출하면 자연스럽게 같은 트랜잭션으로 묶인다.


이미 시작된 트랜잭션이 있으면 참여하고 그렇지 않으면 트랜잭션 없이 진행한다.
트랜잭션이 없긴 하지만 해당 경계 안에서 Connection이나 하이버네이트 Session 등을 공유할 수 있다.


REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다.
반면에 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다.
혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다.


항상 새로운 트랜잭션을 시작한다. 이미 진행 중인 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다.
JTA 트랜잭션 매니저를 사용한다면 서버의 트랜잭션 매니저에 트랜잭션 보류가 가능하도록 설정되어 있어야 한다.


트랜잭션을 사용하지 않게 한다. 이미 진행중인 트랜잭션이 있으면 보류시킨다.


트랜잭션을 사용하지 않도록 강제한다. 이미 진행 중인 트랜잭션도 존재하면 안된다.
만약 있다면 보류시킨다.


이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다.
중첩 트랜잭션은 트랜잭션 안에 다시 트랜잭션을 만드는 것이다.
하지만 독립적인 트랜잭션을 만드는 REQUIRES_NEW와는 다르다.

중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만
자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다.

중첩 트랜잭션은 JDBC 3.0 스펙의 저장포인트(savepoint)를 지원하는 드라이버와
DataSourceTransactionManager 를 이용할 경우에 적용 가능하다.
또는 중첩 트랜잭션을 지원하는 일부 WAS의 JTA 트랜잭션 매니저를 이용할 때도 적용할 수 있다.
유용한 트랜잭션 전파 방식이지만 모든 트랜잭션 매니저에 다 적용 가능한 건 아니므로,
적용하려면 사용할 트랜잭션 매니저와 드라이버, WAS의 문서를 참조해 보고,
미리 학습 테스트를 만들어서 검증해봐야 한다.


트랜잭션 격리 (isolation)

1. 데이터베이스의 Lock

(1) 데이터베이스에서 락이란?

동시에 실행되는 여러 트랜잭션이 서로 충돌하는 일이 벌어질 수 있다.
예를 들어 어떤 트랜잭션에서 UPDATE 명령이 실행되어 어느 레코드를 수정하는 도중에,

다른 트랜잭션에서 그 레코드를 DELETE 해버리면 문제가 발생할 것이다.
이런 충돌을 피하기 위해서, 트랜잭션에서 데이터를 읽고 쓸 때,
다른 트랜잭션이 방해하지 못하도록 그 데이터를 잠시 잠그는(lock) 것이 필요하다.
락(lock)에는 읽기 락과 쓰기 락이 있다

(2) 읽기 락 (Rread Lock)

트랙잭션이 데이터를 읽기 직전에 그 데이터에 읽기 락을 건다.

읽기 락은 여러 개가 중복될 수 있다.
그래서 동시에 여러 트랜잭션이 같은 데이터를 읽는 것은 가능하다.

읽기 락과 쓰기 락은 중복될 수 없다.
그래서 어떤 트랜잭션이 데이터를 읽는 중이라서 읽기 락이 걸려 있는 데이터를
다른 트랜잭션이 수정하는 것은 불가능하다.
데이터 읽기가 끝나고 읽기 락이 풀리면, 그때 쓰기 락을 걸고 수정하게 된다.

(3) 쓰기 락 (Write Lock)

트랜잭션이 데이터를 쓰기 직전에 그 데이터에 쓰기 락을 건다.

쓰기 락은 여러 개 중복될 수 없다.
그래서 동시에 여러 트랜잭션이 같은 데이터를 수정하는 것은 불가능하다.

읽기 락과 쓰기 락은 중복될 수 없다.
그래서 어떤 트랜잭션이 데이터를 수정하는 중이라서 쓰기 락이 걸려 있는 데이터를
다른 트랜잭션이 읽는 것은 불가능하다.
데이터 수정이 끝나고 쓰기 락이 풀리면, 그때 읽기 락을 걸고 읽게 된다.

(4) 쓰기 락의 범위

트랜잭션이 데이터를 수정할 때 먼저 그 데이터에 자동으로 쓰기 락이 걸리게 된다.
데이터 수정 전에 쓰기 락이 걸리는 것은 언제나 자동으로 일어난다.
언제나 트랜잭션이 종료될 때 쓰기 락이 풀린다 (unlock).

즉 쓰기 락은 데이터를 수정하기 직전에 언제나 자동으로 걸리고,
트랜잭션이 종료될 때 쓰기 락이 풀린다.

(5) 읽기 락의 범위

트랜잭션이 데이터를 읽을 때 먼저 그 데이터에 읽기 락이 자동으로 걸려야 한다.
그런데 읽기 락이 언제나 자동으로 걸리는 것은 아니다.

읽기 락을 하냐 마냐는 Transaction Isolation Level 설정에 따라 다르다.
읽기 락이 언제까지 유지할 것인지도 Transaction Isolation Level 설정에 따라 다르다.


2. Transaction Isolation Level

Transaction Isolation Level 설정에 따라,
트랜잭션이 데이터를 읽기 전에 읽기 락을 할지 말지 읽기 락을 언제까지 유지할지가 결정된다.

읽기 락을 많이 걸고 오래 유지할 수록 데이터의 안정성은 좋아지지만 성능은 나빠진다.
돈 거래와 같은 중요한 데이터라면 데이터 안정성이 중요하고,
게시판 덧글과 같이 별로 중요하지 않은 데이터라면 성능이 더 중요할 것이다.


(1) Transaction Isolation Level 설정 명령
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED  

SET TRANSACTION ISOLATION LEVEL READ COMMITTED  

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ  

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 

위 목록에서 아래의 명령일 수록 읽기 락을 좀 더 많이 건다.

Transaction Isolcation Level을 설정하는 명령을 실행하지 않았다면,
디폴트 상태는 다음과 같다.


(2) Read Uncommitted

읽기 락을 전혀 하지 않는다. 그래서 가장 빠르다.

Dirty Reads 문제 발생

읽기 락을 전혀 하지 않기 때문에, 다른 트랜잭션이 수정하고 있어서 쓰기 락이 걸려 있는 데이터도 읽을 수 있다.
그래서 게시글 본문이 반쯤 저장된 상태에서 그 게시글 레코드를 읽는 것이 가능하다.

이렇게 완전하지 않고 반쯤 수정된 데이터가 읽혀질 수 있는 문제를 Dirty Reads 문제라고 부른다.

읽기 락을 하지 않기 때문에, 어떤 트랜잭션이 읽고 있는 중인 데이터를
다른 트랜잭션이 쓰기 락을 걸고 수정하거나 삭제할 수도 있다.

Read Uncommitted 레벨에서는 Dirty Reads 문제가 발생할 수 있다.

이 문제를 피하려면, Transaction Isolation Level 을 다음 단계인 Read Committed로 올려야 한다.


(3) Read Committed

데이터를 읽기 직전에 언제나 읽기 락을 한다.
그리고 그 SQL 문장이 끝나자 마자 읽기 락을 푼다.
즉 읽기 락을 SQL 문장 하나 단위로만 유지한다.

읽기 락을 걸고 데이터를 읽기 때문에,
그리고 읽기 락이 걸려 있는 데이터에 쓰기 락을 걸 수는 없기 때문에,
읽는 중인 데이터를 다른 트랜잭션이 수정할 수 없다.
따라서 Dirty Reads 문제는 발생하지 않는다.

Transaction Isolcation Level을 설정하는 명령을 실행하지 않았다면,
이것이 디폴트 상태이다.

Non-repeatable Reads 문제 발생

Read Committed 단계에서는 Dirty Reads 문제는 해결 되었지만,
모든 문제가 다 해결된 것은 아니다.
미묘한 Non-repeatable Reads 문제가 남아있다.

하나의 트랜잭션에서 어떤 데이터를 처음 읽을 때와 나중에 읽을 때 값이 달라져서
문제가 되는 상황을 Non-Repeatable Reads 문제라고 부른다.

Non-Repeatable Reads 문제를 피하려면
다른 트랜잭션이 사이에 끼어 들어와서 데이터를 수정하지 못하게 막아야 한다.
그러러면 읽기 락을 좀 더 열심히 걸어야 한다.

Non-Repeatable Reads 문제를 피하려면
Transaction Isolation Level 을 다음 단계인 Repeatable Read 단계로 올려야 한다.


(4) Repeatable Read

데이터를 읽기 직전에 읽기 락을 건다.
그리고 읽기 락을 트랜잭션 끝날 때까지 유지해서
다른 트랜잭션이 사이에 끼어 들어와서 데이터를 수정하지 못하게 막는다.

Non-repeatable Reads 문제 해결

읽기 락이 걸려 있는 데이터에 다른 트랜잭션이 쓰기 락을 걸 수 없지만,
읽기 락을 건 바로 그 트랜잭션은, 자신이 걸었던 읽기 락을 쓰기 락으로 변경하는 것이 가능하다.

Phantom Reads 문제 발생

Repeatable Read 단계에서 Non-repeatable Reads 문제는 해결 되었지만
모든 문제가 다 해결된 것은 아니다.
아주 미묘한 Phantom Reads 문제가 남아있다.

예를 들어 수강 신청에서
절차1) 먼저 강좌의 수강 레코드 수를 조회하여 최대 수강 인원 수 보다 크거나 같다면,
그 강좌의 수강 인원이 꽉찬 것이므로 종료.

절차2) 그렇지 않다면 수강 레코드 삽입(insert)

위와 같은 순서로 구현했을 때,
절차1에서 읽은 수강 레코드들에 읽기 락이 걸리고 트랜잭션이 끝날 때가지 유지된다.
그 트랜잭션이 끝날 때가지 읽기 락이 걸려 있는 수강 레코들은 수정 될 수 없도록 보호되지만,
새 수강 레코드가 삽입되는 것은 가능하다.
그래서 절차1과 절차2 사이에 다른 트랜잭션이 그 강좌에 새 수강 레코드를 삽입할 수 있다.

절차1에서 강좌의 수강 레코드 수를 조회할 때는 최대 수강 인원 수 보다 작았는데,
막상 절차2를 실행할 때는 최대 수강 인원 수와 수강 레코드 수가 같을 수 있다.
절차1과 절차2 사이에 다른 트랜잭션이 수강 레코드를 등록할 수 있기 때문이다.

절차1과 절차2에서 조회한 수강 레코드 수가 동일하려면,
절차1과 절차2 사이에 다른 트랜잭션이 그 강좌에 수강 레코드를 등록하지 못하게 막아야 한다.

이렇게 막는 것은 조금 복잡한 읽기 락(lock)이 필요하다.

Phantom Reads 문제를 피하려면
좀 더 복잡한 읽기 락을 걸어야 한다.
Transaction Isolation Level 을 다음 단계인 Serializable 단계로 올려야 한다.


(5) Serializable

Phantom Reads 문제를 피하기 위해 좀 더 복잡한 읽기 락을 건다.
그리고 트랜잭션 끝날 때까지 읽기 락을 유지한다.

테이블에 읽기 락을 걸거나, 테이블 인덱스에 읽기 락을 걸거나,
WHERE 절 조건식으로 읽기 락을 걸기도 한다.

테이블에 읽기 락이 걸리면, 그 테이블에 대한 모든 수정(insert, update, delete)은 막힌다.
데이블 인덱스에 읽기 락이 걸리면, 그 인덱스에 변화를 초래하는 수정(insert, update, delete)은 막힌다.
WHERE 절 조건식으로 읽기 락이 걸리면, WHERE 절 조건식의 true/false 값이 변할 만한 수정(insert, update, delete)은 막힌다.

Phantom Reads 문제 해결

수강 신청에서 절차를 다시 생각해 보자.
절차1) 먼저 강좌의 수강 레코드 수를 조회하여 최대 수강 인원 수 보다 크거나 같다면,
그 강좌의 수강 인원이 꽉찬 것이므로 종료.

절차2) 그렇지 않다면 수강 레코드 삽입(insert)

강좌의 수강 레코드 수 조회 SQL 문은 다음과 같은 형태일 것이.

SELECT COUNT(*) FROM 수강 WHERE lectureId = #{lectureId}   

절차1에서 읽은 강좌 레코드 수를 조회하는 WHERE 절의 조건식에 읽기 락이 걸린다.
위 WHERE 조건식의 값이, 어떤 명령의 실행 전과 후에 달라지다면, 락에 의해서 그 명령의 실행은 막힌다.
위 WHERE 조건식이 true인 레코드를 delete 하는 것도 막힌다.
위 WHERE 조건식이 true인 레코드를 insert 하는 것도 막힌다.

따라서 절차1과 절차2 사이에 다른 레코드가 끼어 들어와서 그 강좌에 새 수강 레코드를 삽입할 수 없다.

Serializable 단계는 모든 읽기 문제가 다 해결된 단계이다.


요약

read uncommitted

어떤 트랜잭션이 수정한 내용이 아직 commit 되기 전부터,

다른 트랜잭션들에게 그 값이 보임. (dirty read 문제)


read committed

어떤 트랜잭션이 수정한 내용이 commit 된 이후에만.

다른 트랜잭션들에게 그 값이 보임. (dirty read 해결됨)

어떤 트랜잭션이 한 번 읽은 레코드를, 트랜잭션 실행 도중 다시 읽었을 때,

그 사이에 다른 트랜잭션이 그 레코드를 수정하고 commit 했다면,

다시 읽은 값은 처음 읽은 값과 달라진다. (nonrepeatable read 문제)


repeatable read

어떤 트랜잭션이 한 번 읽은 레코드를, 트랜잭션 실행 도중 다시 읽었을 때,

그 사이에 다른 트랜잭션이 그 레코드를 수정하고 commit 했더라도,

처음 읽은 값과 다시 읽은 값이 동일함이 보장된다. (nonrepeatable read 해결됨)

어떤 트랜잭션이 아직 읽지 않은 레코드를,

다른 트랜잭션이 수정하는 것이 허용된다.

예를 들어, 어떤 소프트웨어공학과 학생 수를 세기 위해

소프트웨어공학과 학생 레코드들을 읽으면,

그 사이에 다른 트랜잭션이 그 레코드를 수정하고 commit 했더라도,

처음 읽은 값과 다시 읽은 값이 동일함이 보장된다.

하지만,

다른 트랜잭션이 소프트웨어공학과 학생 레코드가 새로 추가하고 commit 한 후에,

다시 소프트웨어공학과 학생 수를 세면, 처음 세었을 때 보다 1 증가했을 것이다.

(phantom read 문제)


serializable

phantom read 문제도 해결됨.