[DB] (7) 동시성(Concurrency) 문제와 제어 기법

이영교·2024년 9월 2일
0

DB

목록 보기
7/11
post-thumbnail

동시성 문제

이전 글에서 알아본 것처럼, 데이터베이스에서 트랜잭션은 데이터의 일관성과 무결성을 유지하기 위한 중요한 매커니즘이다. 하나의 논리적인 단위로 작업을 그룹화하여, 한번에 성공하거나 실패하도록 보장한다. 트랜잭션을 사용함으로써, 데이터베이스의 상태가 항상 일관된 상태를 유지할 수 있다.

하지만, 격리 수준이 낮게 설정됨에 따라 트랜잭션만으로는 항상 일관된 상태를 유지할 수 없는 상황이 존재한다. 그중 하나의 문제는 동시성 문제이다. 동시성 문제는 여러 사용자가 동시에 동일한 자원에 접근하거나, 여러 프로세스가 동시에 실행되면서 발생하는 문제이다.

image

이전 글과 동일하게 은행 계좌를 예로 들 수 있다. 두 명의 사용자가 동시에 은행 계좌에서 돈을 입금하려고 할 때, 각각의 트랜잭션이 다른 하나를 인지하지 못한 상황에서 계좌 잔액이 잘못 계산될 수 있다. 이러한 문제는 데이터 불일치, 데이터 손실 등 시스템에 중요한 문제를 발생시킬 수 있다.

이처럼 여러 유저의 동시 접속 문제는 서비스를 운영하며 발생할 수밖에 없는데, DBMS 자체는 동시성을 제어할 수 있도록 Lock 기능과 SET TRANSACTION 명령어를 통해 트랜잭션의 격리성 수준을 조정할 수 있도록 제공한다.

이번 글에서는 동시성이 발생하는 문제 상황과 제어할 수 있는 기법에 대해서 이해해보고자 한다.

동시성 문제 원인과 상황

동시성 문제가 발생하는 상황은 충돌에 의한 상황이다. 충돌이란 (1) 다른 트랜잭션에 속하고 있으면서 동일한 데이터 아이템을 처리 대상으로 하는 두 연산, (2) 최소한 하나의 기록(write) 연산을 가지고 있는 두 상황을 의미한다. 즉, 충돌된 데이터들에 대해 트랜잭션들 사이의 충돌 순서가 다르기 때문에 발생한다고 볼 수 있다.

이와 같이 충돌이 발생할 때, 아래와 같이 세 가지 상황이 발생할 수 있다.

(1) 갱신 분실(Lost Update)

image

하나의 트랜잭션이 수행한 데이터 변경 연산의 결과를 다른 트랜잭션의 덮어쓰기로 인해 변경 연산이 무효화되는 상황이다. 예를 들어, 두 개의 트랜잭션(T1, T2)가 동시에 실행되고 있는 상단의 그림과 같은 상황이 있다. 이 예제에서는 두 개의 트랜잭션이 각각 x=3000 레코드의 값을 읽은 상황(Read)에서 트랜잭션 T1이 우선 쓰기(Write)를 하였고, 이를 트랜잭션 T2에서 덮어쓰면서 X의 값을 중첩해서 변경해버렸다.

다수의 트랜잭션이 동시에 실행되는 상황에서 위와 같이 서로 다른 트랜잭션이 업데이트 연산을 연속으로 수행하면, 먼저 실행된 업데이트 연산은 Overwriting이 된다. 즉, 먼저 실행된 T1 의 쓰기 연산은 정상적으로 실행됐으나, 그 값은 변경되지 않은 문제가 발생하는 것이다.

아래는 정상적으로 작동했을 때의 결과가 나와야 하는 상황을 나타낸다.

image

(2) 모순성(Inconsistency)

image

하나의 트랜잭션이 일관성 없는 상태의 데이터베이스에서 데이터를 가져와서 연산함으로써 모순된 결과가 발생하는 상황이다. 예를 들어, 트랜잭션 T1에서 X의 값을 읽을 때의 시각과 Y를 읽을 때의 시각 사이에 트랜잭션 T2가 들어가 있는 것을 볼 수 있다. 따라서, 처음 연산을 실행했을 때의 예상과 다른 연산 결과가 발생한다.

이를 해결하기 위해 아래와 같은 트랜잭션을 구성해야 한다.

image

(3) 연산 복귀(Cascading Rollback)

image

트랜잭션이 완료되기 전 장애가 발생하여 Rollback 연산을 수행하면, 장애 발생 전에 해당 트랜잭션이 변경한 데이터를 가져가서 변경 연산을 실행한 다른 트랜잭션에도 Rollback 연산을 연쇄적으로 실행해야 한다.

위의 그림을 문제 상황 예로 보면, 현재 트랜잭션 T1 에서 read(Y)를 하는 상황에 장애가 발생했다고 가정한다. 따라서, 트랜잭션 T1에서 업데이트 한 X = X + 1000의 연산을 롤백해야 하는 상황이지만, 트랜잭션 T2가 이미 실행이 완료된 상황이라 이를 롤백할 수 없는 문제가 발생한다.

문제 없이 완료되려면, 아래와 같이 트랜잭션을 구성해야 한다.

image

동시성 제어 기법

(1) 비관적 락 : 로킹 기법(Locking)

트랜잭션이 사용하려고 하는 데이터 자원에 대하여 상호 배제(Mutual Exclusive) 기능을 제공하는 기법을 의미한다. 기본적인 로킹 규약은 트랜잭션이 접근하려고 하는 데이터에 대해 먼저 Lock을 설정한 후, 독점권을 획득한 데이터에 대해 모든 연산의 수행이 끝나면, 트랜잭션은 unlock 연산을 통해 독점권을 반납한다.

