통상적으로 정보의 교환이나 데이터베이스 갱신 등 일련의 작업들에 대한 연속처리단위를 의미하고, 데이터베이스의 무결성이 보장되는 상태에서 요청된 작업을 완수하기 위한 작업의 기본 단위로 간주한다. (매일경제, 경제용어사전)
데이터베이스의 상태를 변화시키는 작업의 모음
주로 데이터베이스의 상태를 변화시키는 INSERT, DELETE, UPDATE 중 한 개 이상의 DML(Data Manipulation Language)과 같이 사용된다.
트랜잭션(Transaction)이란 질의(Query)를 하나의 묶음 처리해서 만약 중간에 실행이 중단됐을 경우, 처음부터 다시 실행하는 Rollback을 수행하고, 오류없이 실행을 마치면 commit을 하는 실행단위이다. 즉, 한 번 질의가 실행되면 질의가 모두 수행되거나 모두 수행되지 앟는 작업 수행의 논리적인 단위이다.
트랜잭션은 여러 개의 작업들을 하나의 실행 유닛으로 묶어준 것이다. 각 트랜잭션은 하나의 특정 작업을 시작으로하여 묶여있는 모든 작업을 다 완료해야 끝나게 되어 있다. 만약 한 개의 작업이라도 실패하게 된다면 전부 실패하게 된다. 즉, 작업이 하나라도 실패를 하게 되면 모두 실패이고 모든 작업이 성공적이라면 트랜잭션 또한 성공적이게 된다. 이처럼 트랜잭션은 성공 혹은 실패라는 결과만 존재한다. 미완료된 단계없이 전부 성공한다는 뜻이다.
송금 과정을 하나의 트랜잭션으로 보고 예를 들어보자.
트랜잭션이 'A가 B에서 100만원을 입금해야한다.'이라면 총 5개의 작업으로 이루어져있다.
1. A의 통장이 활성화되어있다.
2. A가 100만원 이상을 가지고 있는지 확인한다.
3. A의 계좌에서 100만원을 출금한다.
4. B의 통장이 활성화되어있는지 확인한다.
5. B의 계좌에 100만원을 입금한다.
위의 5가지 중 어느 하나라도 실패한다면 A가 B에게 100만원을 입금시켜서는 안된다. 중간에 하나라도 오류가 발생하면 다시 처음부터 송금을 하는 것이 rollback이다.
오류가 없이 정상적으로 송금이 됐다면 실행이 종료되었으므로 commit을 한다.
트랜잭션은 확정 신호를 알려주어야 데이터베이스에 반영된다. A가 B에게 100만원을 보낸다라는 트랜잭션을 실행한 뒤 확정 신호를 보내야 데이터베이스에 반영이 된다. 확정신호를 보내지 않느다면 데이터베이스에 내용 변화가 이루어지지 않는다.
이러한 확정 신호를 commit이라고 한다.
만약 auto-commit기능이 켜져있다면 확정신호를 보내지 않더라도 테이블을 생성하고 데이터가 INSERT, UPDATE. DELETE되는 것을 확인할 수 있다.
SQLite을 기반으로 예를 들어보자면 아래와 같다.
CREATE TABLE user(
id VARCHAR(10) PRIMARY KEY,
name VARCHAR (10)
);
INSERT INTO user VALUES ('id1', 'user1');
INSERT INTO user VALUES ('id2', 'user2');
COMMIT;
맨 아래의 commit 명령을 실행한다면 메모리에 저장되어있던 작업의 결과가 테이블에 반영된다.
Rollback은 commit과 반대되는 개념으로 앞으로 변경될 작업에 대한 내용을 취소하겠다는 개념이다. 즉, 트랜잭션 수행 중에 지금까지 수행한 내용을 모두 취소한다라는 의미이다.
SQLite을 기반으로 예를 들어보자면 아래와 같다.
INSERT INTO user VALUES ('id3', 'user3');
ROLLBACK;
INSERT INTO user VALUES ('id4', 'user4');
COMMIT;
위의 SQL은 원래 user 테이블에 id3과 id4를 추가하는 트랜잭션이었으나 중간에 Rollback
을 통해 id4만 user 테이블에 추가된다.
트랜잭션은 DB 서버에 여러 개의 클라이언트가 동시에 엑세스하거나 응용프로그램이 갱신을 처리하는 과정에서 중단될 수 있는 경우 등 데이터 부정합을 방지하고자 할 때 사용한다. 부정합이 발생하지 않으려면 프로세스를 병렬로 처리하지 않도록 하여 한 번에 하나의 프로세스만 처리하도록 하면 되지만 이는 효율이 지나치게 떨어진다. 즉, 병렬로 처리해야하는 현실적인 문제로 인한 부정합을 방지하고자 트랜잭션을 사용하게 된다.
ACID는 Atomity, Consistency, Isolation, Durability를 가리키는 말로 각 단어는 데이터베이스 내에서 일어나는 하나의 트랜잭션의 안정성을 보장하기 위해 필요한 성질이다.
하나의 트랜잭션을 구성하는 작업들은 전부 성공하거나 전부 실패해야한다. 부분적으로 실행되면 안되는 것을 의미한다.
충돌요인에 대해 선택지를 제공하기도하지만 SQL에서 특정 쿼리를 실행했는데 부분적으로 실패하는 부분이 있다면 원자성에 의해 전부 실패하게 되어있다.
트랜잭션에서 원자성은 수행하고 있는 트랜잭션에 의해 변경된 내역을 유지하면서, 이전에 commit된 상태를 임시영역에 따로 저장해서 원자성을 보장한다. 현재 수행하고 있는 트랜잭션에 오류가 발생하면 수행하고 있는 내역대신 임시 영역에 저장된 이전의 상태로 rollback을 한다.
이전의 데이터들이 임시로 저장되는 영역을 rollback segment라고 하는데, 현재 수행하고 있는 트랜잭션에 의해 새롭게 변경되는 내역을 데이터베이스 테이블이라고 한다. 즉, 원자성은 rollback segment에 의해 보장되고 있다고 할 수 있다.
그렇다면 트랜잭션의 길이가 매우 길어졌을 때에 rollback을 하게 되면 오류가 발생하지 않은 부분도 처음부터 작업을 수행해야한다. 확실한 부분에 대해서는 rollback을 하지 않도록 중간 저장 지점은 save point를 지정할 수 있다. save point로 rollback을 하게 되면 save point 이전은 확실하다 간주하고 그 이후부터 롤백을 하게 된다.
하나의 트랜잭션 이전과 이후 데이터베이스 상태는 이전과 같이 유효해야한다. 즉, 데이터베이스의 제약이나 규칙에 의거한 데이터베이스이어야한다는 뜻이다.
각 데이터베이스의 유효한 상태는 다를 수 있지만 상태에 대한 일관성을 변하지 않아야 한다.
CREATE TABLE user(
id VARCHAR(10) PRIMARY KEY,
name VARCHAR(10)
);
위의 테이블에서 다음과 같은 트랜잭션들은 해당 성질을 위반한다. 즉, 아래의 트랜잭션을 한 이후의 데이터베이스의 상태가 일관되지 않기 때문에 일관성을 위반하는 것이다.
트랜잭션에서 일관성은 트랜잭션 수행 전후에 데이터 모델의 모든 제약조건(기본키, 외래키, 도메인, 도메인 제약조건 등)을 만족하는 것을 통해 보장한다. A라는 테이블의 B 테이블의 primary key인 customer_id가 외래키로 존재한다고 가정해보자. customer_id의 제약조건이 B 테이블에서 변경되면 A 테이블에서도 customer_id가 변경되어야 한다. 한 쪽의 테이블에만 데이터의 변경사항이 이루어지면 안된다.
트랜잭션은 어떤 이벤트와 조건이 발생했을 때 트리거(trigger)를 통해 보장한다. 트리거는 데이터베이스 시스템이 자동적으로 수행할 동작을 명시하는 데에 사용된다.
CREATE trigger customerId_check
AFTER update of customerId on B
.....
for each row
.....
begin
.....
end
create은 트리거를 생성하고 after는 트리거가 실행되기 위한 event를 나타낸다.
고립성(isolation)은 하나의 트랜잭션이 다른 트랜잭션과 독립되어야 한다는 뜻이다. 실제로 동시에 여러 개의 트랜잭션들이수행될 때에 각 트랜잭션들은 고립되어 있어 연속으로 실행된 것과 동일한 결과를 나타내야 한다.
이를 예시를 통해 더 구체적으로 살펴보자.
A의 계좌에 150만원있다고 해보자. B와 C에게 각각 100만원을 보내기에는 부족하다. 하지만 동시에 하는 경우 연속으로 계좌 B에 먼저 보낸 뒤에 C에게 보내는 것과 결과가 같아야한다는 것이다. B에게 보낸 뒤 잔액이 부족하면 C에게 보낼 수 없다. 둘 중 조금이라도 먼저 시행한 것에 100만원을 보낸 뒤 부족하면 트랜잭션이 원자성으로 인해 아예 실행이 되지 않는 것이다.
각 트랜잭션은 다른 트랜잭션의 연산 내용을 알 수 없다.
또한 동시에 실행될 때와 연속으로 실행될 때의 데이터베이스 상태가 동일해야 한다.
트랜잭션이 고립성을 어떻게 보장하는지 살펴보기 위해서는 병행 트랜잭션에 대해 먼저 알아보아야 한다.
병행처리(concurrent processing)이란 CPU가 여러 프로세스를 처리하는 것처럼, 트랜잭션에 정해진 시간을 할당해서 작업을 하다가 부여된 시간이 끝나면 다른 트랜잭션을 실행하는 방식으로 트랜잭션들을 조금씩 처리하는 것을 말한다. 이렇게되면 많은 트랜잭션들이 조금씩 처리되는 과정에서 공통된 데이터를 조작하게 되는데 데이터가 혼란스러워질 수 있는 우려가 있다.
❓ 데이터가 혼란스러워진다는 것은 mutable한 객체의 단점과 어느 정도 연관이 있는 것으로 보인다.
A 트랜잭션에서 X라는 데이터를 10이라고 설정한 뒤 시간이 만료되어 B 트랜잭션으로 넘어갔다고 했을 때 B 트랜잭션에서 X를 50이라고 저장한 뒤 다시 시간이 만료되어 A가 실행될 경우 X의 값이 50인 상태에서 진행이 되는 것처럼 데이터의 처리가 혼란스러워지는 것이다. 트랜잭션의 원자성과 충돌할 것으로 보인다. 다시 알아봐야한다..
예전에 컴퓨터가 병렬로 동작할 때 하나를 쭉 처리하고 다른 걸 처리하는 것으로 보이지만 작동 시간을 줄이기 위해 빠르게 병렬로 처리한다는 것을 학부 때 들었던 것 같다. 컴퓨터가 작업을 하는 방식과 문제가 해결되는 방식의 차이에서 오는 충돌인지 알아보고 싶다.
이처럼 트랜잭션이 조금씩 수행될 때 공통된 데이터가 다른 트랜잭션에 의해 방해되면 안된다. 트랜잭션의 간섭이 일어날 경우 갱신분실, 오손판독, 반복불가능, 팬텀문제 등의 여러 문제점이 발생할 수 있기 때문이다.
트랜잭션의 고립성을 보장하는 방법으로는 OS의 세마포어(semaphore)와 비슷한 개념으로 lock & excute unlock을 통해 고립성을 보장할 수 있다. 즉, 데이터를 읽거나 쓸 때는 문을 잠궈서 다른 트랜잭션이 접근하지 못하도록 고립성을 보장한다. 수행을 마치면 unlock을 통해 데이터를 다른 트랜잭션이 접근할 수 있도록 허용한다.
트랜잭션에서는 데이터를 읽을 때 여러 트랜잭션이 읽을 수는 있도록 허용하는 shared_lock을 한다. shared_lock은 데이터 쓰기를 허용하지 않고 오직 읽기만 허용한다.
또한 데이터를 쓸 때에는 다른 트랜잭션이 읽거나 쓸 수 없도록 하는 exclusive_lock을 사용하고 읽고 쓰는 작업이 끝나면 unlock을 통해 다른 트랜잭션이 lock을 할 수 있도록 데이터에 대한 현재 트랜잭션의 lock을 풀어준다. lock과 unlock을 잘못 사용하면 모든 트랜잭션이 아무것도 수행할 수 없는 deadlock에 빠질 수 있다.
이러한 deadlock에 빠지지 않도록 어떤 규칙에 의해서 고립성을 보장해야하는 2PL 프로토콜이 연구되었다고 한다. 이 프로토콜은 여러 트랜잭션이 공유하고 있는 데이터에 동시에 접근할 수 없도록 하기 위한 것으로 2가지 단계의 locking이 존재하는데 growing phase(상승 단계)와 shrinking phase(하강 단계)로 구성된다. 상승 단계에는 read_lock, write_lock이 있고 하강 단계에는 unlock이 있다. 이 두 단계는 섞이지 않도록 하여 deadlock을 방지하는 것이다. lock과 unlock이 번갈이 수행되지 않고 lock이 쭉 수행된 후 unlock이 쭉 수행되도록 하는 것이다.
작업의 성능을 위해 병행처리를 하되 트랜잭션의고립성을 보장하기 위해 2PL을 사용한 Serializable Schedule을 하게 된다.
locking을 하는 방법에도 두 가지 방법이 있다.
1. conservative locking
트랜잭션이 시작되면 모든 lock을 얻는 방식으로써, 데드락이 발생하지는 않지만 병행성이 좋지 않다.
지속성은 하나의 트랜잭션이 성공적으로 수행되었다면 해당 트랜잭션에 대한 로그가 남고 런타임 오류나 시스템 오류가 발생해도 해당 기록은 영구적이어야 한다는 뜻이다.
은행에서 계좌이체를 성공적으로 한 뒤에 해당 은행 데이터베이스에 오류가 발생해 종료가 되어도 계좌이체 내역은 남아있어야 하는 것이다.
마찬가지로 계좌 이체를 로그로 기록하기 전에 시스템 오류 등에 의해 종료가 된다면 해당 이체 내역은 실패로 돌아가고 각 계좌들은 계좌이체 이전의 상태들로 돌아가게 된다.