1. 트랜잭션(transaction)이란?
- trans와 action이 합쳐진 단어로, trans는 변화나 이전을 의미하고, action은 행동이나 조치를 통해 뭔가 진행이 되는 걸 의미한다.
- 데이터베이스에서 트랜잭션이란, 하나의 논리적 작업 단위를 구성하는 일련의 연산들의 집합을 트랜잭션이라고 한다. 주로 다양한 데이터 항목들을 엑세스하고 갱신하는 프로그램 수행 단위를 의미한다. 트랜잭션이 성공하면 전체 작업이 정상적으로 완료되어야 하고, 실패하면 아무 것도 실행되지 않고 처음 상태로 돌아가야 하는 등 흔히 ACID 라고 일컫는 특징이 있다.
ACID란?
- ACID(원자성, 일관성, 독립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다. 즉 데이터베이스의 동시성 제어를 보장하고, 다중 사용자 환경에서도 데이터 무결성을 유지하게 하는 성질을 말한다.
1. 원자성(Atomicity)
- 트랜잭션이 DB에 모두 반영되거나, 아니면 전혀 반영되지 않아야 한다. 트랜잭션의 중간 상태를 데이터베이스에 반영해서는 안 된다. 쉽게 ‘all or nothing’ 으로 설명된다.
2. 일관성(Consistency)
- 트랜잭션의 작업 처리 결과는 항상 일관성 있어야 한다. 즉 성공적으로 수행된 트랜잭션은 정당한 데이터들만을 데이터베이스에 반영해야 한다. 트랜잭션 수행을 데이터베이스 상태 간의 전이(transition)으로 봤을 때, 수행 전후의 데이터베이스 상태는 각각 일관성이 보장되는 서로 다른 상태가 된다. 트랜잭션 수행 후 보존해야 할 일관성은 기본 키, 외래 키 제약과 같은 명시적인 무결성 제약 조건들뿐만 아니라 계좌이체를 예로 들면 자금 이체 전후 두 계좌의 잔고의 합이 같아야 한다는 비명시적인 일관성 조건들도 있다.
3. 독립성(Isolation)
- 둘 이상의 트랜잭션이 동시에 병행 실행되고 있을 때, 어떤 트랜잭션도 다른 트랜잭션 연산에 끼어들지 못하도록 보장하는 것을 의미한다. 즉 한 트랜잭션의 중간 결과를 다른 트랜잭션은 몰라야 한다. 이런 독립성이 보장되지 않으면 트랜잭션이 원래 상태로 되돌아갈 수 없게 된다. isolation 성질을 보장하는 가장 쉬운 방법은 트랜잭션을 순차적으로 수행하는 것이다. 하지만 병렬 수행의 장점을 얻기 위해 DBMS는 병렬적으로 수행하면서도 일렬(serial) 수행과 같은 결과를 보장할 수 있는 방식을 제공하고 있다.
4. 지속성(Durability)
- 트랜잭션이 성공적으로 완료되었으면, 결과는 영구적으로 반영되어야 한다. 향후 어떤 소프트웨어나 하드웨어 장애가 발생하더라도 보존되어야 한다.
💡 ACID 원칙을 지키는 건 어렵다
위에서도 잠깐 언급했지만 트랜잭션의 ACID 원칙을 지키는 가장 쉬운 방법은 순차적으로 수행하는 것이다. 하지만 동시성이 매우 떨어지기 때문에 실질적으로 지키기 어려운 원칙이다. 그렇기 때문에 DB 엔진은 ACID 원칙을 희생하여 동시성을 얻을 수 있는 방법을 제공한다. 바로 transaction의 isolation level이다.
Isolation 원칙을 덜 지키는 level을 사용할수록 문제가 발생할 가능성은 커지지만 동시에 더 높은 동시성을 얻을 수 있다. SQL 표준은 transaction isolation에 대해 4가지 레벨로 정의하고 있다. 트랜잭션끼리 얼마나 서로 고립되어 있는지를 나타내는 수준을 말한다. 또 다른 의미로는 다른 트랜잭션이 변경한 데이터에 대한 접근 강도를 의미한다. 레벨이 높아질수록 고립정도가 높아지며 성능저하도 심해진다.
2. Transaction의 Isolation Level(격리 수준)
격리 수준을 알아보기 전에 Isolation이 제대로 동작하지 않을 때 발생할 수 있는 대표적인 이상 현상들에 대해 먼저 짚고 넘어가자.
1. Read Phenomena (읽기 이상 현상)
1. dirty read
- Transaction 1이 x와 y의 값을 읽는 중간에 Transaction 2가 y 값을 70으로 변경한 상황이다. 이 때 Transaction 1은 Transaction 2가 커밋하진 않았지만 변경해버린 y 값을 읽고 그 값을 연산에 이용한다. 결국 Transaction 2가 어떤 문제로 인해 abort가 발생 해 롤백이 일어나도 Transaction 1의 수행 결과 데이터는 바뀌지 않는다. 결국 Transaction 1, Transaction 2가 각기 직렬적으로 수행되는 스케줄과 비교하면 최종적으로 틀린 결과를 가지게 되는 상황이다. 아직 커밋(Commit)되지 않은, 즉 확정되지 않은(신뢰할 수 없는) 다른 트랜잭션의 데이터를 읽고 데이터에 이상이 발생한 상황을 Dirty Read 라고 한다. 예시처럼 데이터가 언제든지 다시 변경되거나 롤백 될 수 있다는 위험성을 내포한다.
- 번외로 ‘Dirty’는 영어에서 본래 의미인 ‘더럽다’ 또는 ‘깨끗하지 않다’라는 뜻이지만, 여기서는 ‘신뢰할 수 없는’ 혹은 ‘변질되었을 가능성이 있는’이라는 의미로 사용되었다. 데이터베이스의 트랜잭션 관점에서 ‘dirty’한 데이터는 아직 확정되지 않은(커밋되지 않은) 상태의 데이터를 가리킨다. 기본적으로 트랜잭션이 커밋되지 않으면 다른 트랜잭션이 이 데이터를 참조하게 해선 안 되는 게 원칙이지만, 낮은 격리 수준에서 이를 허용하여 문제가 발생할 수 있는데 이 때 ‘더럽고 신뢰할 수 없는’ 데이터로 간주되어 ‘dirty’라는 용어가 사용되었다고 볼 수 있다.
2. nonrepeatable read
- 한 트랜잭션 내에서 해당 트랜잭션이 변경하지 않은 값을 다시 조회했음에도 불구하고 다른 트랜잭션이 커밋한 변경된 데이터를 받게 되는 상황이다. 트랜잭션이 독립적이어야 한다는 원칙이 위배되는 상황이다.
- 즉, 다른 트랜잭션이 커밋한 데이터를 읽을 수 있는 것을 의미한다. 보통 데이터의 수정이나 삭제가 발생했을 때 일어난다. 처음 어떤 자료를 읽고, 다시 읽으려고 하는데, 그 사이에 다른 트랜잭션이 자료를 변경하고 커밋했다면, 다음 읽는 값이 커밋된 값으로 읽을 수 있다.
- nonrepeatable이라는 의미는 비반복성, 반복할 수 없는, 반복 불가능한이라는 단어이다. 결국 데이터가 반복되게 조회되지 않고 달라질 수 있는 이상 현상이다. 참고로 Fuzzy라는 단어는 흐릿한, 어렴풋한이라는 의미의 형용사입니다.
3. phantom read
- 한 트랜잭션에서 같은 쿼리를 2번 이상 조회 했을 때 없던 결과가 조회되는 상황을 말한다. 보통 데이터의 삽입이 발생했을 때 일어난다. 분명 직전에 읽었을 때 없었던 데이터인데 뜬금없이 나타났다. 유령같은 현상이다. 한국식으로 말하자면 귀신이 곡 할 노릇이다.
4. serialization anomaly
- 일련의 트랜잭션 단위를 수행한 결과가 순서대로 수행했을 시 확인되는 트랜잭션들의 결과와 상이한 현상이다.
💡 재차 언급하지만 위의 이상 현상들이 모두 발생하지 않도록 할 수 있지만, 그럴수록 제약사항이 많아지고 동시성이 떨어져 결국 전체 처리량이 하락하게 된다. 따라서 일부 이상 현상은 허용하지만 단계별로 사용자가 필요에 의해 적절히 격리 수준(Isolation Level)을 선택할 수 있도록 하는 게 더 효과적이다.
2. Isolation Level (격리 수준)
먼저 1992년 11월 발표된 ISO/IEC 9075:1992 SQL 표준에 정의된 내용 기준을 먼저 살펴보자. 격리 수준에 대한 간략한 개념을 먼저 말하자면, 앞서 설명한 이상 현상을 얼마만큼 허용하는지에 따라 구분된 것이라고 볼 수 있다.
1. Read uncommitted
- 커밋하지 않은 데이터를 읽을 수 있어서 dirty read를 포함해서 4개 이상 현상이 모두 발생할 수 있다.
2. Read committed
- 커밋한 데이터만 읽을 수 있기 때문에 dirty read 문제는 없지만, 같은 트랜잭션 내에서 select 쿼리의 결과가 달라질 수 있는 nonrepeatable read와 phantom read 문제가 발생할 수 있다.
3. Repeatable read
- 트랜잭션 시작 시간을 기준으로 그 전에 commit 된 데이터를 읽는다. 따라서 조회를 했을 때 항상 같은 값을 조회해오는 것을 보장하는 격리 수준이다. dirty read와 nonrepeatable read 문제가 발생하지 않는다.
4. Serializable
- 가장 높은 격리 수준으로 트랜잭션이 동시에 수행되지 않고, 하나씩 순서대로 수행되는 것처럼 작동한다. 읽기 작업에도 lock을 설정하게 되고, 동시에 다른 트랜잭션에서 해당 record를 변경하지 못하게 된다.
- SELECT 쿼리에서 LOCK을 걸기 때문에 PHANTOM READ가 발생하지 않지만 성능 문제가 있다.
- 이상 현상이 발생하지 않는다.
SQL 표준과 PostgreSQL에서 구현한 트랜잭션 isolation level을 나타낸 표
Isolation level | Dirty Read | Nonrepeatable Read | Phantom Read | Serialization Anomaly |
Read uncommitted | 허용, PG에서는 없음 | 허용 | 허용 | 허용 |
Read committed | 해결 | 허용 | 허용 | 허용 |
Repeatable read | 해결 | 해결 | 허용, PG에서는 없음 | 허용 |
Serializable | 해결 | 해결 | 해결 | 해결 |
- 위 표에서 ‘해결’이라는 표현은 해당 격리 수준에서는 이상 현상이 생기지 않는다는 의미로 작성한 것이다.
- 표준 트랜잭션에는 4가지 레벨이 있지만 PostgreSQL에서는 Read uncommitted를 제외한 3가지만 지원한다. Read uncommitted 설정은 Read committed 설정으로 간주한다. ANSI 문법 호환성때문에 Read Uncommitted 문법을 허용할 뿐 내부적으로 Read committed 격리 수준으로 작동한다.
- Repeatable read가 Snapshot Isolation 처럼 동작한다.
- 이 외에도 주요 RDBMS은 SQL 표준에 기반해서 isolation level을 정의하고 있다. 그리고 RDBMS마다 제공하는 isolation level이 다르다. 같은 이름이라도 동작 방식이 다를 수 있기 때문에 개별적으로 확인이 필요하다.
- MySQL : https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html
- Oracle : https://docs.oracle.com/database/121/ADFNS/adfns_sqlproc.htm#ADFNS99999
- SQL Server : https://learn.microsoft.com/ko-kr/sql/t-sql/statements/set-transaction-isolation-level-transact-sql?view=sql-server-ver16
- 결국 사용하는 RDBMS의 Isolation level을 잘 파악해서 적절한 Isolation level을 사용할 수 있도록 해야 한다. 그리고 애플리케이션 설계자는 Isolation level을 통해 전체 처리량(throughput)과 데이터 일관성 사이에서 어느 정도 거래(trade)를 할 수 있다는 점을 염두해서 적절한 설계를 할 수 있어야 하겠다.
고립정도와 성능 차이
ANSI/ISO standard SQL 92에서 정의한 isolation level에 대한 비판
- 3가지 이상 현상의 정의가 모호하다.
- 이상 현상은 3가지 외에도 더 있다.
- Dirty write - 커밋 안 된 데이터를 write 함
- Lost update - 업데이트를 덮어 씀
- Read skew - 데이터가 불일치하는 데이터 읽기
- Write skew - 데이터가 불일치하는 데이터 쓰기
- 확장된 Dirty read - abort(롤백)이 발생하지 않더라도 dirty read가 될 수 있다
- 확장된 Phantom read - 같은 데이터를 읽지 않더라도 서로 연관된 데이터를 읽는 경우에도 없던 결과가 조회될 수 있다
- 상업적인 DBMS에서 사용하는 방법을 반영해서 isolation level을 구분하지 않았다.
- Snapshot Isolation(MVCC의 한 종류)- 트랜잭션 시작 전에 커밋된 데이터만 보인다. First-committer-winner 방식이고, write-write conflict가 발생했을 때 먼저 commit 한 트랜잭션에서 작업한 내용만 인정하고 다른 트랜잭션은 abort 되어 스냅샷이 폐기된다.(애초에 DB에 반영된 적이 없기 때문에 롤백하지 않는다)
여기서 잠깐, MVCC(Multi Version Concurrency Control)란?
최근까지 DB들은 대부분 ACID를 보장하기 위해 락에 의존했다. 하지만 이는 언제나 락이 필요하기 때문에 많은 수의 락을 관리하게 되면 동시작업 수행이 어렵고 성능저하를 초래하게 된다. 그래서 락의 대안으로, 수정되는 모든 데이터를 별도 스냅샷(복사본)으로 관리하는 MVCC가 있다.
여러 버전의 레코드를 저장해 과거의 특정 타임스탬프의 데이터베이스의 일관성을 보장하는 방식이다.
postgresql 역시 내부적으로 MVCC를 사용하여 데이터 일관성을 유지하고 다중 사용자 환경에서 합리적인 성능을 발휘할 수 있도록 잠금 경합을 최소화한다. 락 대신 MVCC 모델의 동시성 제어를 사용하면 얻을 수 있는 가장 큰 장점이 바로 읽기가 쓰기를 차단하지 않고 쓰기가 읽기를 차단하지 않는다는 점이다.
3. JPA 트랜잭션 격리 수준
Spring 트랜잭션에서 기본적인 격리 수준은 'DEFAULT' 이다. 즉, 현재 사용하고 있는 RDBMS가 채택하는 기본 트랜잭션 격리 수준을 따라간다는 뜻이다. 참고로 Postgres는 Read Committed 격리 수준을 기본으로 채택하고 MySQL은 Repeatable read를 기본으로 채택하고 있다. set transaction 명령으로 사용자가 임의로 변경하지 않는 이상 기본 격리 수준을 따른다.
- PSQL
// 격리 수준 조회하기 - 기본값 read committed
postgres=# show transaction_isolation;
transaction_isolation
-----------------------
read committed
(1 row)
// 격리 수준 변경하기(질의)
// 현재 세션에서만 serializable 임시로 변경(재접속 시 기본 격리 수준으로 변경됨)
postgres=# set session characteristics as transaction isolation level serializable;
SET
postgres=# show transaction_isolation;
transaction_isolation
-----------------------
serializable
(1 row)
// 설정으로 변경하기
#default_transaction_isolation = 'read committed'
// DB에 명령어로 격리 수준 변경하기
ALTER DATABASE DBNAME SET DEDFAULT_TRANSACTION_ISOLATION TO 'REPEATABLE READ';
- 스프링 코드 예시
package org.springframework.transaction.annotation;
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void issuedSignUpCoupon(Long memberId) {
~~~
}
@Transactional(isolation = Isolation.DEFAULT)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional(isolation = Isolation.READ_COMMITTED)
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Transactional(isolation = Isolation.SERIALIZABLE)
'개발 > DevOps' 카테고리의 다른 글
대용량 테이블을 위한 DB 파티셔닝 (1) | 2024.12.08 |
---|---|
Docker 빌드에서 운영까지 (using docker compose) (0) | 2022.12.30 |
Docker 컨테이너간 통신 (0) | 2022.12.29 |
Docker Container Storage (0) | 2022.12.29 |
Docker 컨테이너 리소스 관리 (0) | 2022.12.29 |