트랜잭션: Transaction
트랜잭션은 DB에서 일련의 작업을 논리적으로 묶은 것입니다.
즉 여러 SQL 쿼리문을 하나의 처리로 묶는 것입니다. 유명한 예를 들어보겠습니다.
예제)
- 고객 A와 B가 있습니다.
- 고객 A가 고객 B에게 1,000원을 이체하려는 상황입니다.
문제 1.
고객 A가 고객 B에게 1,000원을 이체하게 된다면
고객 A의 잔고는 -1,000원 연산이 이루어져야 하고, 고객 B의 잔고는 +1,000원 연산이 이루어져야 합니다.
하지만 고객 A의 잔고가 줄어드는 연산은 이루어지고, 그 후 오류가 발생하여 B의 잔고가 오르는 연산이 이루어지지 않으면 고객 A의 돈은 감소하고 고객 B의 돈은 증가하지 않게 됩니다.
우리가 바라는 것은 오류가 발생하게 되어 B의 잔고가 오르지 않는다면, 당연히 고객 A의 잔고도 이체하기 전 금액으로 돌아오는 것일겁니다.
문제 2.
고객 A가 B에게 1,000원을 이체하고 있는 도중에 다른 사용자가 고객 A의 잔액을 읽게된다면, 엉뚱한 값을 얻을 수 있습니다. 또한 극단적으로 생각하여 잔고가 1,000원인 고객 A가 동시에 1,000원을 여러 번 이체한다고 가정해보면, A 명령들은 잔고에 1,000원이 있다 생각하여 여러 번 이체하는 작업을 수행할 것이고, 고객 A의 잔액은 음수가 될 것입니다.
이런 문제들은 `트랜잭션`을 통해 해결할 수 있습니다. `트랜잭션`이 적용되면 고객 A의 잔고가 줄어드는 연산과 고객 B의 잔고가 늘어나는 연산이 하나로 묶이게 됩니다. 즉 이체 작업이 원자적인 단위가 되어, 작업이 성공하거나 실패할 때 모두 일관된 상태로 유지할 수 있습니다. 또한 동시에 여러 트랜잭션이 실행되더라도 격리되어 각각의 트랜잭션이 서로에게 영향을 미치지 않습니다.
트랜잭션 ACID
ACID(원자성, 일관성, 고립성, 지속성)는 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질입니다.
원자성 (Atomicity):
트랜잭션은 원자적 연산을 보장해야한다는 뜻입니다. 여기서 원자적 연산이란 작업이 '모두 성공하거나 모두 실패'해야한다는 의미입니다. 즉, 트랜잭션의 단위가 됩니다.
일관성 (Consistency):
트랜잭션이 종료되었을 때 데이터의 무결성이 보장되어야한다는 뜻입니다.
고립성 (Isolation):
트랜잭션은 서로 간섭하지 않고 서로 독립적으로 동작해야한다는 뜻입니다. 하지만 독립적으로 동작함을 보장하기 위해선 동시성 등을 포함한 많은 성능을 포기해야합니다.
성능관련 이유 때문에 개발자가 `격리 레벨`을 설정함으로 Isolation을 제어할 수 있습니다.
지속성 (Durability):
완료된 트랜잭션은 유실되지 않고 결과가 영구적으로 DB에 반영되어야 함을 말합니다. 시스템이 갑자기 중단되어도 트랜잭션의 결과가 유지되어야 합니다. 지속성을 보장하기 위한 방법으로는 변경 사항을 기록하는 Loggin 방법, 변경 전에 해당 변경을 로그에 기록하는 WAL 등이 있습니다.
트랜잭션 격리 레벨
트랜잭션 Isoloation은 동시에 여러 트랜잭션이 실행될 때, 서로 간섭하지 않고 독립적으로 동작해야한다는 뜻입니다. 하지만 이는 성능과 밀접하게 관련이 있기 때문에 개발자가 `격리 레벨`을 설정함으로 트랜잭션 간의 어떤 상호작용을 허용할지를 결정할 수 있습니다. 격리 레벨을 설명하기 위해 다음 세 용어부터 설명하도록 하겠습니다.
트랜잭션 부정합 문제 용어
- Diry Read:
Dirty Read는 한 트랜잭션이 아직 커밋되지 않은 다른 트랜잭션의 변경 내용을 읽는 현상입니다. - Non-Repeatable Read:
한 트랜잭션이 동일한 쿼리를 실행했을 때, 결과가 다르게 나타나는 현상입니다.
예를 들어, 트랜잭션 A가 어떤 데이터를 읽고 있는데, 동시에 트랜잭션 B가 같은 데이터를 수정하고 커밋하는 경우, 트랜잭션 A가 동일한 쿼리를 실행하면 값이 변경됩니다. - Phantom Read:
핸 트랜잭션이 범위(조건)를 지정한 쿼리를 실행했을 때, 다른 트랜잭션이 같은 범위에서 데이터를 추가 또는 삭제하는 현상입니다.
예를 들어, 트랜잭션 A가 특정 조건을 만족하는 데이터를 읽고 있는데, 트랜잭션 B가 해당 조건에 맞는 새로운 데이터를 insert한 경우, 트랜잭션 A가 동일한 쿼리를 다시 실행하면 새로 추가된 데이터가 나타나게 되어 Phantom Read가 발생합니다.
트랜잭션 격리 레벨
- READ UNCOMMITTED (커밋되지 않은 읽기):
- Dirty Read 허용
- Non-Repeatable Read 허용
- Phantom Read 허용
- READ COMMITTED (커밋된 읽기):
- Non-Repeatable 허용
- ReadPhantom Read 허용
- REPEATABLE READ (반복 가능한 읽기):
- Phantom Read 허용
- SERIALIZABLE (직렬화 가능):
- 전부 비허용
Isolation 수준이 높아질수록 (SERIALIZABLE) 일관성이 높아지지만, 동시성이 감소하여 성능은 떨어집니다. 반대로 Isolation 수준이 낮아질수록 (READ UNCOMMITTED) 동시성을 증가하지만, Dirty Read, Non-Repeatable Read, Phantom Read 같은 일관성 문제가 발생할 가능성이 높아집니다.
때문에 실제로 작업을 할 때에 SERIALIZABLE이나 READ UNCOMMITTED는 사용하지 않고, 주로 READ COMMITTED 혹은 REPEATABLE READ를 많이 사용합니다. 둘 중에서도 `REPEADTABLE READ`는 `gap lock` 문제가 발생할 수 있기 때문에 `READ COMMITTED`를 가장 많이 사용합니다. 상황에 따라 적절한 고립성 수준을 선택해주어야 합니다.
gap lock
`gap lock`은 트랜잭션이 일정 범위의 값에 대한 lock을 획득하면서 해당 범위에 속하지 않은 값에 대한 lock도 함께 획득하는 현상입니다.
트랜잭션 A: `WHERE column BETWEEN 10 AND 20 FOR UPDATE;`
트랜잭션 B: `INSERT INTO table (column) VALUES (5);`
트랜잭션 A는 10에서 20까지의 값을 update하기 위해 lock을 획득합니다.
그런데 트랜잭션 B가 5라는 값을 추가하려고 하면, 이 값은 범위에 속하지 않지만 `gap lock`으로 인해 대기 상태에 놓이게 되어 dead lock 상태가 됩니다.
트랜잭션(Transaction) 사용 방법
MySQL에서 트랜잭션 사용하기
`START TRANSACTION`, `COMMIT`, `ROLLBACK`
-- 계좌 이체 트랜잭션
-- 트랜잭션 시작
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT; -- 트랜잭션 커밋 및 종료
-- 또는
ROLLBACK; -- 트랜잭션 롤백
코드에서 트랜잭션 사용하기 (feat. Spring Boot)
1. 선언적 트랜잭션: @Transactional
Spring Boot에서 트랜잭션을 사용하려면 `@Transactional` 어노테이션을 메서드나 클래스에 추가하여 사용하면 됩니다. 해당 어노테이션을 붙여주면 자동으로 트랜잭션을 실행하고 종료하며, 수행 중 예외가 발생하면 롤백됩니다.
메서드에 추가
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
@Transactional
public void myMethod() {
// 트랜잭션 내에서 수행되어야 하는 작업
myRepository.saveEntity(entity1);
myRepository.updateEntity(entity2);
}
}
클래스에 추가
@Service
@Transactional
public class MyService {
@Autowired
private MyRepository myRepository;
public void myMethod1() {
}
public void myMethod2() {
}
}
2. 프로그래밍으로 트랜잭션 다루기: TransactionTemplate 클래스
선언적 트랜잭션 관리와 비슷하게 동작하지만, 더 세말한 제어를 제공합니다. 하지만 웬만한 처리는 선언적 트랜잭션(@Transactional)으로 충분하기 때문에 사용하는 빈도는 낮습니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
@Autowired
private TransactionTemplate transactionTemplate;
public void myMethod() {
transactionTemplate.execute(status -> {
try {
myRepository.saveEntity(entity1);
myRepository.updateEntity(entity2);
return true; // 트랜잭션 성공
} catch (Exception e) {
status.setRollbackOnly(); // 트랜잭션 롤백
return false;
}
});
}
}
`TransactionTemplate`내에 `execute` 메서드를 통해 트랜잭션을 적용합니다. `execute` 내에 트랜잭션 내에서 수행되어야 하는 작업을 정의하고, 트랜잭션이 성공하면 `return true`, 실패하여 롤백이 필요한 경우에는 `status.setRollbackOnly();`를 호출하여 롤백을 할 수 있습니다.
'DBMS > SQL, RDBMS' 카테고리의 다른 글
SQL 조회 성능을 위한 인덱스(Index) (1) | 2024.01.22 |
---|---|
MySQL 데이터베이스(DB)에 배열 넣기 (feat. JSON) (0) | 2023.06.13 |
[MySQL] port 변경하기 (0) | 2022.10.05 |
Maria DB 설치 (0) | 2022.10.04 |
[MyBatis + MySQL] INSERT 시 PK값 가져오기 (0) | 2022.09.27 |