[12] 트랜잭션

hyunsooo·2023년 6월 16일
0
post-thumbnail

1. 트랜잭션

1.1. 트랜잭션이란?

트랜잭션(transaction)을 간단하게 설명하면 데이터베이스 프로그램(어플리케이션)입니다. 데이터베이스를 사용하는 조직은 논리적인 작업을 기준으로 업무를 수행하게 됩니다. 예를 들어, 은행에서의 송금 프로그램은 송금자의 계좌에서 금액을 차감하고 수신자의 계좌에는 금액을 더해주는 작업을 묶어 하나의 논리적인 작업으로 이루어 지며 이것을 트랜잭션이라고 합니다.

위의 예시를 실제 SQL문으로 작성해보면 아래와 같습니다.

UPDATE CUSTOMER
SET BALANCE = BALANCE - 100000
WHERE CUST_NAME = '김철수'

UPDATE CUSTOMER
SET BALANCE = BALANCE + 100000
WHERE CUST_NAME = '김영희'

위와 같은 상황에서 김철수의 계좌에서 돈을 출금하고 서버가 다운되었다면 현재 상태가 기대한 상황과는 달라지게 되고 이런 상태를 일관성을 잃은 상태(inconsistent)라고 합니다. 이런 상황을 방지하기 위해 논리적인 작업의 흐름을 하나의 트랜잭션으로 묶고 모든 작업이 끝나지 않았다면 다시 초기 상태로 돌려놓게 됩니다.

기본적으로 각각의 SQL문이 하나의 트랜잭션으로 취급되고 위의 상황처럼 두 개 이상의 SQL문을 하나의 트랜잭션으로 취급하려면 사용자가 이를 명시적으로 표기해야 합니다.

한 가지 예를 더 들어보겠습니다. 비행기 좌석을 예약하는 상황에서 하나의 빈좌석은 여러명의 고객들이 확인할 수 있고 많은 고객들이 동시에 예약을 누르더라도 한 사람에게 좌석을 발권해야 합니다.

# 비행기번호, 날짜, 출발지, 도착지, 팔린좌석, 총좌석
# FLIGHT(FNO, DATE, SOURCE, DESTINATIO, SEAT_SOLD, CAPACITY)
# 예약비행기번호, 날짜, 고객명, 비고
# RESERVED(FNO, DATE, CUST_NAME, SPECIAL)

# input = (flight_no, date, customer_name)
SELECT SEAT_SOLD, CAPACITY
FROM FLIGHT
WHERE FNO=filght_no AND DATE = date;

# if SEAT_SOLD == CAPACITY:
#	"빈 좌석 없음"
#    Abort
# else:

UPDATE FLIGHT
SET SEAT_SOLD = SEAT_SOLD + 1
WHERE FNO = flight_no AND DATE = date;

INSERT
INTO RESERVED(FNO, DATE, CUST_NAME, SPECIAL)
VALUES (filght_no, date, customer_name, null)
Commit
# 예약 완료

위의 예시도 마찬가지로 모든 SQL문이 완전하게 수행되거나 하나도 수행되어는 안되도록 하나의 트랜잭션으로 묶어서 관리해야 합니다.

트랜잭션의 상태

  • 활동 : 트랜잭션이 실행중인 상태

  • 부분 완료 : 마지막 연산을 실행한 직후

  • 완료 : 메모리 버퍼에 저장된 내용을 데이터베이스(디스크)에 저장완료한 상태

  • 실패 : 트랜잭션의 실행 중 장애나 오류가 발생하여 트랜잭션을 진행할 수 없는 상태

  • 철회 : Rollback 연산을 수행한 상태

1.2. 트랜잭션의 완료와 철회

트랜잭션에서 변경하려는 내용이 데이터베이스에 완전하게 반영시키는 것을 트랜잭션 완료(commit)이라고 합니다. 즉, 지속성 특성을 보장하는 작업하고 SQL 구문상으로는 COMMIT WORK를 사용합니다. 트랜잭션이 수행한 갱신을 데이터베이스에 제대로 반영이 되어 있는지 확인하고 반영되지 않은게 있다면 재실행을 통해 반영합니다.

반면에 변경하려는 내용이 데이터베이스 일부만 반영된 경우 원자성 특성을 보장하기 위해 트랜잭션이 갱신한 사항을 수행되기 전으로 되돌리는 것을 트랜잭션 철회(abort)입니다. SQL 구문상으로는 ROLLBACK WORK를 사용합니다.

1.3. 트랜잭션의 특성 (ACID 특성)

원자성 (Atomicity)

