Please enable JavaScript to view the comments powered by Disqus.트랜잭션을 이해하기 위한 기반지식 어떻게, 어디에 저장되는 걸까?
Search
🧾

트랜잭션을 이해하기 위한 기반지식 어떻게, 어디에 저장되는 걸까?

태그
DataBase
Transaction
Concurrency
공개여부
작성일자
2023/07/18
ACID 트랜잭션을 지키기 위해 DB는 무엇을 할까? 어디에 저장하는 것이고, 그 저장된 데이터는 어디로 옮겨가며 이러한 흐름속에서 어떻게 동시성을 만족하여 트랜잭션의 ACID를 지켜낼까?

저장 장치의 구조

트랜잭션에서 원자성과 지속성을 보장하기 위해 어떻게 저장되는지 살펴본다.
이 글에서 살펴본 것과 같이 저장 장치는 속도, 용량, 실패에서 복구(recovery)로 구분되고 저장 장치 혹은 비휘발성 저장 장치로 분류된다.
이름
특징
휘발성 저장 장치 Volatile storage
시스템 장애시 손실 예시: 메인 메모리, 캐시 메모리 특성: 메모리 자체의 접근 소도가 빠르고 데이터 직접 저근이 가능하여 접근 속도는 매우 빠르다.
비휘발성 저장 장치 Non-volatile stroage
시스템 장애시 보존 예시: 2차 저장 장치(자기 디스크, 플래시 저장 장치, 보관용 저장소 (광학 미디어, 자기 테이프) 랜덤 접근이 휘발성에 비해 느리다.
안정 저장 장치 Stable storage
절대 손실되지 않는다 라고 하지만, “절대”란 없다. 데이터 손실이 거의 없도록하는 기술로 근접하게 구현한다.
트랜잭션의 원자성을 보존하기 위해 변경 사항이 디스크에 반영 되거나 레코드 안정 저장 장치에 기록되어야 한다.
한 시스템이 지속성과 원자성을 얼마나 보장할 수 있는가는 안정 저장가 얼마나 신뢰할 수 있는지에 영향을 받는다. 이런 경우 디스크는 여러 개의 사본이 필요할 수 있다.(실제로 AWS RDS는 8개씩 사본을 둔다고도 한다.)
즉, 매우 이상적인 형태의 안정 저장 장치 구현이 필요하다.

트랜잭션 원자성과 지속성

트랜잭션은 항상 성공적으로 완료되진 않는다. 이러한 상태를 abort라 한다.
만약 원자성이 올바르게 지켜졌다면, 트랜잭션이 abort 되었을 때 DB에는 어떠한 변화도 일어나선 안되기 때문에 변화는 모두 이전 상태로 복구되어야 한다.
트랜잭션의 abort로 인해 원복되는 것을 rollback이라 한다.
이러한 rollback은 로그를 사용하는 방법으로 지켜진다. (이에 대한 내용은 매우 흥미롭기 때문에 2023년 8월중에 완료하겠다.)
트랜잭션에서 일어난 수정은 모두 로그에 기록된다.
로그엔 다음의 데이터가 포함된다.
[Transaction 식별자][데이터 항목의 식별자][데이터 이전 값][데이터 새로운 값]
Plain Text
복사
Log에 포함되는 내용
이러한 로그가 남는다면 abort 상황에서 rollback을 할 수도 있고, 특정 시점에서 재실행을 할 수도 있다.
성공적으로 실행이 완료된 트랜잭션을 commit이라 한다.
갱신이 commit되면 데이터베이스 시스템에 변경이 반영된 것이며, 이 시스템에 장애가 발생한다 하더라도 지속되어야 하며, commit된 트랜잭션은 되돌릴 수 없다.
무엇보다도 일관성(consistency)이 보장되어 반영된 것이다.
만약 트랜잭션을 되돌려야 한다면, 보상 트랜잭션이 필요하다.
보상 트랜잭션 계좌 A에서 계좌 B로 100$을 이체했다면, 다시 계좌 B에서 계좌 A로 100$을 이체하는 것
문제는 보상 트랜잭션이 데이터베이스 시스템에서 책임지는 것이 아니라 사용자(개발자) 책임져야 할 부분이다.

트랜잭션의 상태

Transaction Commit 에 대해 더 정확한 이해가 필요하다.
트랜잭션은 다음 중 하나의 상태를 반드시 가져야 한다.
이름
내용
Active(동작)
초기 상태, 현재 트랜잭션이 실행 중이면 동작 상태라 표현한다.
Partially committed(부분 커밋)
마지막 명령문이 실행 된 후의 상태 실행 결과가 아직 메인 메모리에 있기 때문에 여전히 트랜잭션은 abort 될 가능성을 갖고 있다.
Failed(실패)
정상적인 실행이 더 진행될 수 없을 때.
Abort(중단)
트랜잭션이 롤백되어 트랜잭션 시작 전 상태로 돌아가고 난 상태 Abort 된다 하더라도 로그는 disk에 저장된다.
Commit
트랜잭션이 성공적으로 완료된 후의 상
만약 트랜잭션이 Abort 혹은 Commit 상태로 들어가면 그 트랜잭션은 terminated 되었다고 할 수 있다.
트랜잭션 상태 다이어그램
만약 트랜잭션이 abort 하도록 데이터베이스 시스템이 결정하면, 트랜잭션은 failed 상태로 진입한다. 이러한 트랜잭션은 반드시 롤백되어야 하며 시스템은 다음 중 하나의 선택을 한다.
하드웨어 오류, 트랜잭션 자체의 논리적 오류가 아닌 케이스로 트랜잭션을 재시작 할 수 있다.
재시작한 트랜잭션은 새로운 트랜잭션으로 간주한다.
트랜잭션을 강제 종료(kill)할 수 있다.
프로그램 자체의 오류, 입력이 잘못된 경우
필요한 데이터가 없는 경우

트랜잭션의 고립성

트랜잭션 처리 시스템은 트랜잭션이 동시에 수행하는 것을 허용한다.
동시에 실행하는 경우 일관성과 관련된 여러 문제가 발생한다. 동시에 여러 트랜잭션이 실행되면서 일관성을 보장하기 위해 추가적인 노력이 필요하다.
만약 모든 트랜잭션이 순차적으로 실행되면 일관성을 관리하는 것이 간단해진다. 하지만 동시성을 허용하여 다음의 이점을 얻는다.

처리율(throughput), 이용률(utilization) 향상

하나의 트랜잭션은 여러 단계로 구성된다.
I/O 처리, CPU 처리 등이 대표적인 예시인데, 컴퓨터는 이 두 작업을 병렬적으로 수행할 수 있다.
I/O, CPU 작업을 각각 병렬적으로 수행하면 여러 트랜잭션을 동시에 처리할 수 있다.
트랜잭션 T0T_0 에서 읽기와 처리를 수행한다.
트랜잭션 T1T_1 에서 CPU 작업을 수행한다.
트랜잭션 T2T_2 에서 다른 디스크의 I/O 작업을 수행한다.
이러한 능력은 시스템의 처리율(throughput), 디스크 이용률(utilization)을 향상시킬 수 있다.
즉, 프로세서와 디스크가 휴식을 취하지 않고 계속 일하게 만들 수 있다.
Database의 동시성 기술 동시성 기술을 크게 두 목적을 위해 사용한다. 1. 처리 시간이 긴 쿼리(ex. 10분)의 서로 다른 부분을 동시에 처리하여 속도를 향상한다. 2. 많은 사용자가 요청한 매우 많은 수의 쿼리를 동시에 처리한다.

대기 시간 감소

무수히 많은 N개의 트랜잭션 집합 T가 (T={T0,T1,Tn1}T= \{T_0, T_1, … T_{n-1}\}) 실행된다고 하자.
임의의 트랜잭션 TiT_i 는 다른 트랜잭션과 비교하여 매우 긴 처리 시간(예, 10분)이 필요하고
임의의 트랜잭션 TjT_j 의 실행 시간은 매우 짧다(예, 0.0001초)고 하자.
만약 순차적으로 트랜잭션이 실행되어 TiT_i 이후 TjT_j 가 실행되면 TjT_j 입장에선 10분 0.0001초가 지연시간이 되어 지연을 초래한다.
만약 트랜잭션 집합 TT의 트랜잭션이 적절히 서로 다른 부분에서 실행된다고 하면 CPU 사이클과 디스크를 공유하면서 동시에 수행하는 것이 더 좋을 것이다.
이러한 동시 실행은 평균 응답 시간(Average response time)을 줄일 수 있을 것이다.
DB가 동시 수행을 적용하게 된 계기는 mutli-programming을 사용하게 된 동기와 같다.
이때 고립성(isolation)이 지켜지지 않았다면, 일관성(consistency)이 깨질 수 있다.
데이터베이스 시스템에서 일관성을 위해 트랜잭션 집합 TT 의 모든 트랜잭션의 상호 작용을 제어하는 동시성 제어 기법(concurrency-control scheme)을 알아보자.

동시성 제어 기법의 개념

예시를 위해 앞선 포스팅에서 사용한 예시를 다시 가져온다.
트랜잭션 T1T_1 은 계좌 A에서 $50을 계좌 B로 이체한다.
T1:  read(A)A:=A50;write(A);read(B);B:=b+50write(B);\begin{aligned} T_1:\; &read(A) \\ &A := A-50; \\ &write(A); \\ &read(B); \\ &B:= b+50 \\ &write(B); \end{aligned}
트랜잭션 T2T_2는 계좌 A의 잔액 10%를 계좌 B로 이체한다.
T2  :read(A);temp:=A×0.1A:=Atemp;write(A);read(B);B:=B+temp;write(B);\begin{aligned} T_2 \;: &read(A);\\ &temp:= A \times 0.1 \\ &A := A-temp; \\ &write(A); \\ &read(B); \\ &B := B + temp; \\ &write(B); \end{aligned}
직관적인 이해를 위해 다음을 가정한다.
1.
계좌 A는 $1000, 계좌 B는 $2000이다.
2.
트랜잭션 T1T_1 T2T_2 는 순차적으로 실행한다. (동시가 아니다)
시간(단조 증가)
T1T_1
T2T_2
0
T1:  read(A)A:=A50;write(A);read(B);B:=b+50write(B);\begin{aligned} T_1:\; &read(A) \\ &A := A-50; \\ &write(A); \\ &read(B); \\ &B:= b+50 \\ &write(B); \end{aligned}
1
T2  :read(A);temp:=A×0.1A:=Atemp;write(A);read(B);B:=B+temp;write(B);\begin{aligned} T_2 \;: &read(A);\\ &temp:= A \times 0.1 \\ &A := A-temp; \\ &write(A); \\ &read(B); \\ &B := B + temp; \\ &write(B); \end{aligned}
T1,T2T_1, T_2 순서로 실행하면 계좌 A는 $855 계좌 B는 $2145가 되며 A+B = 3,000 이다.
이와 반대로 위의 가정 2를 변경하여 T2,T1T_2, T_1 순서로 실행한다 가정하자.
시간(단조 증가)
T1T_1
T2T_2
0
T2  :read(A);temp:=A×0.1A:=Atemp;write(A);read(B);B:=B+temp;write(B);\begin{aligned} T_2 \;: &read(A);\\ &temp:= A \times 0.1 \\ &A := A-temp; \\ &write(A); \\ &read(B); \\ &B := B + temp; \\ &write(B); \end{aligned}
1
T1:  read(A)A:=A50;write(A);read(B);B:=b+50write(B);\begin{aligned} T_1:\; &read(A) \\ &A := A-50; \\ &write(A); \\ &read(B); \\ &B:= b+50 \\ &write(B); \end{aligned}
이와 같이 실행되면 A는 $850, B는 $2,150이며 A+B = 3,000이 된다.
이러한 순서를 스케줄(schedule) 이라 한다.
스케줄들은 순차적이며, 스케줄 내부에서도 순서를 갖는다. (write(A)write(A) 이후에 read(B)read(B)가 호출된다.)
스케쥴은 n개의 트랜잭션이 있다면 n!n! 개의 서로 다른 스케쥴이 나온다.
그렇다면 동시에 실행되면 어떻게 될까??
두 개의 트랜잭션이 완전히 동시에 실행된다고 가정하자.
여러 트랜잭션을 동시에 실행하면 컴퓨터는 CPU를 트랜잭션간 공유하여 사용하게 된다. 이때 context switching 이 발생하기 때문에 서로 다른 두 트랜잭션의 명령어가 번갈아가며 배치될 수 있다.
시간(단조 증가)
T1T_1
T2T_2
0
read(A)A:=A50;write(A);\begin{aligned} &read(A) \\ &A := A-50; \\ &write(A); \\ \end{aligned}
1
read(A);temp:=A×0.1A:=Atemp;write(A);\begin{aligned} &read(A);\\ &temp:= A \times 0.1 \\ &A := A-temp; \\ &write(A); \end{aligned}
2
read(B);B:=b+50write(B);\begin{aligned} &read(B); \\ &B:= b+50 \\ &write(B); \end{aligned}
3
read(B);B:=B+temp;write(B);\begin{aligned} &read(B); \\ &B := B + temp; \\ &write(B); \end{aligned}
이 스케줄의 실행이 완료되면 A+B = 3,000 으로 일관성이 만족된다. 즉, T1,T2T_1, T_2 의 순차적으로 실행한 경우와 동일한 결과를 반환할 수 있다.
그런데 이렇게 graceful 한 경우만 있을까??
시간(단조 증가)
T1T_1
T2T_2
0
read(A)A:=A50;\begin{aligned} &read(A) \\ &A := A-50; \\ \end{aligned}
A는 950이 되지만 아직 write 되지 않음
1
read(A);temp:=A×0.1A:=Atemp;write(A);read(B);\begin{aligned} &read(A);\\ &temp:= A \times 0.1 \\ &A := A-temp; \\ &write(A);\\ &read(B); \\ \end{aligned}
A를 읽으면 1000으로 읽히기 때문에 temp는 100이 된다.
2
write(A);read(B);B:=b+50write(B);\begin{aligned} &write(A); \\ &read(B); \\ &B:= b+50 \\ &write(B); \end{aligned}
3
B:=B+temp;write(B);\begin{aligned} &B := B + temp; \\ &write(B); \end{aligned}
이러한 context switching이 발생한다면 A는 $950, B는 $2,100이 된다.
A+B의 결과가 $3,050 이 되어 비일관성 상태가 된다.
스케줄은 앞서 언급한 것 처럼 n!n! 이 되며, 그 안에는 비일관성 상태가 되는 스케줄이 많을 수 있다.
Database의 역할은 항상 일관된 상태(ACID의 C)를 만족하도록 적절한 제어가 필요하다.
이 역할을 concurrency-control component가 수행하여 직렬 가능 스케줄을 보장한다.
직렬 가능(serializable) 스케줄 동시 수행한 스케줄의 결과가 트랜잭션을 하나씩 순차적으로 수행하여 스케줄의 실행결과가 동일하게 하여 일관성을 보장한다.
이 다음으로 직렬 가능성에 대해 살펴볼 것이다.
이것 역시 큰 주제이기 때문에 따로 떼어 포스팅하는 것이 좋겠다.