좀 더 들여다 보는 데이터베이스

profile
FE Developer - 죠지
December 02, 2023
GPT 요약
Thumbnail

안녕하세요. 죠지입니다.
지난 시간 "너도 알아야 한다, SQL과 데이터베이스" 라는 포스팅으로 찾아뵈었던 것에 이어서 데이터베이스에 대해 좀 더 자세히 알아보겠습니다.

데이터베이스의 내용이 너무 방대하다보니, 어떤 내용을 다루어야 할지 고민이 많았습니다. 너무 기초적인 내용은 지루하고, 너무 심화적인 내용은 이해하기 어려울 것 같았습니다. 그래서 적당한 주제로 "트랜잭션 관리와 격리 수준" 을 선택하였습니다.

Thumbnail

아래는 이번 포스팅에서 다룰 내용입니다.

  • 트랜잭션의 정의와 중요성: 데이터베이스에서 트랜잭션이란 무엇인지, 왜 중요한지에 대한 기본적인 설명.

  • ACID 속성: 트랜잭션의 네 가지 기본 속성인 Atomicity(원자성), Consistency(일관성), Isolation(고립성), Durability(영속성)에 대해 설명.

  • 격리 수준(Isolation Levels): 다양한 격리 수준에 대해 설명.

  • 트랜잭션 격리와 성능: 더 높은 격리 수준이 보장하는 데이터 일관성, 그에 따른 성능에 대한 영향(격리 수준이 높아질수록 동시성이 줄어들고, 이는 시스템의 처리량에 영향을 줄 수 있음).

  • 데드락과 트랜잭션 관리: 격리 수준과 트랜잭션 관리에서 데드락이 발생할 수 있는 상황을 설명.

  • 최신 동향 및 기술: 클라우드 데이터베이스, 분산 데이터베이스 시스템에서의 트랜잭션 관리와 격리 수준에 대한 논문 공유.

트랜잭션의 정의와 중요성

트랜잭션의 정의

데이터베이스에서의 트랜잭션은 데이터에 대한 하나 이상의 작업을 하나의 단위로 묶는 것을 의미합니다.

트랜잭션의 중요성

트랜잭션은 데이터베이스 시스템에서 데이터의 일관성무결성 을 유지하는 데 도움이 됩니다.

  • 일관성: 일관성은 트랜잭션이 데이터베이스의 모든 규칙을 준수하며 실행되어야 한다는 것을 의미합니다. 예를 들어, 은행 시스템에서 두 계정 간의 자금 이체는 두 계정의 총 잔액이 변하지 않아야 합니다.

  • 무결성: 무결성은 데이터가 정확하고 신뢰할 수 있어야 한다는 원칙을 말합니다. 트랜잭션을 사용하면 시스템 오류, 전원 손실 또는 기타 문제가 발생할 경우에도 데이터 무결성이 유지됩니다.

ACID 속성

ACID

트랜잭션은 ACID 속성을 준수해야 합니다.

원자성(Atomicity)

트랜잭션은 원자적이어야 합니다.

이는 트랜잭션의 모든 작업이 성공적으로 완료되거나 실패해야 한다는 것을 의미합니다. 트랜잭션의 모든 작업이 성공적으로 완료되지 않으면 트랜잭션은 롤백되어야 합니다. 예를 들어, 은행 시스템에서 두 계정 간의 자금 이체는 성공적으로 완료되거나 실패해야 합니다. 하지만 중간에 오류가 발생하여 자금 이체 프로세스가 중단되면, 트랜잭션은 롤백되어야 합니다.

일관성(Consistency)

트랜잭션은 일관성이 있어야 합니다.

이는 트랜잭션이 데이터베이스의 일관성을 유지해야 한다는 것을 의미합니다. 예를 들어, 은행 시스템에서 두 계정 간의 자금 이체는 두 계정의 총 잔액이 변하지 않아야 합니다. 좀 더 쉽게 말하면 계좌 A와 계좌 B의 잔액의 합은 자금 이체 전과 이후에도 동일해야 한다는 것입니다. 물론 계좌 A와 계좌 B의 잔액은 자금 이체 후에는 달라질 수 있습니다.

고립성(Isolation)

트랜잭션은 격리되어야 합니다.