지금까지 설명한 트랜잭션의 특성이 바로 원자성입니다. 쉽게 말해 all or nothing을 의미하며 트랜잭션은 모든 연산이 완전히 수행되거나 또는 서버가 다운되었다면 전혀 수행되지 않은 상태로 되돌리는 특성입니다.

일관성 (Consistency)

어떤 트랜잭션이 수행되기 전에 데이터베이스를 일관된 상태라면 트랜잭션을 수행하고 난 후에 데이터베이스는 새로운 일관된 상태를 가져야 합니다.

송금하는 과정에서 출금만 이루어진 상태는 일시적인 일관되지 않은 상태이지만 트랜잭션이 정상적으로 수행되고 나면 데이터베이스는 일관된 상태를 나타내기 때문에 일관성을 위배하는 상황은 아닙니다.

현재의 잔액이 만원인데 2만원을 출금하는 상황은 일관성을 위배하는 상황입니다.

고립성 (Isolation)

한 트랜잭션이 데이터를 갱신하는 동안 갱신 중인 데이터에 다른 트랜잭이 접근하지 못하도록 해야하는 특성입니다.

다수의 트랜잭션이 동시에 수행되더라도 그 결과는 일련의 순서에 따라 트랜잭션을 하나씩 차례대로 수행한 결과와 같아햐 하며 DBMS의 동시성 제어 모듈이 트랜잭션 고립성을 보장합니다. DBMS는 요구사항에 따라 다양한 고립 수준을 제공합니다.

지속성 (Durability)

한번 완료된 트랜잭션의 결과는 데이터베이스에 영구적으로 반영되어야 하는 특성입니다.

2. 동시성 제어 (Concurrency control)

DBMS는 다수 사용자들이 함께 사용하기 때문에 동시에 동일한 테이블에 접근하기도 합니다. 일반적으로 READ(검색) 명령은 다수의 사용자가 동시에 접근해도 전혀 문제가 되지 않습니다. 하지만 공유자원에 대해 WRITE(수정) 명령어들은 충돌을 일으킵니다. 이러한 DBMS는 충돌들을 해결하기 위해 사용하는 기법이 동시성 제어입니다.

  • 직렬 스케줄(serial schedule) : 여러 트랜잭션들의 집합을 한 번에 한 트랜잭션씩 차례대로 수행

  • 비직렬 스케줄(non-serial schedule) : 여러 트랜잭션들을 동시에(순서가 없음) 수행

  • 직렬성 스케줄(serializable schedule)

직렬 스케줄은 성능이 좋지 않고 비직렬 스케줄은 순서를 모르기 때문에 일관성을 보장할 수 없습니다. 이 직렬성 스케줄은 비직렬 스케줄과 같이 동시에 수행을 했는데 결과가 직렬 스케줄과 같은 스케줄입니다.

2.1. 동시성 제어를 하지 않았을때의 문제

  • 갱신 손실(lost update) : 수행 중인 트랜잭션이 갱신한 내용을 다른 트랜잭션이 덮어 씀으로 갱신이 무효가 됨

하나의 SQL문은 여러 개의 명령들로 나뉘어 수행되고 동시에 요청한 트랜잭션의 명령들이 섞여서 수행될 수 있습니다. 위의 그림처럼 T1은 X에서 Y로 100,000을 이체하고 T2는 X에 50,000을 입금하려고 하는 상황에서 갱신 손실이 일어날 수 있습니다.

  • 오손 데이터 읽기(dirty read) : 완료 되지 않은 트랜잭션이 갱신한 데이터를 읽는것

T1이 A라는 사람의 잔액을 100,000원 감소시킨 후에 T2가 모든 계좌의 평균값을 검색한 상황입니다. 그 이후에 T1이 트랜잭션을 철회하게 된다면 T2는 완료되지 않은 트랜잭션이 갱신한 데이터(틀린 데이터)를 읽은 상황입니다.

  • 반복할 수 없는 읽기(unrepeatable read) : 한 트랜잭션이 동일한 데이터를 두번 읽을때 서로 다른 값을 읽는 것

T2가 모든 계좌의 평균값을 검색하고 T1이 A의 잔액을 100,000 감소시키고 완료되었습니다. 그 이후 T2가 다시 평균값을 검색하면 첫 번째 평균값과 다른 값을 보게 됩니다.

  • 팬텀 문제(phantom problem)

팬텀 문제는 반복 불가능 읽기 문제와 비슷하지만 중간에 새로운 데이터 삽입(INSERT)가 이루어져 처음보는 데이터가 보이는 현상입니다.

