DB 트랜잭션 격리 수준(Isolation Level): 동시성과 데이터 일관성의 트레이드오프
들어가며: 동시성과 데이터 일관성 사이의 줄다리기
데이터베이스에서 트랜잭션(Transaction) 은 ‘모두 실행되거나, 아니면 모두 실행되지 않아야 하는’ 원자적인(atomic) 작업 단위입니다. 실제 서비스에서는 여러 사용자가 동시에 접근하고 여러 트랜잭션이 함께 실행됩니다. 이 동시성(Concurrency) 덕분에 시스템은 더 많은 일을 처리할 수 있습니다.
하지만 여러 트랜잭션이 아무런 제어 없이 같은 데이터에 접근하면 데이터가 오염되거나 일관성이 깨질 수 있습니다. 그렇다고 모든 트랜잭션을 순서대로 하나씩 처리하면 데이터는 안전하겠지만 성능은 크게 떨어집니다.
트랜잭션 격리 수준(Isolation Level) 은 데이터 일관성 과 동시성 사이의 균형을 맞추기 위한 설정입니다. 특정 트랜잭션이 다른 트랜잭션의 변경을 어디까지 볼 수 있는지를 정하고, 그에 따라 성능과 일관성의 트레이드오프가 달라집니다.
먼저 알아야 할 3가지 동시성 문제 현상
격리 수준을 이해하려면, 동시성이 충분히 제어되지 않았을 때 생기는 대표적인 현상 세 가지를 먼저 봐야 합니다.
1. Dirty Read
Dirty Read 는 한 트랜잭션이 아직 커밋(commit)되지 않은 다른 트랜잭션의 수정 내용을 읽는 현상입니다. 만약 데이터를 수정한 트랜잭션이 나중에 롤백(rollback)된다면, 데이터를 읽은 트랜잭션은 결국 존재하지 않는 ‘더러운’ 데이터를 기반으로 동작하게 되어 데이터 불일치를 유발합니다.
| Transaction 1 (사용자 A) | Transaction 2 (사용자 B) |
|---|---|
| 1. 사용자 B의 나이 조회: 20세 | |
| 2. 사용자 B의 나이를 21세로 수정 (아직 커밋 안 함) | |
| 3. 사용자 B의 나이 다시 조회: 21세 (Dirty Read 발생) | |
| 4. 작업을 취소하고 롤백 |
Transaction 1은 최종적으로 롤백될 21세라는 잘못된 데이터를 읽었습니다.
2. Non-Repeatable Read (반복 불가능한 읽기)
Non-Repeatable Read 는 한 트랜잭션 내에서 동일한 쿼리를 두 번 실행했을 때, 그 사이에 다른 트랜잭션이 데이터를 수정 또는 삭제 하고 커밋하여 두 쿼리의 결과가 다르게 나타나는 현상입니다. 한 트랜잭션 내에서 데이터의 일관성이 깨지게 됩니다.
| Transaction 1 (사용자 A) | Transaction 2 (사용자 B) |
|---|---|
| 1. ID가 1인 사용자의 나이 조회: 20세 | |
| 2. ID가 1인 사용자의 나이를 21세로 수정 | |
| 3. 커밋 | |
| 4. 동일한 사용자 나이 다시 조회: 21세 (결과가 다름) | |
| 5. 커밋 |
Transaction 1은 동일한 데이터를 조회했지만, Transaction 2의 개입으로 인해 일관된 결과를 얻지 못했습니다.
3. Phantom Read (유령 읽기)
Phantom Read 는 한 트랜잭션 내에서 특정 범위의 레코드를 두 번 이상 읽었을 때, 첫 번째 쿼리에서는 없었던 새로운 레코드(유령) 가 두 번째 쿼리에서 나타나는 현상입니다. 이는 주로 다른 트랜잭션의 삽입(INSERT) 작업 때문에 발생합니다.
| Transaction 1 (사용자 A) | Transaction 2 (사용자 B) |
|---|---|
| 1. 20세 미만 사용자 수 조회: 5명 | |
| 2. 10세인 신규 사용자 삽입 | |
| 3. 커밋 | |
| 4. 20세 미만 사용자 수 다시 조회: 6명 (유령 레코드 발생) | |
| 5. 커밋 |
Transaction 1은 동일한 범위 조건으로 조회했지만, 중간에 새로운 레코드가 추가되면서 결과 집합이 달라졌습니다.
4가지 트랜잭션 격리 수준
데이터베이스는 이런 문제를 다루기 위해 4가지 격리 수준을 제공합니다. 격리 수준이 높아질수록 데이터 일관성은 강해지지만, 동시성은 낮아집니다.
Level 0: Read Uncommitted
- 설명: 가장 낮은 격리 수준으로, 다른 트랜잭션이 커밋하지 않은 데이터 를 읽는 것을 허용합니다.
- 발생 문제: Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생할 수 있습니다.
- 특징: 동시성은 가장 높지만 데이터 일관성은 거의 보장하지 않으므로, 실제 운영 환경에서는 거의 사용되지 않습니다.
Level 1: Read Committed
- 설명: 커밋된 데이터 만 읽는 것을 허용합니다. 대부분의 상용 데이터베이스(Oracle, SQL Server, PostgreSQL 등)가 기본으로 채택 하는 격리 수준입니다.
- 발생 문제: Non-Repeatable Read, Phantom Read는 여전히 발생할 수 있습니다.
- 특징: Dirty Read를 방지하여 최소한의 데이터 정합성을 보장하며, 합리적인 수준의 동시성을 제공하여 가장 널리 사용됩니다.
Level 2: Repeatable Read
- 설명: 트랜잭션이 시작될 때 읽은 데이터를 트랜잭션이 끝날 때까지 다른 트랜잭션이 수정하거나 삭제하지 못하도록 보장합니다. 이를 통해 한 트랜잭션 내에서 동일한 로우(row)를 여러 번 조회해도 항상 동일한 결과를 얻을 수 있습니다.
- 발생 문제: Phantom Read는 여전히 발생할 수 있습니다. (새로운 로우가 추가되는 것은 막지 못함)
- 특징: 데이터의 일관성을 높여주며, MySQL(InnoDB)의 기본 격리 수준 입니다.
Level 3: Serializable
- 설명: 가장 높은 격리 수준으로, 모든 트랜잭션을 마치 순서대로 하나씩 실행 하는 것처럼 동작하게 만듭니다. 특정 범위의 데이터를 읽을 때, 다른 트랜잭션이 해당 범위에 새로운 레코드를 삽입하는 것조차 막습니다.
- 발생 문제: 모든 동시성 문제를 방지합니다.
- 특징: 가장 강한 데이터 일관성을 보장하지만, 트랜잭션이 동시에 실행되기 어려워 동시성이 크게 낮아집니다. 성능 영향이 크기 때문에, 데이터 정합성이 특히 중요한 일부 처리에서만 신중하게 사용해야 합니다.
요약: 격리 수준과 동시성 문제
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | 발생 | 발생 | 발생 |
| Read Committed | 방지 | 발생 | 발생 |
| Repeatable Read | 방지 | 방지 | 발생 |
| Serializable | 방지 | 방지 | 방지 |
결론: 어떤 격리 수준을 선택해야 할까?
격리 수준 선택에는 정답이 없으며, 애플리케이션의 요구사항과 데이터의 특성에 따라 결정해야 합니다.
- 대부분의 일반적인 웹 애플리케이션에서는 Read Committed 수준으로 충분합니다. 합리적인 동시성을 보장하면서 Dirty Read와 같은 심각한 문제를 방지할 수 있습니다.
- 하나의 트랜잭션 내에서 동일한 데이터를 여러 번 조회하고, 그 결과가 항상 동일해야 하는 비즈니스 로직이 있다면 Repeatable Read 수준을 고려해야 합니다.
- 데이터의 정합성이 시스템의 그 어떤 가치보다 중요하고, 성능 저하를 감수할 수 있는 경우에만 Serializable 수준을 제한적으로 사용해야 합니다.
필요 이상으로 높은 격리 수준을 쓰면 동시성이 불필요하게 낮아지고 전체 성능에도 영향을 줍니다. 각 격리 수준이 막아주는 문제와 그 대가를 함께 보고 선택해야 합니다.
이 글은 AI의 도움을 받아 교정 및 정리되었습니다.