[DB] 트랜잭션, 격리수준에 대해서 알아보자.(+ 면접 준비)
트랜잭션이란?
트랜잭션은 데이터베이스에서 하나의 작업 단위로 수행되는 연산 집합이다. 트랜잭션은 작업이 모두 성공적으로 완료되거나, 실패 시 모든 작업을 취소(롤백)함으로써 데이터의 일관성을 보장한다. 하나의 트랜잭션은 Commit 되거나 Rollback 된다.
트랜잭션을 왜 사용할까?
데이터의 일관성을 유지하면서 안정적으로 데이터를 복구하기 위함이다. 트랜잭션은 하나의 논리적인 작업의 단위이기 때문에, 여러 개의 작업을 하나의 논리적인 단위로 묶어서 반영과 복구를 조정할 수 있기 위해 사용한다. 따라서, 데이터의 부정합이 일어났을 경우 롤백을 하여 데이터의 부정합을 방지할 수 있다. (참고)
또한 동시성 제어에도 사용한다. 데이터베이스는 다수의 사용자가 동시에 동일한 데이터에 접근할 수 있으므로, 데이터 충돌 방지를 위해 트랜잭션은 락(Lock) 메커니즘을 사용한다. Exclusive Lock을 통해서는 데이터 변경 시 트랜잭션이 완료될 때까지 다른 트랜잭션은 읽기와 쓰기 모두 불가능하다. Shared Lock을 통해서는 데이터를 읽을 시, 여러 사용자가 동시에 데이터를 읽을 수 있지만 수정이 불가능하다. 이를 통해서 데이터가 작업 순서대로 처리되도록 보장한다.
락(Lock)
트랜잭션을 사용하는 이유가 전체 단위를 롤백만 하기 위한 것은 아니다. DB 락이라는 것이 있는데 여러 사용자가 동일한 데이터를 액세스할 때 작업이 순차적으로 처리되도록 함으로써 일관성과 정합성을 보장할 수 있다.
Lock종류는 두 종류가 있다.
- Exclusive Lock
Exclusive Lock은 데이터를 변경하고자 할 때 사용되며 트랜잭션이 완료될 때까지 유지된다. Exclusive Lock는 해제될 때까지 다른 트랜잭션(읽기 포함)이 해당 리소스에 접근할 수 없다. 또한 해당 Lock은 다른 트랜잭션이 수행되고 있는 데이터에 대해서는 접근하여 함께 Lock을 설정할 수 없다. Write Lock이라고도 불린다.
- Shared Lock
Shared Lock은 데이터를 읽을 때 사용되어지는 Lock이다. Shared Lock 끼리는 동시에 접근이 가능하다. 즉, 동시에 여러 사용자가 수정은 할 수 없지만 데이터를 읽을 수는 있다. 하지만 Shared Lock이 설정된 데이터에 베타 Lock을 사용할 수는 없다. Read Lock이라고도 불린다.
즉, DBMS에서는 락을 통해 동시성을 제어하게 된다. 이때 락의 범위를 비즈니스에서 필요한 최소한으로 설정하는 것이 중요하다.
락을 너무 넓게 가져가면 쉽게 일관성과 정합성이 보장되지만 대기하는 DB 커넥션이 많아지므로 커넥션풀 고갈로 이어질 수 있다.
(MySQL에서는 트랜잭션의 커밋 혹은 롤백 시점에 잠금이 풀린다. 트랜잭션이 곧 락의 범위가 된다.)
즉, 트랜잭션의 범위를 최소화하는 것이 락의 범위를 최소화하는 것이 된다.
참고
ACID : 트랜잭션의 4가지 특징
- Atomicity(원자성): 모든 연산이 완료되거나, 모두 취소된다. (All or Nothing)
- Consistency(일관성): 트랜잭션 수행 전후 데이터의 일관성이 유지된다.
- Isolation(독립성): 실행 중인 트랜잭션의 중간 결과를 다른 트랜잭션이 접근할 수 없다.
- 동시에 여러 트랜잭션이 실행될 때 데이터 충돌이나 불일치가 발생하지 않도록 보장한다. 제 3자가 도중에 쿼리를 실행하더라도 특정 계좌간 이체하는 양 쪽 데이터에 접근할 수 없다.
- Durability(지속성): 트랜잭션 완료 후 데이터는 영구적으로 저장된다.
트랜잭션에서 발생하는 Commit과 Rollback
Commit은 트랜잭션이 성공적으로 완료된 후, 데이터베이스에 수행한 변경 사항을 영구적으로 저장하는 연산이다. 트랜잭션의 최종 상태를 확정하며, 다른 트랜잭션이 이 데이터를 사용할 수 있도록 만든다.
- 변경사항 확정: 데이터베이스에 수행된 작업이 영구적으로 저장된다.
- ACID의 Durability 보장: Commit된 데이터는 시스템 장애가 발생해도 손실되지 않는다.
- 공유 가능: Commit 후 다른 트랜잭션에서 데이터를 읽거나 수정할 수 있다.
Rollback은 트랜잭션 도중 오류가 발생하거나 취소 명령이 내려졌을 때, 트랜잭션이 시작되기 전의 상태로 데이터를 되돌리는 연산이다.
- 변경사항 취소: 트랜잭션 도중 발생한 모든 작업이 취소된다.
- ACID의 Atomicity 보장: 모든 작업이 성공하거나 실패해야 한다.
- 일관성 유지: 데이터베이스가 트랜잭션 시작 이전 상태로 복구된다.
트랜잭션 상태
트랜잭션은 실행 중에 다음과 같은 상태를 거친다.
상태 | 설명 |
Active | 트랜잭션이 실행 중이며 작업을 진행하는 상태. |
Partially Committed | 모든 연산이 완료되고 Commit 연산이 실행되기 전의 상태. |
Committed | Commit이 성공적으로 완료되어 변경 사항이 영구적으로 저장된 상태. |
Failed | 트랜잭션 중 오류가 발생하거나 실패가 감지된 상태. |
Aborted | Rollback이 완료되어 데이터베이스가 트랜잭션 이전 상태로 복구된 상태. |
- Commit 연산 상태 변화 예시
- Active 상태
- 트랜잭션이 작업을 수행 중.
- 예: UPDATE accounts SET balance = balance - 100 WHERE id = 1;
- Partially Committed 상태
- 트랜잭션의 모든 연산이 성공적으로 실행되었지만, 아직 Commit이 완료되지 않은 상태.
- 예: 트랜잭션 내 모든 SQL이 실행됨.
- Committed 상태
- Commit 연산이 완료되며 변경 사항이 영구적으로 저장됨.
- 예: 데이터베이스에 COMMIT; 명령이 실행되고 저장 완료.
- Rollback 연산 상태 변화 예시
- Active 상태
- 트랜잭션이 작업을 수행 중.
- 예: UPDATE accounts SET balance = balance - 100 WHERE id = 1;
- Failed 상태
- 트랜잭션 실행 중 오류가 발생하거나 취소 명령이 내려진 상태.
- 예: balance 값이 음수가 되는 비즈니스 로직 오류.
- Aborted 상태
- Rollback 연산이 완료되어 트랜잭션 이전 상태로 복구된 상태.
- 예: ROLLBACK; 명령이 실행되어 변경 사항 취소.
격리수준은 트랜잭션 간의 간섭을 방지하기 위한 접근 레벨이 이며 DB에 따라 설정이 가능하다. 격리 수준은 다수의 트랜잭션이 동시에 실행될 때, 데이터의 무결성과 일관성을 보장하면서도 시스템 성능을 최적화하기 위해 필요하다. 높은 격리수준은 데이터의 일관성을 더 잘 보장하지만, 성능 저하를 초래할 수 있다. 하지만, 낮은 격리수준은 트랜잭션 간 충돌 가능성을 높인다.
SQL 표준에서 정의된 격리수준(Transaction Isolation) 4가지에 대해 알아보자.
- SERIALIZABLE: 가장 높은 격리수준. 완벽한 트랜잭션 독립성을 보장한다.
- REPEATABLE READ: 동일한 쿼리를 실행할 때 데이터가 변하지 않음을 보장한다.
- READ COMMITTED: 커밋된 데이터만 읽는다.
- READ UNCOMMITTED: 커밋되지 않은 데이터도 읽을 수 있다.
격리 수준 | 설명 |
SERIALIZABLE (레벨 3) |
|
REPEATABLE READ (레벨 2) |
|
READ COMMITTED (레벨 1) |
|
READ UNCOMMITTED (레벨 0) |
|
격리수준 | 성능 | 동시성 | 데이터 일관성 |
SERIALIZABLE | 매우 낮음 | 매우 낮음 | 가장 강력함 |
REPEATABLE READ | 낮음 | 낮음 | Dirty Read, Non-repeatable Read 방지 |
READ COMMITTED | 보통 | 보통 | Dirty Read 방지 |
READ UNCOMMITTED | 매우 높음 | 매우 높음 | 가장 약함 (Dirty Read 허용) |
각 격리수준 별로 발생하는 무결성 문제들에 대해서 더 자세히 알아보겠다.
3가지 무결성 문제
1. Dirty Read : 한 트랜잭션이 아직 커밋되지 않은 데이터를 읽는 경우를 말한다.
해당 문제는 READ UNCOMMITTED 격리수준에서 발생할 수 있다.
EX)
- 트랜잭션 A: 데이터 업데이트 실행 (아직 커밋하지 않음).
- 트랜잭션 B: 트랜잭션 A의 변경사항을 읽음.
-- 1. 트랜잭션 A:
UPDATE accounts SET balance = 500 WHERE id = 1; -- balance를 500으로 변경 (커밋 X)
-- 2. 트랜잭션 B:
SELECT balance FROM accounts WHERE id = 1; -- 결과: 500 (Dirty Read)
-- 3. 트랜잭션 A:
ROLLBACK; -- 데이터 원상복구 (balance = 1000)
예시를 보면 트랜잭션 B는 존재하지 않는 500이라는 값을 읽은 것을 볼 수 있다. 이 때 발생한 것이 Dirty Read이다.
2. Non-repeatable Read : 한 트랜잭션 내에서 같은 데이터를 두 번 읽었을 때, 다른 값이 반환되는 경우를 말한다.
해당 문제는 READ COMMITTED 이하 격리수준에서 발생할 수 있다. 다른 트랜잭션이 데이터를 수정 또는 삭제하고 커밋한 결과가 반영될 떄 발생한다.
EX)
- 트랜잭션 A: 같은 데이터를 두 번 읽음.
- 트랜잭션 B: 트랜잭션 A 사이에 데이터를 수정하고 커밋.
-- 1. 트랜잭션 A:
SELECT balance FROM accounts WHERE id = 1; -- 결과: 1000
-- 2. 트랜잭션 B:
UPDATE accounts SET balance = 800 WHERE id = 1;
COMMIT;
-- 3. 트랜잭션 A:
SELECT balance FROM accounts WHERE id = 1; -- 결과: 800
예시를 보면, 트랜잭션 A가 동일한 데이터를 읽었지만, 값이 달라진 것을 볼 수 있다.
3. Phantom Read : 한 트랜잭션 내에서 동일한 조건으로 데이터를 두 번 조회했을 때, 결과에 추가되거나 사라진 행(Phantom Record)이 발생하는 경우를 말한다.
해당 문제는 REPEATABLE READ 이하 격리수준에서 발생할 수 있다. 이 문제는 다른 트랜잭션에서 새로운 데이터를 삽입하거나 삭제했기 때문에 발생한다.
EX)
- 트랜잭션 A: 특정 조건에 맞는 데이터를 조회함.
- 트랜잭션 B: 트랜잭션 A 사이에 조건에 맞는 새로운 행을 삽입하고 커밋.
-- 1. 트랜잭션 A:
SELECT * FROM employees WHERE department = 'IT'; -- 결과: 3명 (Alice, Bob, Charlie)
-- 2. 트랜잭션 B:
INSERT INTO employees (name, department) VALUES ('David', 'IT');
COMMIT;
-- 3. 트랜잭션 A:
SELECT * FROM employees WHERE department = 'IT'; -- 결과: 4명 (Alice, Bob, Charlie, David)
예시를 보면, 처음 조회에서는 3명이었지만, 나중에 조회하니 4명이 된 것을 볼 수 있다.
알아보면서, Non-Repeatable Read과 Phantom Read의 차이가 좀 헷갈려서 아래와 같이 구분해보았다.
주요 차이점 (Non-Repeatable Read vs Phantom Read)
구분 | Non-Repeatable Read | Phantom Read |
대상 데이터 | 동일한 Row의 값 | 결과 집합 (다수의 Row) |
원인 | 데이터 수정(Update) 또는 삭제(Delete) | 데이터 삽입(Insert) 또는 삭제(Delete) |
변화 | 특정 Row의 값이 변경됨 | 결과 집합의 Row 개수나 데이터가 변동됨 |
방지 가능 격리수준 | REPEATABLE READ 이상 | SERIALIZABLE |
격리 수준 설정 예제에 대해 알아보겠다.
1. MySQL
-- 현재 격리수준 확인
SELECT @@tx_isolation; -- MySQL 8.0 이상에서는 @@transaction_isolation 사용
-- 격리수준 설정
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 특정 트랜잭션의 격리수준만 변경하려면 START TRANSACTION과 함께 사용:
START TRANSACTION ISOLATION LEVEL READ COMMITTED;
2. Spring(Java)
// Spring의 @Transactional 애너테이션으로 서비스 레벨에서 격리수준을 지정할 수 있다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processTransaction() {
// 비즈니스 로직
}
실제로 많이 사용하는 격리수준과 이유
- READ COMMITTED: 대부분의 애플리케이션에서 사용한다. 성능과 데이터 일관성의 균형을 맞춘다.
- REPEATABLE READ: 은행 계좌와 같은 데이터 정확성이 중요한 경우에 사용된다.
주요 DBMS의 기본 격리수준
- MySQL: REPEATABLE READ
- 이유: InnoDB 스토리지 엔진이 트랜잭션을 처리할 때 MVCC(Multi-Version Concurrency Control)를 사용하여 동시성과 데이터 무결성을 효과적으로 관리하기 때문.
- Non-repeatable Read를 방지하면서도 Phantom Read는 MVCC로 해결.
- PostgreSQL: READ COMMITTED
- 이유: 높은 동시성과 성능을 제공하며, 대부분의 애플리케이션에서 데이터 일관성이 충분히 보장됨.
- Oracle: READ COMMITTED
- 이유: 트랜잭션이 읽기 작업 시 다른 트랜잭션에 영향을 주지 않으며, 성능이 중요시됨.
실무에서 격리수준을 변경해야 할 상황
- Deadlock이 빈번하게 발생하는 경우
- Deadlock은 높은 격리수준(REPEATABLE READ, SERIALIZABLE)에서 자주 발생한다.
- 데이터를 수정하는 트랜잭션 간 교착 상태를 방지하려면 격리수준을 낮춰 동시성을 높이는 것이 필요하다.
- 데이터 일관성보다 성능이 우선인 경우
- 데이터가 즉시 반영되거나 읽기 작업이 많은 환경에서는 READ COMMITTED나 심지어 READ UNCOMMITTED를 사용할 수 있다.
- 예: 로그 처리 시스템, 실시간 분석 서비스.
예상 면접 질문
- 트랜잭션에 대해서 설명해 주세요. 그리고 왜 사용해야 하는지 말씀해 주세요.
-
더보기데이터베이스 트랜잭션이란 데이터베이스에서 한 단위의 작업을 의미합니다. 이 작업은 ACID(원자성, 일관성, 고립성, 지속성) 특성을 따라야 하며, Commit 또는 Rollback으로 완료되어야 합니다. 이를 통해 데이터베이스의 일관성과 무결성을 유지할 수 있습니다.
-
- A 계좌에서 B계좌로 일정 금액을 이체하는 작업에 대해 생각해 봅시다. 이때 트랜잭션은 어떻게 정의할 수 있을까요?
-
더보기상황 설명:
A 계좌에서 100원을 출금하고, B 계좌로 100원을 입금하는 작업을 트랜잭션으로 정의할 수 있습니다.
트랜잭션 단위
1. A 계좌에서 100원 출금(잔액 감소)
2. B 계좌로 100원 입금(잔액 증가)
3. 작업 전체가 성공하면 Commit, 하나라도 실패 시 Rollback
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'B';
COMMIT;
-
- ACID에 대해서 설명해 주세요.
- 트랜잭션 격리 수준(Transaction Isolation Levels)에 대해서 설명해 주세요. 그리고 왜 필요한가요?
- 트랜잭션의 Commit 연산에 대해서 트랜잭션의 상태를 통해 설명해 주세요.
-
더보기Commit이란?
트랜잭션이 성공적으로 완료된 후 변경사항을 데이터베이스에 영구적으로 반영하는 연산입니다.
트랜잭션 상태 변화:
- Active: 트랜잭션이 실행 중인 상태.
- Partially Committed: 모든 연산이 완료되었지만, 아직 Commit되지 않은 상태.
- Committed: Commit 연산이 성공적으로 실행되어 변경사항이 확정된 상태.
-
- 트랜잭션의 Rollback 연산에 대해서 트랜잭션의 상태를 통해 설명해 주세요.
-
더보기Rollback이란?
트랜잭션 실행 도중 오류가 발생하거나 사용자가 취소 명령을 내렸을 때, 데이터베이스를 트랜잭션 시작 전 상태로 되돌리는 연산입니다.
트랜잭션 상태 변화 :
1. Active: 트랜잭션이 실행 중인 상태.
2. Failed : 오류가 발생하거나 작업이 실패한 상태.
3. Aborted: Rollback이 완료되어 데이터베이스가 이전 상태로 복구된 상태.
4. Failed: 오류가 발생하거나 작업이 실패한 상태.
-
- Partial Committed 상태에서 ommitted 상태가 되는 과정에 대해 자세히 설명해 주세요.
-
더보기
- 트랜잭션이 모든 연산을 성공적으로 완료하면 Partially Committed 상태가 된다.
- 데이터베이스는 트랜잭션 로그를 기반으로 변경사항을 영구적으로 저장한다.
- 저장이 완료되면 Committed 상태가 되어 트랜잭션이 성공적으로 종료된다.
-
- 트랜잭션을 사용할 때 주의할 점
-
더보기
- 적절한 격리수준 설정: 높은 격리수준은 성능 저하를 초래할 수 있으므로, 애플리케이션 요구사항에 맞는 수준을 선택해야 한다.
- Deadlock 방지: 여러 트랜잭션이 동일한 자원을 동시에 사용하려고 할 때 교착 상태를 피하도록 쿼리 순서와 Lock 관리를 신중히 해야 한다.
- 성능 고려: 트랜잭션이 너무 오래 지속되면 동시성이 저하될 수 있다. 필요한 최소 범위에서만 트랜잭션을 사용해야 한다.
- 적절한 Rollback 처리: 오류 발생 시 데이터를 일관성 있게 복구하기 위해 롤백을 신속히 처리해야 한다.
-