3. 락(Lock)

모든 트랜잭션에 대하여 최적화(optimal)된 스케줄링을 사용하기에는 복잡성과 오버헤드면에서 거의 불가능합니다. 따라서 모든 트랜잭션이 따라야 할 일종의 규약을 정하여 동시성 제어를 적용할 수 있습니다.

3.1. 락킹(Locking)

갱신 손실, 오손 데이터 읽기, 반복할 수 없는 읽기와 같은 문제는 결국 트랜잭션이 접근하는 데이터가 공유 되기 때문입니다. 이처럼 공유 데이터에서 생기는 문제를 충돌문제라고 정의합니다. 이는 운영체제에서의 임계구역 문제와 같은 맥락입니다.

운영체제에서도 이 문제를 해결하기 상호 배타를 적용합니다. 이처럼 데이터베이스는 이라는 변수를 이용하여 상호 배타성을 유도합니다.

상호 배타를 담당하는 락을 배타 락(X-lock, eXclusive lock)이라고 하며 트랜잭션에서 갱신을 목적으로 데이터에 접근할 때 이용합니다. 반면에 읽을 목적으로 데이터에 접근할 때는 공유 락(S-lock, Shared lock)을 이용합니다. 공유 락이 걸려 있는 데이터에 대해서는 다른 트랜잭션이 읽을 수는 있지만 수정을 할 수는 없습니다. 따라서 공유 락이 걸려 있는 데이터에 다른 트랜잭션이 또 공유 락을 걸 수 있습니다. 하지만 공유 락 이나 배타 락이 걸려 있는 데이터에 배타 락을 걸 수는 없습니다.

이처럼 락을 이용하여 다른 트랜잭션의 접근을 막는 기법을 락킹이라고 하며 트랜잭션이 종료되기 전에는 락킹을 해제하기 위해 unlock을 수행하게 됩니다.

3.2. 2단계 로킹 프로토콜 (2-phase locking protocol)

락을 요청하는 것과 락을 해제하는 것이 2단계로 이루어집니다. 위에서 설명한 것 처럼 하나의 트랜잭션이 락을 걸고 끝나기전에 락을 해제하고 또 다른 트랜잭션이 위와 같이 수행되면 동시성 제어 문제가 발생할 수 있습니다. 따라서 모든 트랜잭션에 대해 락을 거는 확장 단계가 지난 후에 락을 해제하는 수축 단계로 나누어 집니다. 하나라도 락을 걸게 되면 확장 단계만 지속되고 더 이상 락을 걸 수 없다면 수축 단계에 들어갑니다.

3.3. 데드락 (Deadlock)

운영체제에서도 상호 배타를 적용했을 때 데드락문제가 일어날 수 있었습니다. 데이터베이스도 운영체제와 유사한 상황으로 인하여 데드락(교착상태)가 일어날 수 있습니다.

운영체제와 유사하게 상호배타, 점유대기, 비선점과 같은 원인으로 교착상태가 발생합니다.
일반적으로 데이터베이스에서 데드락을 해결하기 위해서는 victim을 선정하여 비선점을 해소하는 방법을 사용합니다.

3.4. 다중 락의 단위

락의 단위가 작을수록 락킹에 따른 오버헤드가 증가하고 동시성의 정도는 증가하게 됩니다.

3.5. 트랜잭션 고립 수준 명령어

DBMS는 트랜잭션의 동시성 제어를 위해 락보다 완화된 방법인 명령어를 사용합니다.

READ UNCOMMITTED (LEVEL = 0)

고립 수준이 가장 낮은 명령어로 자신의 데이터에 아무런 공유락을 걸지 않습니다. 반면에 배타락은 갱신손실 문제로 걸어줍니다. 따라서 오손 데이터 읽기, 반복할 수 없는 읽기, 팬텀 문제는 발생할 수 있습니다.

READ COMMITTED (LEVEL = 1)

오손 데이터 읽기 문제를 피하기 위해 자신의 데이터를 읽는 동안 공유락을 걸어 수정이 불가능하게 합니다. 대부분의 DBMS가 채택하는 고립 수준입니다.

REPEATABLE READ (LEVEL = 2)

자신의 데이터에 설정된 공유락과 배타락을 트랜잭션이 종료될 때까지 유지하여 다른 트랜잭션이 자신의 데이터를 갱신할 수 없도록 합니다. 따라서 SELECT문이 반복적으로 지속될 경우 같은 값으로 반환됩니다.

SERIALIZABLE (LEVEL = 3)

