데이터베이스를 다루게 되면 빠질 수 없는 개념 중 하나인 Transaction(이하 '트랜잭션')
에 대해서 이번 게시글에서 다뤄 보려고 한다.
트랜잭션이란 쉽게 말해 "쪼갤 수 없는 업무 처리의 최소 단위"이라고 할 수 있으며 다른 표현으로는 데이터베이스에서 하나의 논리적 기능을 수행하기 위한 작업의 단위이다. 데이터베이스에 접근하는 방법은 쿼리이기에 여러 개의 쿼리들을 하나로 묵는 단위로도 볼 수 있다. 이 때, 1초당 처리 할 수 있는 트랜잭션의 개수를 TPS라고 한다.
트랜잭션은 데이터의 부정합을 방지하기 위해 꼭 필요한 개념이기에 어느정도의 기초 개념을 알고 넘어가야 한다.
트랜잭션에는 중요한 4가지의 특징이 존재하는데, 이들은 각 원자성(Atomicity), 일관성(Consistency), 독립성(Indepence), 그리고 지속성(Durability)로 일명 ACID
로 불리운다.
이 게시글에서는 아래의 코드를 참고로 트랜잭션의 특징을 알아보려고 한다.
[계좌 이체]
import { getConnection, Transaction } from "typeorm";
async function transferFunds(fromAccountId: number, toAccountId: number, amount: number) {
const connection = getConnection();
return await connection.transaction(async transactionalEntityManager => {
const fromAccount = await transactionalEntityManager.findOne(Account, { id: fromAccountId });
const toAccount = await transactionalEntityManager.findOne(Account, { id: toAccountId });
if (!fromAccount || !toAccount) {
throw new Error("Account not found");
}
if (fromAccount.balance < amount) {
throw new Error("Insufficient funds");
}
fromAccount.balance -= amount;
toAccount.balance += amount;
await transactionalEntityManager.save(fromAccount);
await transactionalEntityManager.save(toAccount);
return true;
});
}
위의 코드는 계좌이체 행위가 일어나는 Query를 TypeORM으로 TypeScript 언어를 활용하여 간단하게 만든 함수입니다.
기초적인 트랜잭션 문법에 대해서는 따로 다루질 않을 예정이니 궁금하신 분들은 TypeORM의 공식문서를 참고 바랍니다.
TypeORM - Transaction
트랙잭선이 시작되었을 때 그 업무가 모두 수행되었거나 되지 않았을 때의 결과를 보장한다.
위의 코드를 한 번 살펴보자, 위 함수는 매개변수로 출금되는 통장의 아이디와, 입금되는 통장의 아이디 그리고 금액을 받아서 데이터베이스에 연결 후 트랜잭션을 실행하는 함수 입니다.
이 때 트랜잭션 함수가 실행되면 데이터베이스의 사용자는 중간 과정을 볼 수가 없으며 인수와 그 인수에 대한 결과 값을 기다릴 수 밖에 없습니다.
그리고 트랜잭션이 만약 출금 통장에서 돈이 빠져나가 입금통장에 돈이 입금 되려던 찰나에 실패했다 하더라도 트랜잭션은 롤백
을 지원하기 때문에 결과가 저장이 되지 않고 함수가 실행됙 전의 상태로 돌아가게 된다. 만약 입출금이 전부 제대로 행해졌다면 결과가 커밋
이 되고 트랜잭션이 성공적으로 마무리 된다.
위에서 서술한 것 처럼, 트랜잭션은 아주 중요한 2개의 키워드를 가지고 있는데, 그것이 바로 롤백
과 커밋
이다.
하나의 트랜잭션이 성공적으로 끝까지 실행되었을 시 최종적으로 커밋이 진행되며 트랜잭션을 확정하는 명령어 이다.
=> 커밋이 실행되지 않으면, 트랜잭션이 마무리 되지 않은 상태이기에 DataBase(DB)에 반영이 되지 않는다.
만약 트랜잭션 도중 에러 또는 서버의 문제가 생긴다면 트랜잭션이 실행되기 바로 전 상태로 돌아가는데 이것을 롤백이라고 한다.
위에 언급된 커밋
과 롤백
덕에 데이터의 무결성이 보장되고 데이터 관리가 조금 더 수월해집니다.
데이터 베이스에 기록된 모든 데이터는 여러 가지 조건, 규칙에 따라 유효함을 가지게 되는데, 만약 이 유효성을 통과하지 못할 경우, 데이터의 수정은 불가하다. 이 때 일관성을 가지게 된다고 한다. 즉, 일관성은
허용된 방식
으로만 데이터를 변경해야 하는 것 을 뜻한다.
예를 들어, 위 코드에서는 DB의 스키마 정립되어 있지 않아 이해가 어려울 수 있으나, 만약 스키마에서 계좌의 최소 금액을 0이라고 설정을 해놓았다면 fromAccount.balance
가 0 밑으로 떨어질 경우 DB 변경이 거부가 되어 실행되지 않고 롤백
이 된다.
하나의 트랜잭션 수행시 다른 트랜잭션의 작업이 끼어들지 못하도록 보장하는 것이다. 즉, 트랜잭션 끼리는 서로를 간섭할 수 없다. 트랜잭션이 실행하는 도중에 변경한 데이터는 이 트랜잭션이 완료될 때까지 다른 트랜잭션이 참조하지 못하게 하는 특성이다.
=> 물론 순차적으로 데이터를 실행하면 된다고 생각할 수도 있지만, 이렇게 할 시 성능은 말할 것도 없다.
격리성은 다른 특징보다 약간 복잡한데, 그 이유는 격리 수준이 여러개로 나눠지며, 그 격리 수준에 따라 발생하는 현상이 달라진다.
하나 하나 천천히 알아보자
1 > 4 번 수준으로 4번으로 갈수록 격리 수준이 낮아진다고 생각하면 된다.
뜻 그대로, 트랜잭션을 순차적으로 진행 하는 것
위에서 언급했지만 병렬적으로 트랜잭션을 진행하는 것이 가장 이상적인 것에 비해 직렬화를 시키게 되면 A -> B -> C 수준으로 업무가 진행되고 A가 끝나야지만 B가 실행되기 때문에 성능이 많이 떨어진다.
하나의 트랜잭션이 수정한 행을 다른 트랜잭션이 수정할 수 없도록 도와주지만 새로운 행을 추가하는 것은 막지 않습니다.
현재 많은 DataBase들의 기본 값으로 설정이 되어있는 격리 수준이며 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없게 되어 있다. 즉, 커밋이 완료된 데이터만 읽을 수 있다.
하지만 트랜잭션 A와 트랜잭션 B가 서로의 데이터를 수정 할 수 있기 때문에 커밋의 결과 값이 영향을 받을 수 있다.
무차별적으로 커밋이 되지 않아도 트랜잭션이 끼리 서로 조회가 가능하며 수정도 가능하다. 데이터의 무결성을 해칠 확률이 높지만 그만큼 성능은 가장 빠르다.
트랜잭션 내에서 동일한 쿼리를 보냈을 때 해당 조회 결과가 다른 경우
예를 들어, 트랜잭션 T1과 T2가 있고 T1은 A은행에 등록된 계좌의 정보들을 조회하고 T2는 새로운 계좌를 등록하고 있다고 해보자 이 때 T1이 작업중이고 T2가 커밋까지 완료가 되었을 시 T1은 작업이 끝나기 전이기에 T2로 인해 등록된 새로운 계좌도 조회하게 된다.
한 트랜잭션 내에 같은 행에 두 번 이상 조회가 발생했을 때 결과 값이 다른 경우
쉽게 말해 계좌 내역을 2번 조회하는데 첫 번째 조회 내역과 두 번째 조회 내역이 다른 트랜잭션의 수정으로 인해 다른 결과를 내 뱉게 되는 경우를 뜻 한다.
바로 예제를 통해서 뜻을 알아보자.
A와 B의 트랜잭션이 동시에 진행 중일 때 A의 트랜잭션은 단순 조회, B의 트랜잭션은 A가 읽어오는 데이터 중 일부 수정이라고 가정했을 때 B가 데이터를 수정은 하였지만 커밋이 완료되지 않은 상태에서 A가 수정된 내용까지 조회하는 경우를 뜻 한다.
사실 아직까지 다양한 격리 수준으로 프로그램을 짜본적이 없고 Transaction을 이용할 만큼의 로직을 구현해본적이 없어 위에 현상을 겪어 보지는 못했다.
성공적으로 커밋된 트랜잭션은 영원히 반영된다는 것을 의미한다. 쉽게 말해 데이터베이스에 문제가 발생해도 원래 상태를 유지 할 수 있어야 하는 기능을 뜻한다.
위에서 간혹 언급되었던 무결성이라는 개념은 단어 자체에서 알 수 있 듯이 결성이 없다는 뜻이지만 조금 더 자세히 파보자면
데이터의 정확성, 일관성, 유효성을 유지하는 것을 뜻한다.
개체 무결성(PRIMARY KEY) - 기본키로 선택된 필드는 빈 값을 허용하지 않는다
참조 무결성(FOREIGN KEY) - 서로 참조 관계에 있는 두 테이블의 데이터는 항상 일관된 값을 유지 한다.
고유 무결성(UNIQUENESS) - 고유 값으로 설정되면 그 값은 고유해야 한다.
NULL 무결성 - NOT NULL 과 NULL 은 유지되어야 한다.