최근 회사에서 검수 기능을 고도화하는 업무가 있었습니다. 상품을 스캔한 후 적절한 비즈니스 로직에 따라 수량을 데이터베이스에 자동으로 저장하는 로직입니다.
@Transactional
public void inspectGoods(goods request) {
// 상품 정보 조회
Goods goods = findGoods(request);
// 상품 상태에 따른 입고 수량 설정 로직
// 상품 검수
merge(goods);
}
검수 기능의 흐름은 대략적으로 위 코드와 같습니다. 기존에 존재하던 기능에서 수량 설정 로직만 추가되었고 추가로 QC를 진행 후 정상적인 과정을 통해 운영에 신규 기능이 배포되었습니다. 하지만 며칠 뒤 APM 모니터링을 통해 신규 개발 API 에 primary key violation
에러가 간헐적으로 발생되는 것이 확인됐습니다.
해당 API 네트워크 워터폴을 분석해보니 매우 짧은 시간(거의 동시에)에 동일한 API 를 n 번 호출하여 발생한 문제였습니다. 조치를 위해 확인해보니 클라이언트 사이드에서 다음과 같은 문제점이 있었습니다.
- 두개의 상품(마스터, 검수) 조회 로직으로 잘못된 비즈니스 플로우로 진행된다.
- 네트워크 지연이 발생하면 자체적으로 검수 상품 조회 API 를 호출한다.
부끄럽지만 기능 구현 요건을 잘못 이해하여 실수가 발생했고 해당 부분을 담당자와 문제를 신속하게 파악하고 수정을 통해 정상적으로 복구했습니다.
비록 원인이 비정상적이었던 해프닝(?)이었지만 위 문제를 해결하기 위해 여러 자료를 조사했습니다. 그 중 하나인 'merge 문의 동시성'에 대해 알게 된 내용을 공유하고자 이렇게 포스팅을 작성하게 되었습니다.
결론부터 말하자면 merge
구문은 all or nothing 의 의미로 atomic 을 보장하지만 로우 레벨의 오퍼레이션에서 독점적으로 lock 을 획득하는 과정은 atomic 하지 않습니다.
시나리오
먼저 실무에서 발생한 오류 사항은 먼저 매우 짧은 시간에 동일한 merge 문이 n 번 호출됨으로 인해 발생한 race condition 문제입니다. 즉 선행 트랜잭션이 종료되기 이전에 후행 트랜잭션에 의해 pk 충돌이 발생하여 tx lock 경합이 발생한 상황입니다.
이 상황을 테스트해보기 위해 먼저 로컬 환경에서 테스트용 테이블을 생성합니다.
create talbe foo (
id number primary key,
name varchar2(10),
);
그 후 두개의 세션에서 순차적으로 실행할 쿼리를 다음과 같이 작성합니다.
merge into foo a using (select 1 as id, 'test' as name from dual) b on (a.id = b.id)
when matched then
update set a.name = 'test1'
when not matched then
insert (id, name) values (b.id, b.name);
먼저 첫번째 세션에서 merge 문을 실행 후 활성화된 락 정보를 확인하기 위해 v$lock 뷰를 조회합니다.
select * from v$lock where type = 'TX';
V$LOCK
v$lock 은 Oracle 에서 현재 활성화된 락 정보를 통해 락 상태를 모니터링하고 분석하는데 사용되는 뷰로 주요 컬럼에 대한 설명은 다음과 같습니다.
주요 컬럼
- ADDR: 락의 주소.
- SID: 락을 보유한 세션의 식별자(ID).
- TYPE: 락의 유형으로 DML, DDL, CF, TM, UL 등이 있습니다.
- LMODE: 락을 보유하거나 획득하기 위해 대기 중인 세션의 락 모드.
- REQUEST: 락을 요청한 세션의 락 모드(lock mode)로 세션이 락을 요청하고 있는 경우에만 사용됩니다.
- BLOCK: 락을 획득하거나 획득하기 위해 대기 중인 세션의 식별자(ID)로 락이 블로킹 상태인 경우에만 해당됩니다.
- BLOCKER: 락을 획득하거나 획득하기 위해 대기 중인 세션을 블로킹하는 세션의 식별자(ID)로 락이 블로킹 상태인 경우에만 해당됩니다.
LMODE & REQUEST
- 0(none): 세션이 현재 락을 보유하고 있지 않은 상태.
- 1(null): 오라클 내부에서 사용되는 값.
- 2(row-S(SS)): 다수의 세션 간에 동시에 데이터를 읽는 것을 명시적으로 허용하는 락 모드.
- 3(row-X(SX)): 특정 데이터 행을 수정하기 위해 사용되는 배타적인 락 모드.
- 4(share(S)): 공유 락 모드로 다른 세션들이 데이터를 읽을 수 있지만 수정할 수 없는 락 모드.
- 5(S/Row-X(SSX)): 다수의 세션이 데이터를 공유하면서도 특정 데이터 행에 대한 수정이 필요한 경우 사용하는 락 모드.
- 6(exclusive(X)): 하나의 세션이 데이터를 수정하는 동안 다른 세션들은 해당 데이터에 대한 읽기 및 쓰기 접근을 할 수 없는 락 모드.
위의 쿼리를 실행하면 다음과 같은 결과를 얻습니다.
첫번째 세션(314)에서 실행된 merge 문은 현재 X Lock 을 획득한 상태입니다. 그 후 동일한 mere 문을 두번째 세션에서 실행하면 아래와 같이 첫번째 세션에 의해 blocked 된 상태로 대기합니다.
락 상태를 모니터링하기 위한 시스템 카탈로그 뷰인 dba_blockers 와 dba_waiters 를 조회합니다. 먼저 다른 세션의 작업을 차단하고 있는 세션을 조회할 수 있는 dba_blockers 뷰를 조회합니다.
select * from dba_blockers;
선행 세션 314가 현재 블로킹 상태에 있음을 확인할 수 있습니다.
select * from dba_waiters;
dba_waiters 를 위의 쿼리를 이용하여 조회합니다. dba_waiters 뷰는 락 대기 상태에 있는 세션들에 대한 정보를 제공하며 대기 중인 세션과 해당 세션이 어떤 락을 기다리고 있는지 그리고 현재의 락 모드와 같은 정보를 제공합니다.
반환된 행의 내용을 해석해보자면 두번째 세션(172)은 현재 share lock 락을 요청(mode_requested)하고 대기 중인 세션(waiting_session)으로 현재 락을 점유 중인 314 세션(holding_session)의 exclusive lock(mode_held) 의해 blocked 된 상태입니다. 즉 현재 하나의 자원이 두개의 세션에 의해 경합이 발생된 상태가 식별된 것입니다.
그 후 v$lock 뷰를 재조회합니다.
두번째 세션이 172 에 대한 락 정보가 첫번째 세션과는 다르게 두개의 행이 추가되었습니다. 먼저 두번째 세션에서 share lock 을 요청한 것을 통해 오라클은 데이터 정합성을 위해 어떤 전략으로 데이터에 접근하는지 간접적으로 추론할 수 있습니다. 쓰기 작업이 발생하면 테이블에 로우를 추가한 후 인덱스도 함께 추가하게 되는데 이 과정에서 레코드가 unique 속성을 위반하는지 확인하기 위해 share lock 을 사용하는 것입니다.
결국 matched 를 판단하는 on 절의 조건은 consistent read 로 동작되기 때문에 선행 세션에서 커밋 되기 이전 후행 세션에서도 동일하게 x lock 을 획득 후 insert 문으로 계획이 세워진 후 blcoked 된 채로 대기 중입니다. 이후 선행 세션이 정상적으로 merge 문을 실행 후 커밋을 수행하게 되면 후행 세션에서는 update 문이 수행되지 않고 ORA-00001 에러가 발생합니다.
즉 정리하자면 다음과 같은 시나리오로 정리할 수 있습니다.
- 두개의 세션에서 동시에 MERGE 구문을 실행한다.
- 두개의 세션에서 동시에 S LOCK을 획득하지만 결국 두 개 세션 중 하나의 세션만이 X Lock 을 선점하게 된다.
- 따라서 후속으로 실행된 세션을 첫번째 세션이 트랜잭션이 커밋될때까지 BLOCKED 된 상태로 대기한다. 하지만 후속 세션은 조회 단계에서 데이터가 존재하지 않았기 때문에 INSERT 로 계획이 세워진다.
- 첫번째 세션에서 INSERT 작업 후 트랜잭션이 커밋된다.
- 두번째 세션이 LOCK 을 얻어 INSERT 를 실행하지만 이미 ROW 가 존재하기 때문에 PK Viloation 이 발생한다.
PS
추가로 리더분들께서 merge
문 사용 여부에 대해 열띈 토론을 하셨는데 그에 대한 최종 의사결정은 "병행해서 사용하자" 였습니다. 하지만 저희 파트는 Entity 를 개발자가 직접 제어해야한다는 맥락에서 값 존재 여부를 개발자가 알 수 없는 merge 문에 다소 회의적인 의견을 수렴하여 이번 일을 계기로 merge
문을 제거하도록 의사결정을 했습니다.
'ETC' 카테고리의 다른 글
[ETC] 분산 트랜잭션 처리 전략 MA 환경에 적용하기(1) (0) | 2024.10.08 |
---|