들어가며: 동시성과 데이터 일관성 사이의 줄다리기

데이터베이스에서 트랜잭션(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 ReadNon-Repeatable ReadPhantom Read
Read Uncommitted발생발생발생
Read Committed방지발생발생
Repeatable Read방지방지발생
Serializable방지방지방지

결론: 어떤 격리 수준을 선택해야 할까?

격리 수준 선택에는 정답이 없으며, 애플리케이션의 요구사항과 데이터의 특성에 따라 결정해야 합니다.

  • 대부분의 일반적인 웹 애플리케이션에서는 Read Committed 수준으로 충분합니다. 합리적인 동시성을 보장하면서 Dirty Read와 같은 심각한 문제를 방지할 수 있습니다.
  • 하나의 트랜잭션 내에서 동일한 데이터를 여러 번 조회하고, 그 결과가 항상 동일해야 하는 비즈니스 로직이 있다면 Repeatable Read 수준을 고려해야 합니다.
  • 데이터의 정합성이 시스템의 그 어떤 가치보다 중요하고, 성능 저하를 감수할 수 있는 경우에만 Serializable 수준을 제한적으로 사용해야 합니다.

필요 이상으로 높은 격리 수준을 설정하는 것은 시스템의 동시성을 불필요하게 저하시켜 전체 성능에 악영향을 줄 수 있습니다. 따라서 각 격리 수준의 트레이드오프를 명확히 이해하고 신중하게 선택하는 것이 중요합니다.