이는 트랜잭션의 작업이 다른 트랜잭션의 작업에 영향을 주지 않아야 한다는 것을 의미합니다. 예를 들어, 은행 시스템에서 두 계정 간의 자금 이체는 다른 트랜잭션의 작업에 영향을 주지 않아야 합니다. 다른 트랜잭션에서 계좌 A의 잔액을 변경하더라도, 이러한 연산 작업에 다른 트랜잭션이 영향을 줄 수 없습니다.

지속성(Durability)

트랜잭션은 지속성이 있어야 합니다.

이는 트랜잭션이 성공적으로 완료되면 해당 트랜잭션의 결과가 영구적으로 저장되어야 한다는 것을 의미합니다. 예를 들어, 은행 시스템에서 두 계정 간의 자금 이체는 성공적으로 완료되면 해당 이체가 영구적으로 저장되어야 합니다. 단순히 생각해보면 계좌 A의 모든 잔고 1000만원을 계좌 B(잔액:1000만원)로 이체하는 트랜잭션이 성공적으로 완료되면, 계좌 A의 잔고는 0원이 되어야 하고 계좌 B의 잔고는 2000만원이 되어야 합니다.

트랜잭션 격리 수준

여기서부터는 백엔드 개발자라도 짚고 넘어가야 할 중요한 내용입니다.

트랜잭션 격리 수준(Transaction Isolation Levels)은 데이터베이스 관리 시스템(DBMS)에서 트랜잭션이 서로 간섭하지 않도록 제어하는 방법을 정의하고 있습니다. 격리 수준이 높을수록 트랜잭션 간의 간섭 가능성은 줄어들지만, 성능은 떨어질 수 있습니다. 반대로 격리 수준이 낮을수록 동시성은 증가하지만, 일관성 문제가 발생할 수 있습니다. 이러한 트레이드 오프(Trade-off)는 데이터베이스 관리 시스템(DBMS)마다 다르기 때문에 격리 수준을 설정할 때는 데이터베이스 관리 시스템(DBMS)의 특성을 고려해야 합니다.

격리 수준의 문제점

시작에 앞서 다양한 격리 수준은 다음과 같은 문제를 해결하거나 발생시킬 수 있다는 것을 인지해야 합니다.

  • 더티 리드 (Dirty Read): 한 트랜잭션이 아직 커밋되지 않은 다른 트랜잭션의 데이터를 읽는 것.
  • 반복 불가능한 읽기 (Non-Repeatable Read): 한 트랜잭션 내에서 같은 데이터를 두 번 읽을 때, 두 번째 읽기에서 다른 값이 반환되는 것.
  • 팬텀 리드 (Phantom Read): 한 트랜잭션 내에서 일정 범위의 데이터를 두 번 읽을 때, 두 번째 읽기에서 새로운 행(row)이 나타나는 것.

격리 수준의 종류

Read Uncommitted (읽기 미확정)

가장 낮은 격리 수준으로, 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있습니다. 더티 리드가 발생할 수 있습니다.

Read Uncommitted

그림은 tran_2가 커밋되지 않은 데이터를 읽는 것을 보여줍니다. tran_1이 table의 일부분을 수정했지만 낮은 격리 수준 때문에 읽을 수 있습니다. 이는 tran_1이 커밋되지 않고 롤백되면 tran_2가 읽은 데이터는 더 이상 유효하지 않다는 것을 의미합니다. 유효하지 않은 데이터라는 말은 즉 tran_2가 읽은 데이터가 실제로는 존재하지 않는 데이터라는 것이고, 금융 시스템에서는 심각한 문제가 발생할 수 있습니다.

예를 들어, tran_1이 계좌 A(잔고 2000만원)에서 1000만원을 인출하는 트랜잭션을 수행하고 있습니다. tran_1이 커밋되지 않았지만 tran_2가 계좌 A의 잔액을 조회하는 트랜잭션을 수행합니다. tran_2는 계좌 A의 잔액이 1000만원이라는 결과를 얻습니다.

만약 잔고에 따라서 특정한 계약을 체결하는 시스템이라면 tran_2는 잔고가 1000만원이라는 잘못된 정보를 바탕으로 계약을 체결할 수 있습니다. 대출 시스템에서도 마찬가지로 tran_2는 잔고가 1000만원이라는 잘못된 정보를 바탕으로 대출을 승인할 수 있습니다.

