[Database] 트랜잭션(Transaction)에 대하여
- -
데이터베이스를 다루며, 트랜잭션이란 개념에 대해 많이 들어봤지만 깊은 내부의 내용까지는 모두 잘 알지 못하는 것 같아 정리하는 글을 작성한다.
🟢 DB 트랜잭션
트랜잭션이란 DB 의 상태를 변경시키는 작업이다. 즉, 한꺼번에 수행되어야 할 연산을 모아놓은 것이다.
연산들을 모두 처리하지 못 한 경우에는 원 상태로 복구하며, 작업의 일부만 적용되는 현상이 발생하지 않는다. 이를 통해 트랜잭션은 작업은 완정성을 보장해준다. 사용자의 입장에서는 작업의 논리적 단위이고, 시스템의 입장에서는 데이터들을 접근 또는 변경하는 단위가 된다.
▪️ 트랜잭션의 4가지 특징, ACID
1. 원자성(Atomicity)
트랜잭션이 DB 에 모두 반영되거나, 혹은 전혀 반영되지 않아야 한다. (All or Nothing)
2. 일관성(Consistency)
트랜잭션의 작업 처리 결과는 항상 일관성이 있어야 한다. 시스템이 가지고 있는 고정 요소는 트랜잭션 수행 전과 수행 후의 상태가 같아야 한다는 말로, DB 의 제약 조건을 위배하는 작업을 트랜잭션 과정에서 수행할 수 없음을 나타낸다. 쉽게 말해 트랜잭션이 진행되는 동안에 데이터베이스가 변경 되더라도, 업데이트된 데이터베이스로 트랜잭션이 진행되는 것이 아니라, 처음에 트랜잭션을 진행하기 위해 참조한 데이터베이스로 진행된다.
3. 독립성(Isolation)
둘 이상의 트랜잭션이 동시에 병행 실행되고 있을 때, 어떤 트랜잭션도 다른 트랜잭션 연산에 끼어들 수 없다. 하나의 특정 트랜잭션이 완료될 때까지, 다른 트랜잭션이 특정 트랜잭션의 결과를 참조할 수 없다.
4. 지속성(Durability)
트랜잭션이 성공적으로 완료되었으면, 결과는 영구적으로 반영되어야 한다.
▪️ 트랜잭션의 연산
트랜잭션의 개념은 데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 혹은 데이터베이스 시스템에서 복구 및 병행 수행시 처리되는 작업의 논리적 단위이다.
1. COMMIT 연산
트랜잭션이 성공적으로 수행되었음을 선언하는 연산
COMMIT 연산의 실행을 통해 트랜잭션의 수행이 성공적으로 완료되었음을 선언하고, 그 결과를 최종 DB에 반영한다
2. ROLLBACK 연산
트랜잭션 수행이 실패했음을 선언하고 작업을 취소하는 연산
트랜잭션이 수행되는 도중 일부 연산이 처리되지 못한 연산이라면 ROLLBACK 연산을 실행하여 트랜잭션 수행이 실패했음을 선언하고, DB 를 트랜잭션 수행 전과 일관된 상태로 되돌려야 한다.
▪️ 트랜잭션의 상태
Active, 활성
트랜잭션 활동 상태
트랜잭션이 실행 중이며 동작 중인 상태를 말한다.
Partially Committed, 부분 완료
트랜잭션의 COMMIT 명령이 도착한 상태
트랜잭션의 COMMIT 이전 SQL 문이 수행되고, COMMIT 만 남은 상태를 말한다.
트랜잭션의 마지막 연산까지 실행하고 COMMIT 연산을 실행하기 직전의 상태
Failed, 실패
트랜잭션 실패 상태
더 이상 트랜잭션이 정상적으로 진행될 수 없는 상태를 말한다.
Commited, 완료
트랜잭션 완료 상태
트랜잭션이 정상적으로 완료된 상태를 말한다.
Aborted, 철회
트랜잭션 취소 상태
트랜잭션이 취소되고, 트랜잭션 실행 이전 데이터로 돌아간 상태를 말한다.
트랜잭션 수행이 실패하고 ROLLBACK 연산을 실행한 상태
✅ Partially Committed vs Committed
COMMIT 요청이 들어오면, Partially Committed 상태가 된다.
이후 COMMIT 을 문제 없이 수행할 수 있으면 Committed 상태로 전이되고, 오류가 발생한다면 Failed 상태가 된다.
즉, Partially Committed 는 COMMIT 요청이 들어왔을 때를 말하며, Committed 는 COMMIT 을 정상적으로 완료한 상태를 말한다.
⭐️ 트랜잭션 사용 시 주의할 점
트랜잭션은 꼭 필요한 최소한의 코드에만 적용하는 것이 좋다. 즉, 트랜잭션의 범위를 최소하라는 의미이다.
일반적으로 DB 커넥션의 개수가 제한적이다. 그런데 각 단위 프로그램이 커넥션을 소유하는 시간이 길어지면, 사용 가능한 여유 커넥션의 개수는 줄어들게 된다. 그러다 어느 순간에는 각 단위 프로그램에서 커넥션을 가져가기 위해 기다려야 하는 상황이 발생할 수 있기 때문이다.
▪️ 병행제어
병행제어란, 여러 개의 트랜잭션이 실행될 때, 트랜잭션들이 DB의 일관성을 파괴하지 않고 다른 트랜잭션에 영향을 주지 않으면서 트랜잭션을 제어하는 것을 의미한다.
▪️ 병행 실행 시 발생 가능한 문제들
분실된 갱신
두 개의 트랜잭션이 같은 데이터에 대해서 동시에 갱신 작업을 하면, 하나의 갱신 작업이 분실 되는 경우
모순성
한 개의 트랜잭션 작업이 갱신 작업을 하고 있는 상태에서 또 하나의 트랜잭션이 같은 작업 구역에 침범하여 작업하게 되어, DB의 일관성을 해치는 경우
연쇄복귀
같은 자원을 사용하는 두 개의 트랜잭션 중 한 개의 트랜잭션이 성공적으로 일을 수행하였다 하더라도 다른 트랜잭션이 처리하는 과정에서 실패하게 되면, 두 개의 트랜잭션 모두가 복귀되는 현상
비완료 의존성
한 개의 트랜잭션이 수행 과정에서 실패하였을 때, 이 트랜잭션이 회복되기 전에 다른 트랜잭션이 수행 결과를 참조하는 현상
⭐️ 병행제어 기법
(1) 로킹(Locking)
트랜잭션이 어떤 데이터에 접근하고자 할 때 로킹을 수행하며, 한 트랜잭션만이 로킹 해제가 가능하다.
트랜잭션은 로킹이 된 데이터에 대해서만 연산을 수행할 수 있으며, 필드/레코드/파일/DB 모두 로킹의 단위 및 대상이 될 수 있다.
- 로킹 단위가 크면 관리가 용이(로킹 오버헤드 감소)하지만 동시성 수준이 감소한다.
- 로킹 단위가 작으면 동시성 수준이 증가하지만 관리가 어렵다.(로킹 오버헤드 증가)
2단계 로킹 규약(Two-Phase Locking Protocol)
Lock 과 Unlock 이 동시에 이루어지면 일관성이 보장되지 않으므로, Lock 만 가능한 단계와 Unlock만 가능한 단계를 구분하여 직렬 가능성을 보장한다.
하지만 교착상태가 발생할 수도 있다.
- 확장단계 → 트랜잭션이 Lock 기능, Unlock 불가능
- 축소단계 → 트랜잭션이 Unlock 가능, Lock 불가능
- ex) T1: write(A) read(B), T2: read(B) write(A) ⇒ dead lock 발생
로킹의 종류
- S-lock, 공유잠금
- 공유잠금을 설정한 트랜잭션은 데이터 항목에 대해 읽기 연산만 가능
- 하나의 데이터 항목에 대해 여러 개의 공유잠금 가능
- 다른 트랜잭션도 읽기 연산만을 실행
- X-lock, 베타잠금
- 베타잠금을 설정한 트랜잭션은 데이터 항목에 대해 읽기 연산과 쓰기 연산 모두 가능
- 하나의 데이터 항목에 대해서는 하나의 베타잠금만 가능
- 다른 트랜잭션은 읽기/쓰기 연산 모두 불가능
(2) 타임 스탬프 (Time Stamp)
데이터에 접근하는 시간을 미리 정하여 정해진 시간의 순서대로 데이터에 접근하여 수행.
직렬 가능성을 보장하며, 시간을 나눠 사용하기 때문에 교착 상태가 발생하지 않는다.
But, 연쇄 복귀를 초래할 수 있다.
(3) 낙관적 병행제어(Optimistic Concurrency Control)
트랜잭션 수행동안은 어떠한 검사도 하지 않고 트랜잭션 종료 시에 일관적으로 검사하는 기법.
트랜잭션 수행동안 그 트랜잭션을 위해 유지되는 데이터 항목의 지역사본에 대해서만 갱신하며, 트랜잭션 종료시에 동시성을 위한 트랜잭션 직렬화가 검증되며 일시에 DB에 반영한다.
(4) 다중 버전 병행제어(Multi-Version Concurrency Control)
하나의 데이터 아이템에 대해 여러 버전의 값을 유지하며 조회 성능을 최대한 유지하기 위한 기법.
트랜잭션 간의 충돌 문제는 대기가 아니라 복귀 처리함으로써 연쇄복귀초래 발생 가능성 존재.
⭐️ 교착상태 (DeadLock)
교착상태란, 두 개 이상의 트랜잭션이 특장 자원(테이블 또는 행) 의 잠금(Lock) 을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구해 아무리 기다려도 상황이 바뀌지 않는 상태를 의미한다.
교착상태의 빈도를 낮추는 방법
- 트랜잭션을 자주 COMMIT 한다.
- 정해진 순서로 테이블을 접근한다. 즉, 트랜잭션들이 동일한 테이블 순으로 접근하게 한다.
- 읽기 잠금 획득(SELECT ~ FOR UPDATE) 의 사용을 피한다.
- 한 테이블의 복수 행을 복수의 연결에서 순서없이 갱신하면 교착상태가 발생하기 쉬우므로, 테이블 단위의 잠금을 획득해 갱신을 직렬화한다. → 동시성 수준을 떨어지지만 교착 상태를 회피할 수 있다.
⭐️ 트랜잭션 격리수준(Isolation Level)
ACID 중 Isolation(독립성, 고립성) 을 구현하는 개념으로, 동시에 여러 트랜잭션이 처리될 때 트랜잭션끼리 얼마나 서로 고립되어 있는 지를 나타내는 것이다.
즉, 트랜잭션이 다른 트랜잭션에 변경한 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것이다.
격리 수준은 크게 아래의 4가지로 구분된다.
- READ UNCOMMITTED
- READ COMMITED
- REPEATABLE READ
- SERIALIZABLE
아래로 내려갈수록 트랜잭션 간 고립 정도가 높아지며 성능이 떨어지는 것이 일반적.
일반적으로 온라인 서비스에서는 READ COMMITED 나 REPEATABLE READ 중 하나를 사용한다.
▪️ 트랜잭션 격리수준의 필요성
DB 는 ACID 특징과 같이 트랜잭션이 독립적인 수행을 하도록 한다. 따라서 Locking 을 통해 트랜잭션이 DB 를 다루는 동안 다른 트랜잭션이 관여하지 못하도록 막는 것이 필요하다. 하지만 무조건 Locking 으로 동시에 수행되는 수많은 트랜잭션들을 순서대로 처리하는 방식으로 구현하게 되면, DB의 성능은 떨어지게 된다. 그렇다고 성능을 높이기 위해 Locking 의 범위를 줄인다면 잘못된 값이 처리될 문제가 발생하게 되므로, 최대한 효율적인 Locking 방법이 필요하다.
🔻 READ UNCOMMITTED (레벨 0), 커밋되지 않는 읽기
- 각 트랜잭션에서의 변경 내용을 COMMIT 이나 ROLLBACK 여부에 상관없이 다른 트랜잭션에서 값을 읽을 수 있다.
- 정합성에 문제가 많은 격리 수준이기 대문에 사용하지 않는 것을 권장.
- DIRTY READ 현상 발생
- DIRTY READ 란, 트랜잭션 작업이 완료되지 않았음에도 다른 트랜잭션이 볼 수 있게 된다.
DIRTY READ 예시
ex) 아래 그림과 같이 COMMIT 되지 않은 상태지만 Update 된 값을 다른 트랜잭션에서 읽기 가능
🔻 READ COMMITTED (레벨 1), 커밋된 읽기
- RDB 에서 대부분 기본적으로 사용되고 있는 격리 수준이며, 커밋된 데이터만 조회할 수 있다.
- 실제 테이블 값을 가져오는 것이 아니라, Undo 영역에 백업된 레코드에서 값을 가져옴.
- DIRTY READ 와 같은 현상은 발생하지 않지만 Non-repeatable Read(반복 읽기 불가능) 현상이 발생
- Non-repeatable Read 란, 한 트랜잭션에서 같은 쿼리를 두번 수행할 때, 그 사이에 다른 트랜잭션 값을 수정/삭제하면서 두 쿼리의 결과가 상이하게 나타나는, 일관성이 깨진 현상
- 즉, 하나의 트랜잭션 내에서 동일한 SELECT 쿼리를 실행했을 때 항상 같은 결과를 보장해야 한다는 REPEATABLE READ 정합성에 어긋난다.
Non-repeatable Read 예시
T2 에서 222 를 조회하는데 첫번째에는 Undo 영역에 백업된 레코드에서 값을 가져와 BUSAN 이 조회되지만 T1 이 UPDATE 및 COMMIT 한 후, JEJU 가 조회된다.
🔻 REPEATABLE READ (레벨 2), 반복가능한 읽기
- 자신의 트랜잭션이 생성되기 이전의 트랜잭션에서 COMMIT 이 된 데이터만 읽는다.
- Undo 공간에 백업해두고 실제 레코드 값을 변경.
- MySQL 과 MariaDB 가 기본으로 사용하는 격리 수준
- MySQL 에서는 트랜잭션 마다 트랜잭션ID 를 부여해 트랜잭션 ID보다 작은 번호에서 변경한 것만 읽음.
- 백업된 데이터는 불필요하다고 판단하는 시점에 주기적으로 삭제
- Undo 에 백업된 레코드가 많아지면 MySQL 서버의 처리 성능이 떨어질 수 있음
- 이러한 변경 방식을 MVCC(Multi Version Concurrency Control) 라고 부름
- PHANTOM READ 현상 발생
- PHANTOM READ 란, 다른 트랜잭션에 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상.
- PHANTOM READ 에 의하여 원래 출력되지 않아야 하는데 Update 영향을 받은 후부터 출력
- 방지 방법에는 쓰기(Write) 잠금을 걸어야 한다.
🔻 SERIALIZABLE (레벨 3), 직렬화 기능
- 가장 단순한 격리 수준이면서 가장 엄격한 격리 수준
- 완벽한 읽기 일관성 모드 제공
- 다른 사용자는 트랜잭션 영역에 해당되는 데이터에 대한 수정 및 입력 불가능
- 성능 측면에서는, 동시 처리 성능 가장 낮음
- PHANTOM READ 가 발생하지 않으며 DB 에서 거의 사용하지 않음
[정리] 낮은 격리수준을 활용했을 때 발생하는 현상들
이 현상들은 트랜잭션의 Isolation(고립성)과 데이터 무결성의 지표로 사용된다.
- Dirty Read
생성, 갱신 또는 삭제 중에 커밋되지 않은 데이터 조회를 허용함으로써, 트랜잭션이 종료되면 더이상 존재하지 않거나, 롤백되었거나, 저장 위치가 바뀌었을 수도 있는 데이터를 읽어들이는 현상 - Non-repeatable Read
한 트랜잭션 내에서 같은 행이 두 번 이상 조회됐는데 그 값이 다른 경우
ex) A와 B가 마지막 남은 영화표를 예매하는데, A가 고민하는 중에 B가 표를 구매하여 A는 상반된 정보를 받게 되는 경우 - Phantom Read
한 트랜잭션 안에서 일정 범위의 레코드를 두 번 이상 읽었을 때, 첫 번째 쿼리에서 없던 레코드가 두 번째 쿼리에서 나타나는 현상
'Database' 카테고리의 다른 글
[Database] 데이터베이스 정규화에 대하여 (2) | 2024.05.15 |
---|---|
[MySQL] WITH RECURSIVE (1) | 2023.12.07 |
[Database] Redis란 ? (2) | 2023.12.06 |
[Database] MySQL 와 Maria DB 는 무엇이 다른 걸까 (0) | 2023.07.27 |
[Database] DB, DBMS 와 RDBMS의 차이 (0) | 2023.07.26 |
소중한 공감 감사합니다