트랜잭션 - 개념 이해
데이터를 저장할 때 단순히 파일에 저장해도 되는데, 데이터베이스에 저장하는 이유는 무엇일까?
여러가지 이유가 있지만, 가장 대표적인 이유는 바로 데이터베이스는 트랜잭션 이라는 개념을 지원하기 때문이다.
트랜잭션은 데이터베이스의 상태를 변화시키기 위한 하나의 작업 단위이며,
단위 작업을 안전하게 처리하도록 보장해주는 역할을 한다.
그런데 하나의 작업을 안전하게 처리하려면 생각보다 고려해야 할 점이 많다.
예를 들어서 A의 5000원을 B에게 계좌이체한다고 생각해보자.
A의 잔고를 5000원 감소하고, B의 잔고를 5000원 증가해야한다.
ex) 5000원 계좌이체
- A의 잔고를 5000원 감소
- B의 잔고를 5000원 증가
계좌이체라는 거래는 이렇게 2가지 작업이 합쳐져서 하나의 작업처럼 동작해야 한다.
만약 1번은 성공했는데 2번에서 시스템에 문제가 발생하면 계좌이체는 실패하고,
A의 잔고만 5000원 감소하는 심각한 문제가 발생한다.
데이터베이스가 제공하는 트랜잭션 기능을 사용하면 1,2 둘다 함께 성공해야 저장하고,
중간에 하나라도 실패하면 거래 전의 상태로 돌아갈 수 있다.
만약 1번은 성공했는데 2번에서 시스템에 문제가 발생하면 계좌이체는 실패하고, 거래 전의 상태로 완전히 돌아갈 수 있다.
결과적으로 A의 잔고가 감소하지 않는다.
모든 작업이 성공해서 데이터베이스에 정상 반영하는 것을 커밋( Commit )이라 하고,
작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백( Rollback )이라 한다.
트랜잭션 ACID
트랜잭션은 ACID라고 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability) 을 보장해야한다.
- 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.
- 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
- 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.
- 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
트랜잭션은 원자성, 일관성, 지속성을 보장한다.
문제는 격리성인데 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야 한다.
이렇게 하면 동시 처리 성능이 매우 나빠진다.
이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
트랜잭션 격리 수준 - Isolation level
격리성(Isolation) 보장없이 트랜잭션을 다루게 되면,
DIRTY READ, NON-REPEATABLE READ, PHANTOM READ 문제가 발생하게 됨
이 문제들을 모두 위배하지 않도록 관리하려면 모든 트랜잭션이 일렬로 처리되어야는데
그렇게하면 트랜잭션 처리 성능이 매우 떨어짐.
이전 트랜잭션이 끝날 때까지 계속 대기하며 병목현상 발생
때문에 사용자가 임의로 특정 문제는 허용하도록 조절해가며 트랜잭션을 관리할 수 있도록
격리 수준(Isolation Level)이라는 것을 정의하게 된 것
그렇게해서 정의된 격리 수준의 종류는 다음과 같다.
SERIALIZABLE (직렬화 가능)
가장 엄격한 트랜잭션 격리 수준으로, 어떠한 부정합 문제도 발생하지 않는다.
발생할 수 있는 문제: 동시 처리 성능 저하
SELECT for update 등의 구문이 아닌 일반적인 SELECT 문은 데이터 조회 시, 락(Lock)을 걸지 않는다.
하지만 SERIALIZABLE 격리 수준에서는 일반 조회 시에도, 데이터에 락을 걸어 어떠한 부정합도 발생하지 않도록 방지한다.
현재 트랜잭션 작업의 범위에 영향을 미칠 수 있는 부분들에 대해 모두 락을 걸어 접근을 막으며,
락이 걸린 부분은 삽입,삭제,수정 등 트랜잭션에 영향이 미칠 수 있는 동작을 제한당한다.
덕분에 어떠한 부정합도 발생하지 않고 데이터를 관리할 수 있지만
데이터 조회 시에도 락을 사용하니, 락을 얻기 위해 대기하는 트랜잭션들에 의해 병목현상이 발생하여 동시 처리 성능이 굉장히 떨어진다.
꼭 필요한 상황이 아니라면 사용하지 않는 편이 성능에 좋다.
REPEATABLE READ (반복 가능한 읽기)
한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다.
발생할 수 있는 문제: PHANTOM READ
발생 시점: 2번 이상의 조회 요청을 보내는 경우 발생
PHANTOM READ 문제 상황
트랜잭션 2가 A데이터를 2개 추가하고
아직 커밋하지 않은 상태라고 가정하자.
이 때 트랜잭션1 이
A 데이터를 조회하면
처음 조회한 데이터의 개수는 0개였다.
이 시점에서 A데이터 2개가 추가된 트랜잭션2 가 Commit 된다면 어떻게 될까?
트랜잭션1 은
첫 번째 조회에서는 A 데이터가 0개였지만,
두 번째 조회에서는 이전과 다르게 A 데이터가 2개라는 응답을 받게 될 것이다.
이렇게 한 트랜잭션에서 2번 이상의 조회 요청을 보내는 경우
각 요청이 서로 다른 데이터를 개수를 가져오는 문제를 PHANTOM READ 라고 한다.
READ COMMITTED (커밋된 읽기)
커밋한 데이터만 읽을 수 있다.
발생할 수 있는 문제: NON-REPEATABLE READ
발생 시점: 2번 이상의 조회 요청을 보내는 경우 발생
NON-REPEATABLE READ 문제 상황
name이 “A”인 데이터가 있다고 가정하자
이때 트랜잭션 1이 name이 “A”인 데이터를 “B”로 변경했고
아직 커밋하지 않은 상태이다.
이 때 트랜잭션2 가 데이터를 2번 동시 조회를 한다.
그리고 아직 트랜잭션1의 변경이 커밋되지 않은 상황이기 때문에
현재 조회한 데이터의 Name은 “A” 이다.
이 시점에서 Name의 “B”로 수정된 트랜잭션1 이 Commit 된다면
어떻게 될까?
트랜잭션2 는 첫 번째 조회에서는 Name이 “A”였지만,
두 번째 조회에서는 Name이 “B”로 조회될 것이다.
이렇게 한 트랜잭션에서 2번 이상의 조회 요청을 보내는 경우
각 요청이 서로 다른 데이터를 조회해오는 문제를 NON-REPEATABLE READ 라고 한다.
READ UNCOMMITED (커밋되지 않은 읽기)
커밋하지 않은 데이터를 읽을 수 있다.
발생할 수 있는 문제: DIRTY READ
발생 시점: 트랜잭션1이 사용하는 데이터를 트랜잭션2에서 변경했다가 rollback되는 경우
DIRTY READ 문제 상황
a=10, b=10 을 가진 데이터가 존재한다고 가정하자.
이때 트랜잭션2에서 b를 70으로 변경했다.
그 다음 트랜잭션1에서
a + b의 값을 a에 저장하는 연산을 수행해서 a=80, b=10 이라는 값이 되었다.
(아직 트랜잭션1은 커밋되지 않았다.)
그런데 트랜잭션2에 문제가 발생해서 b가 다시 10으로 롤백되었다.
그럼 A에 더해진 70이라는 값은 문제가 있는 값이 되며
그 값이 사용되어 변경된 A의 값 또한 문제가 있는 값이 된다.
이렇게 트랜잭션1이 사용하는 데이터를 트랜잭션2에서 변경했다가 rollback되는 경우
데이터의 정합성에 문제가 발생하는 상황을 DIRTY READ 라고 한다.
Reference
'부트캠프 > 우아한테크코스 6기' 카테고리의 다른 글
[Java] Java 반복 작업 수행 시, For-Loop와 Stream 사이 성능 비교 (0) | 2024.04.06 |
---|---|
테스트를 위한 객체, 테스트 더블(Test Double) (0) | 2024.04.04 |
[Java] Fluent API란? (feat. JDBC에 적용해보기) (0) | 2024.04.04 |
[Java] 생성자 체이닝(Constructor Chaining) 기법 (0) | 2024.04.02 |
[Java] 자바에서 라인을 ‘잘’ 개행하는 방법 (2) | 2024.03.01 |