가장 높은 수준의 고립 명령어로 실행 중인 트랜잭션은 다른 트랜잭션으로부터 완벽하게 분리됩니다. SELECT문이 사용하는 모든 데이터에 공유락이 걸리므로 데이터 갱신이 불가능합니다. REPEATABLE READ는 SELECT문에서 사용되는 조건 범위 데이터에만 공유 락이 걸린다는 차이점이 있습니다.

4. 회복

어떤 트랜잭션을 수행하는 도중에 시스템이 다운되었을 때, 해당 트랜잭션의 일부분(원자성 위배)만 데이터베이스에 반영될 수 있습니다. 또는 트랜잭션이 완료된 후 시스템이 다운되면 트랜잭션으로 인한 데이터 갱신이 메모리로부터 디스크에 기록(지속성 위배)되지 않았을 수 있습니다.아니면 물리적 저장 장치의 고장으로 디스크 데이터베이스에 접근할 수 없을 수도 있습니다.

이처럼 다양한 오류로부터 정상 상태로 복귀하는 것이 회복이며 주요 회복 기법으로는 로그 기반, 검사점 기반, 그림자 페이지 사용 등이 있습니다.

4.1. 로그 기반 회복

로그 기반 회복은 Undo와 Redo 동작으로 회복을 수행합니다.

고장이 발생하기 전에 트랜잭션이 완료 명령을 수행했다면 회복 모듈은 이 트랜잭션의 갱신 사항을 재실행(Redo)하여 트랜잭션의 갱신이 데이터베이스에 영구적으로 반영(지속성)시켜야 합니다.

고장이 발생하기 전에 트랜잭션 완료 명령을 수행하지 못했다면 원자성을 보장하기 위해 이 트랜잭션이 데이터베이스에 반영했을 가능성이 있는 갱신 사항을 취소(Undo) 해야합니다.

그렇다면 로그도 어떤 물리적인 디스크에 저장되어야 합니다. 만약 디스크에 문제가 생긴다면 로그도 같이 문제가 생길 수 있기 때문에 안전 저장 장치(stable storage)를 사용합니다. 현대의 디스크는 RAID기법을 사용하여 여러 디스크를 하나로 묶어서 사용하기 때문에 로그 정보를 중복하여 다른 디스크에 나누어 저장해 하나의 디스크에 문제가 생기더라도 다른 디스크에 저장되어 있는 로그 정보를 사용할 수 있도록 구성합니다.

로그 레코드는 식별 가능한 번호(로그 순서 번호, LSN)를 부여하여 저장됩니다.
Redo는 로그 파일에 시작(START)과 끝(COMMIT)을 기준으로 내용을 기록하는 과정이며, Undo는 로그 파일에 끝(COMMIT)이 없는 경우 데이터베이스에 내용이 기록되었을 수 있기 때문에 원상복구를 시키게 됩니다.

즉시 갱신 : 갱신 데이터 -> 로그, 버퍼 -> 데이터베이스 작업이 부분완료 전에 동시에 진행될 수 있다.

지연 갱신 : 갱신 데이터 -> 로그가 전부 끝난 후 부분 완료 하고 버퍼 -> 데이터베이스 작업이 진행된다.

4.2. 검사점(checkpoint) 기반 회복

시스템이 다운된 시점으로부터 오래 전에 완료된 트랜잭션들이 데이터베이스를 갱신한 사항은 이미 디스크에 반영되었을 가능성이 큽니다. DBMS가 로그를 사용하더라도 어떤 트랜잭션의 갱신 사항이 메모리 버퍼로부터 디스크에 기록되었는가를 구분할 수 없습니다. 따라서 DBMS는 회복시 재수행할 트랜잭션 수를 줄이기 위해서 주기적으로 체크포인트를 수행합니다.

체크포인트 시점에는 메모리 버퍼 내용이 디스크에 강제로 기록되므로 체크포인트를 수행하면 디스크 상에서 로그와 데이터베이스의 내용이 일치하게 되며 체크포인트 작업이 끝나면 로그에 checkpoint 로그 레코드가 기록되어 구분할 수 있습니다.

4.3. 그림자 페이징

일반적으로 갱신이 일어나면 디스크 내의 페이지에 덮어쓰기 방식으로 수행합니다. 그림자 페이징 방식은 이런 방법을 사용하지 않고 새로운 데이터를 새로운 페이지에 저장하게 합니다. 이러한 방식은 트랜잭션 중간에 문제가 생기더라도 old data는 영향을 받지 않는 특징을 이용해 회복할 수 있습니다.

profile
CS | ML | DL

0개의 댓글