SQL) 트랜잭션과 Lock Ⅱ

jinsung·2025년 12월 5일

SQL

목록 보기
14/46
post-thumbnail

3. 비관적 vs 낙관적 동시성 제어

동시성 제어는 비관적 동시성 제어와 낙관적 동시성 제어로 나뉜다

1. 비관적 동시성 제어(Pessimistic Concurrency Control)

사용자들이 같은 데이터를 동시에 수정 할 것이라고 가정한다.
따라서, 한 사용자가 데이터를 읽는 시점에 Lock을 걸고 조회 또는 갱신처리가 완료될 때까지 이를 유지한다. Locking은 첫 번째 사용자가 트랜잭션을 완료하기 전까지 다른 사용자들이 그 데이터를 수정할 수 없게 만들기 때문에 비관적 동시성 제어를 잘못 사용하면 동시성의 저해를 받게 된다.

SELECT 적립포인트, 방문횟수, 최근방문일시, 구매실적
FROM 고객
WHERE 고객번호 = "cuto_num FOR UPDATE;

--- 새로운 적립포인트 계산

UPDATE 고객 
SET 적립포인트 = :적립포인트 
WHERE 고객번호 = :cust_num

고객의 다양한 실적정보를 읽고 복잡한 산출공식을 이용해 적립포인트를 계산하는 동안 다른 트랜잭션이 같은 고객 레코드를 변경한다면 문제가 발생할 수 있다.

위 쿼리와 같이 SELECT문에 FOR UPDATE를 사용해서 해당 고객 레코드에 Lock을 걸어둔다면 데이터가 잘못 갱신되는 문제를 방지할 수 있다.
하지만, SELECT 시점에 Lock을 거는 비관적 동시성 제어는 자칫 시스템 동시성을 심각하게 떨어뜨릴 우려가 있다. 그러므로 wait나 nowait 옵션을 함께 사용하면, 다른 트랜잭션에 의해 Lock이 걸렸을 때 Exception을 만나게 되므로 트랜잭션을 종료할 수 있다. 따라서 동시성을 증가시킬 수 있다.

FOR UPDATE nowait --> 대기 없이 Exception(ORA-00054)를 던짐
FOR UPDATE wait 3 --> 3초 대기 후 Exception(ORA-3006)을 던짐

2. 낙관적 동시성 제어(Optimistic Concurrency Control)

사용자들이 같은 데이터를 수정하지 않을 것이라고 가정한다.
따라서, 데이터를 읽을 때는 Lock을 설정하지 않는다. 읽는 시점에 Lock을 사용하지 않았지만, 데이터를 수정하고자 하는 시점에 앞서 읽은 데이터가 다른 사용자에 의해 변경되었는지를 반드시 검사해야 한다.
낙관적 동시성 제어를 사용하면 Lock이 유지되는 시간이 매우 짧아져 동시성을 높이는 데에 유리하다. 하지만 다른 사용자가 같은 데이터를 변경했는지 검사하고 그에 따라 분기해 나가야 하는 귀찮음이 따른다.


4. 동시성 구현 사례

1. 일련번호 채번 동시성 높이기

첫 번째는, Locking을 최소화하면서 채번 테이블로부터 일련번호를 채번하고자 할 때 사용할 수 있는 사례이다. 일련번호를 채번할 때 가장 좋은 선택은 DBMS가 제공하는 Sequence 기능을 이용하는 것이다.
하지만, 이 기능을 어떤 상황에서는 사용할 수 없고 항상 이 기능을 사용할 수 없으므로 그 때의 방법이 있다.

1. 데이터가 삽입되는 시점에 실시간으로 현재의 MAX 값을 취해 1만큼 증가시킨 값을 이용한다

-> 실시간으로 MAX 값을 얻어 처리할 때는 두 개의 트랜잭션이 동시에 같은 값을 읽었을 경우, INSERT 하려는 순간 PK 제약에 위배되므로 예외처리를 통해 동시성 제어를 할 수 있다.

2. MAX 값을 관리하는 별도의 채번 테이블에서 값을 가져온다

autonomous 트랜잭션이 뭐임?

오라클에서 제공하는 기능으로, 메인 트랜잭션에 영향을 주지 않고 서브 트랜잭션만 따로 커밋하는 기능이다.


2. 선분이력 정합성 유지

선분이력 모델이 뭔데?

선분이력모델이란 데이터의 유효 기간(시작일~종료일)을 선분(선의 구간)처럼 관리하며, 시간에 따라 어떻게 변경됐는지를 저장하는 이력 관리 모델을 말한다

선분이력모델은 여러 측면에서 장점이 있지만 잘못하면 데이터 정합성이 쉽게 깨질 수 있다는 단점이 있다.

declare
	cur_dt varchar(14);
begin
1.  cur_dt := to_char(sysdate, 'yyyymmddhh24miss');

2.	update 부가서비스이력
	set 종료일시 = to_date(:cur_dt, 'yyyymmddhh24miss') - 1/24/60/60
    where 고객ID = 1
	and 부가서비스ID = 'A'
	and 종료일시 = to_date('99991231235959','yyyymmddhh24miss');
    
3.  insert into 부가서비스이력(고객ID, 부가서비스ID, 시작일시, 종료일시)
    values(1, 'A', to_date(:cur_dt, 'yyyymmddhh24miss'),
    		to_date('999912312235959', 
            'yyyymmddhh24miss');

4.	commit;
end;

위 트랜잭션은 기존 최종 선분이력을 끊고 새로운 이력 레코드를 추가하는 전형적인 처리 루틴이다. 신규 등록 건이면 2번 update문에서 실패하고 3번에서 한 것이 insert 된다.

만약, 첫 번째 트랜잭션이 1을 수행하고 2로 진입하기 직전에 두 번째 트랜잭션이 동일 이력에 대해 1~4번을 먼저 진행한다면 선분이력이 깨지게 된다.
따라서, 트랜잭션이 순차적으로 진행할 수 있도록 직렬화 장치를 마련해야 하는데, 1번 문장을 수행하기 직전에 select for update문을 사용해 해당 레코드에 Lock을 설정하면 된다.

select 고객ID
from 부가서비스이력
where 고객ID = 1
and 부가서비스ID = 'A'
and 종료일시 = to_date('99991232135959', yyyymmddhh24miss')
FOR UPDATE NOWAIT;

그런데 아래처럼 부가서비스이력에 Lock을 걸어 동시성을 관리한다면 기존에 부가서비스이력이 전혀 없던 고객일 경우 Lock이 걸리지 않는다. 그러면 동시에 두 트랜잭션이 3번 insert문에 들어갈 수 있고, 결과적으로 시작일시는 다르면서 종료일시가 같은 두 개의 이력 레코드가 생긴다.

따라서 부가서비스이력의 상위 엔터티인 고객 테이블에 Lock을 걸어야 한다.

select 고객ID
from 고객
where 고객ID = 1
FOR UPDATE NOWAIT;

부가서비스이력의 상위 엔터티는 고객과 부가서비스가 있는데 부가서비스는 여러 사용자가 동시에 접근할 가능성이 커 고객테이블에 Lock을 설정했다.

profile
Data Engineer

0개의 댓글