이때, 다른 트랜잭션은 이미 lock 연산이 실행된 데이터에 대해 lock 연산을 수행할 수 없다.

로킹 연산을 실행하는 대상 데이터의 크기(데이터베이스, 릴레이션, 튜플, 속성까지도 가능)에 따라 병행성과 제어가 상관관계를 가지게 된다. 로킹 단위가 커질수록(데이터베이스 쪽) 병행성은 낮아지지만, 제어가 쉽다는 장점이 있으며, 그 반대는 반대의 효과를 가지게 된다. 하지만, 너무 단위가 크게 되면 성능 문제가 발생할 수 있다는 문제가 있다.

이를 해결하기 위해, 기본적인 로킹 규약의 효율성을 높이기 위한 방법이 존재한다. 데이터에 동시에 read 연산을 실행하는 것을 허용한다. lock 연산을 두 가지 종류로 구분해서 사용한다.

image

위 표에서 볼 수 있듯이, 서로 다른 트랜잭션은 같은 데이터에 공용 lock 연산을 동시에 실행할 수 있으나, 전용 lock 연산을 실행한 데이터에는 공용 lock, 전용 lock 모두 실행 불가하다.

이러한 로킹 기법에는 허점이 존재한다. 락을 걸거나 해제하는 시점에 제한을 두지 않으면 두 개의 트랜잭션이 동시에 실행될 때 데이터의 일관성이 보장되지 않을 수 있다. 즉, 중간에 락의 적용이 되지 않음으로써, 허점이 발생하는데 이를 2단계 로킹 기법(2 Phase Locking)으로 해결할 수 있다.

우선 문제가 되는 상황부터 살펴보도록 하자.

image

2단계 로킹 규약이란, lock과 unlock 연산의 수행 시점에 대한 새로운 규약을 추가한 것으로, 트랜잭션이 확장 단계와 축소 단계로 나누어져 실행할 수 있도록 만든 것이다.

image

트랜잭션이 처음 수행되면, 확장 단계로 들어가 lock 연산만 실행 가능하도록 한다. 이후 unlock 연산을 실행하게 되면, 축소 단계로 들어가 unlock 연산만을 수행할 수 있도록 한다. 즉, 트랜잭션은 첫 번째 unlock 연산을 실행하기 전에 필요한 모든 lock 연산을 실행해야 한다.

image

위와 같이 2단계 로킹 규약을 설정함에 따라, 모든 트랜잭션이 데이터에 일관성을 유지할 수 있게 된다. 하지만, 이 락을 거는 방법이 과연 만능일 것인가??

그렇지 않다. 그 이유는 데드락(DeadLock)이라는 개념을 이해해야 한다. 해당 개념은 다음 블로그 글에서 정리하고자 한다. (내용이 너무 길어지기 때문에,,)

(2) 낙관적 락

낙관적 락의 경우, 데이터를 읽을 때 락을 걸지 않고 업데이트할 때만 이전의 데이터와 현재 데이터를 비교하여 충돌 여부를 판단한다. 이 방식은 데이터베이스의 성능 저하를 최소화하고, 동시성을 높이는 데 유리하기 때문이다.

데이터베이스 레벨이 아닌 애플리케이션 레벨에서 락을 핸들링 함으로써 동시에 여러 트랜잭션이 데이터를 읽을 수 있으며, 성능이 향상될 수 있다. 버전 관리를 톻애 동작하며, 엔티티의 상태가 변경될 때마다 버전이 증가하며, 엔티티의 버전을 통해 충돌을 감지하고 처리한다.

충돌이 발생했다고 판단이 되면 데이터를 다시 읽고, 업데이트해야 하는 추가 작업이 필요하다. 이때 롤백을 진행하게 되는데, 충돌을 해결하기 위해선 충돌 발생 시점부터 다시 트랜잭션을 시작해야 하기 때문이다. 롤백될 때 자원 낭비가 발생하기 떄문에, 동시 사용 빈도가 낮은 시스템에서 주로 사용한다.

시뮬레이션을 진행하면 다음과 같다.

  1. 데이터 읽기
    트랜잭션 A가 데이터를 읽으며 이때의 버전 정보(id : 1, version:1)을 함께 불러온다.
  2. 데이터 수정 및 업데이트
    트랜잭션 B가 동일한 데이터를 읽고, 수정 작업을 진행하고 A보다 먼저 커밋을 진행한다. 그렇게 되면 version이 2로 올라가게 된다.
  3. 업데이트 시도 및 충돌 감지
    기존의 데이터가 버전이 1이지만, 현재는 2임을 확인하고 트랜잭션을 롤백한다. 만약 같다면 그대로 수정 작업을 진행한다.

결론

데이터베이스 시스템에서 데이터의 일관성, 무결성을 유지하는 것은 매우 중요하다고 생각한다. 이를 위해, 트랜잭션과 동시성 제어를 고려해야 하는데, 트랜잭션은 데이터베이스 작업을 안전하게 실행하는 기본 단위를 의미하며, 동시성 제어는 여러 트랜잭션이 동시에 실행될 때 발생할 수 있는 문제를 해결한다.

동시성을 제어할 수 있는 방법으로 크게 두 가지로 나눌 수 있는데, 비관적 락 방법은 데이터의 일관성을 유지하는 데 초점을 맞추고 있는 반면, 낙관적 락의 경우 시스템의 성능에 조금 더 초점을 맞추고 있다. 두 가지 방법에 대한 장단점을 잘 활용해서 필요한 곳에 사용할 수 있도록 노력해야겠다.

profile
글쓰기 연습하는 사람입니다.

0개의 댓글