Read Committed (읽기 확정)

대부분의 DBMS에서 기본적으로 사용하는 격리 수준입니다. 커밋된 데이터만 읽을 수 있어 더티 리드는 방지하지만, 반복 불가능한 읽기는 여전히 발생할 수 있습니다.

Read Committed

그림은 tran_2가 커밋된 데이터만 읽는 것을 보여줍니다. tran_1이 table의 일부분을 수정했지만 tran_1이 커밋되지 않았기 때문에 tran_2는 수정된 데이터를 읽을 수 없습니다. 하지만 tran_2가 같은 데이터를 다시 읽을 때, tran_1이 커밋되어 수정된 데이터를 읽을 수 있습니다.

이는 tran_2가 같은 데이터를 두 번 읽을 때, 두 번째 읽기에서 다른 값이 반환되는 논리판독이 발생할 수 있다는 것을 의미합니다. 다시 생각해보면 tran_2가 읽은 데이터는 tran_1이 커밋되지 않았기 때문에 유효하지 않습니다. 커밋이 완료된 시점의 데이터가 유효한 데이터이기 때문에 tran_2가 다시 같은 데이터를 읽을 때, 값이 달라 시스템에 문제가 발생할 수 있습니다.

Repeatable Read (반복 가능 읽기)

한 트랜잭션 내에서 같은 데이터를 여러 번 읽을 경우 일관된 결과를 반환합니다. 반복 불가능한 읽기는 방지하지만 팬텀 리드는 발생할 수 있습니다.

여기서는 MVCC(Multi-Version Concurrency Control) - 다중버전 동시성 제어라는 개념이 등장합니다. MVCC는 한 트랜잭션이 데이터를 읽을 때, 해당 데이터의 버전을 읽어옵니다.

그리고 간략히 정리한 아래의 개념을 숙지하면 MVCC를 이해하는데 도움이 됩니다.

리두 로그 (Redo Log)

리두 로그는 데이터베이스가 수행한 모든 변경 사항을 기록하는 로그 파일입니다. 시스템 오류나 다운으로 인해 데이터베이스가 예기치 않게 종료되었을 때, 데이터베이스를 마지막 일관된 상태로 복구하는 데 사용됩니다. 리두 로그를 사용하면 트랜잭션이 커밋된 후 발생한 장애 상황에서도 데이터 손실 없이 트랜잭션을 다시 실행하여 데이터베이스 상태를 복구할 수 있습니다.

언두 로그 (Undo Log)

언두 로그는 트랜잭션이 수행하기 전의 데이터 상태를 기록합니다. 트랜잭션이 실패하거나 취소되었을 때, 변경 사항을 되돌리기 위해 사용됩니다. 언두 로그를 통해 데이터베이스는 트랜잭션이 시작하기 전 상태로 데이터를 복구할 수 있으며, 이는 데이터의 무결성과 일관성을 유지하는 데 중요합니다.

Buffer Pool(버퍼풀)

버퍼 풀은 데이터베이스의 메인 메모리 내에 위치하는 저장 공간으로, 디스크에 저장된 데이터 페이지들을 캐싱하는 데 사용됩니다. 이를 통해 데이터베이스는 디스크 I/O를 줄이고, 데이터 접근 시간을 단축시켜 성능을 향상시킬 수 있습니다. 버퍼 풀은 데이터 페이지를 읽거나 수정할 때 중요한 역할을 하며, 데이터베이스의 전반적인 성능에 큰 영향을 미칩니다.

Repeatable Read

먼저 Repeatable Read가 어떻게 동작하는지 살펴보겠습니다. MVCC는 트랜잭션의 버전을 읽어 오고 고유한 트랜잭션 ID를 부여합니다. 또한 트랜잭션 ID는 자신보다 높은 ID를 가진 트랜잭션이 수정한 데이터는 읽지 않습니다.

tran_2가 tran_1보다 낮은 ID를 가지고 있기 때문에 tran_1이 수정한 데이터를 읽지 않습니다. 대신 undo log에 저장된 데이터를 읽습니다.

그럼 팬텀 리드는 어떻게 발생할까요?

Repeatable Read 2

다시 락의 개념이 추가로 필요합니다. 락은 트랜잭션이 데이터를 읽거나 수정할 때, 다른 트랜잭션이 해당 데이터를 읽거나 수정하지 못하도록 막는 역할을 합니다. 그리고 해당 조회는 언두 로그를 통해 데이터를 읽는 것이 아니라 버퍼 풀을 통해 데이터를 읽습니다. 그렇기 때문에 두번째 조회에서는 tran_1이 수정한 데이터를 읽습니다.

Serializable (직렬화 가능)

가장 높은 격리 수준으로, 트랜잭션들이 마치 순서대로 실행되는 것처럼 보장합니다. 모든 종류의 읽기 문제를 방지하지만, 동시성이 크게 감소할 수 있습니다.

Serializable

데드락과 트랜잭션 관리

데드락은 여러 트랜잭션이 동시에 실행되는 환경에서 발생할 수 있는 문제입니다. 트랜잭션이 서로의 자원을 기다리고 있지만, 그 자원이 다른 트랜잭션에 의해 이미 잠겨있어 상호 대기 상태에 빠지게 되는 것을 말합니다. 특히 이러한 상황은 격리 수준이 높을수록, 즉 트랜잭션 간의 공유 자원 접근이 제한적일수록 더 자주 발생합니다.

예를 들어, 격리 수준이 높은 환경에서 두 트랜잭션 A와 B가 있을 때, A가 특정 데이터를 읽거나 수정하고, 동시에 B도 다른 데이터를 읽거나 수정하려고 합니다. A가 데이터 1을 잠그고 데이터 2의 잠금을 기다리는 동안, B가 데이터 2를 잠그고 데이터 1의 잠금을 기다린다면, 양쪽 모두 상대방이 잠근 자원을 기다리는 데드락 상태에 빠지게 됩니다.

물론 트랜잭션이 더 추가되어도 상황은 동일합니다. 세 개의 트랜잭션이 각각 다른 데이터를 잠그고, 다른 데이터의 잠금을 기다리는 상황이라면, 세 트랜잭션 모두 데드락 상태에 빠지게 됩니다.

Dead Lock

예방

데드락을 예방하는 가장 간단한 방법은 트랜잭션들이 자원을 요청하는 순서를 정하는 것입니다. 예를 들어, 트랜잭션 A와 B가 각각 데이터 1과 2를 요청하려고 할 때, 두 트랜잭션 모두 데이터 1을 먼저 요청하고, 데이터 2를 그 다음에 요청하도록 순서를 정한다면, 데드락이 발생하지 않습니다.

또한 트랜잭션들이 자원을 요청할 때, 이미 다른 트랜잭션에 의해 잠겨있는 자원을 요청하는지 확인하는 것도 방법입니다. 그리고 실행 계획에 따라 필요한 데이터 전부를 한 번에 요청하는 것도 데드락을 예방하는 방법 중 하나입니다.

하지만 이러한 방법들은 데드락을 완전히 예방할 수는 없습니다. 그리고 트랜잭션의 병행성을 떨어뜨리는 단점도 있습니다.

회피

데드락을 회피하는 선점형과 비선점형 방법이 있습니다. 물론 은행원 알고리즘과 같은 방법도 있지만 좀 더 단순하게 구분하겠습니다.

선점형

선점형에는 wound-wait가 있습니다.

  • wait-die: 선행 프로세스가 접근하면 대기하고, 후행 프로세스가 접근하면 포기하는 방식입니다.

비선점형

비선점형에는 wait-die가 있습니다.

  • wound-wait: 선행 프로세스가 접근하면 선점하고, 후행 프로세스가 접근하면 대기하는 방식입니다.

실제 사례 연구

좋은 예제가 있어서 가져왔습니다.

understanding isolation levels sql-server 2008 r2 2012

최신 동향 및 기술

클라우드 데이터베이스, 분산 데이터베이스 시스템에서의 트랜잭션 관리와 격리 수준에 대한 최신 기술과 동향을 알아 볼 때, 참고할만한 논문들을 정리해 보았습니다.

하이브리드 클라우드 환경에서의 고급 일관성 관리

클라우드 컴퓨팅에서의 분산 데이터베이스 관리

분산 데이터베이스에서의 트랜잭션 관리

감사합니다!

profile
안녕하세요 👏
FE Developer 죠지입니다.
githubgithubgithub