최근 운영 중이던 서버에서 데드락으로 인해 결제가 되지 않는 이슈가 발생하였다.
트랜잭션, 데드락 등의 개념은 항상 이해했다고 생각하면서도 문제 발생 시 바로 원인과 해결법이 떠오르지 않는 개념이라 생각한다. 이번 이슈에 있었던 내용과 함께 새로이 알게 된 내용을 정리해 보려고 한다.
데드락은 서로 다른 트랜잭션이 있을때 각 트랜잭션에서 점유된 내용을 대기할 때 교착상태에 이르는 경우라고 할 수 있다.
임의의 테이블 ORDER 테이블이 있다고 가정하자 이때 PK 는 ORDER_ID라고 하겠다
1번 트랜잭션과 2번 트랜잭션이 아래와 같은 상황일 때 데드락은 발생한다.
-- 1번 트랜잭션: ORDER_ID = 1 인 ROW에 대한 쓰기락
UPDATE ORDER
SET ORDER_STATE = '신규값'
WHERE ORDER_ID = 1;
-- 2번 트랜잭션: ORDER_ID = 2 인 row 에 대한 쓰기락
UPDATE ORDER
SET ORDER_STATE = '신규값'
WHERE ORDER_ID = 2;
-- 1번 트랜잭션: ORDER_ID = 2인 ROW에 대해 접근하나 2번 트랜잭션의 락으로 인한 대기
UPDATE ORDER
SET ORDER_STATE = '신규값'
WHERE ORDER_ID = 2;
-- 2번 트랜잭션: ORDER_ID = 1인 ROW에 대해 접근하나
-- 1번 트랜잭션의 락으로 인한 대기
UPDATE ORDER
SET ORDER_STATE = '신규값'
WHERE ORDER_ID = 1;
위의 예시처럼 트랜잭션에서 데이터에 수정이 이루어지면 row 에 lock 이 걸리기 때문에 다른 트랜잭션은 대기하게 되고 이러한 상황이 맞물리며 트랜잭션간 교착상태에 걸릴 경우 락이 걸리게 되는것이다.
이 ORDER 테이블에 ORDER_NO라는 유니크한 값이 있다고 할 때 해당 컬럼에 유니크 키 설정이 안되어있고, 아래와 같은 상황이 발생한 것이 운영중이던 서버에서 발생했던 락 상황이다.
-- 1번 트랜잭션: ORDER_ID = 1 인 ROW에 대한 쓰기락
UPDATE ORDER
SET ORDER_STATE = '신규값'
WHERE ORDER_ID = 1;
-- 2번 트랜잭션: ORDER_ID = 2 인 row 에 대한 쓰기락
UPDATE ORDER
SET ORDER_STATE = '신규값'
WHERE ORDER_ID = 2;
-- 1번 트랜잭션: ORDER_NO = 1 은 ORDER_ID = 1 인 row 와 같은 하나의 row
UPDATE ORDER
SET ORDER_STATE = '신규값'
WHERE ORDER_NO = 1;
-- 2번 트랜잭션: ORDER_NO = 2는
-- ORDER_ID = 2인 row와 같은 하나의 row
UPDATE ORDER
SET ORDER_STATE = '신규값'
WHERE ORDER_NO = 2;
얼핏 보면 문제 없을 것 같은 상황이다. 1번 트랜잭션은 1번 row 에만, 2번 트랜잭션은 2번 row 에만 접근하고 있기에 서로의 트랜잭션에 영향을 주지 않을 것 같이 예상된다. 당시 운영 중이던 서버에서 발생한 상황도 위와 같았음에도 불구하고 트랜잭션 락에 걸리게 된다. 락에 걸리게 된 이유는 제목에 있는 내용처럼 유니크 키를 설정할 경우에 해결되는 상황이었다.
락에 걸린 상황에 대해 설명해보겠다.
각 업데이트 문을 1 ~ 4번 업데이트라고 순서대로 칭하겠다.
1 ~ 2번 업데이트는 평범한 락 획득 과정이다. 3번 업데이트가 발생할 때 무슨 일이 발생할까?
우선 ORDER 테이블의 ORDER_NO = 1 인 값을 테이블에서 찾아야 할 것이다. ORDER_NO는 유니크 키가 아니므로 해당 row 를 찾기 위해 인덱스 스캔이 아닌 풀스캔이 발생할 것이다. ORDER_NO = 1 인 값을 찾기 위해 테이블 전체를 스캔하게 될 것이고 2번 트랜잭션에서 ORDER_ID = 1 인 row에 락을 걸어두었기 때문에 1번 트랜잭션은 대기에 빠지게 된다. 같은 경우로 4번 업데이트 문도 대기에 빠지게 되며 두 트랜잭션은 데드락에 걸리게 되는 것이다.
하지만 유니크 키를 걸게 되면 어떻게 될까?
3번 업데이트문과 4번 업데이트문은 풀스캔이 아닌 인덱스 스캔을 하게 될 것이고 해당 row 에 락을 한 것은 동일 트랜잭션이므로 스캔하는 것에 문제가 없게 되고, 두 트랜잭션은 독립적으로 처리되어 데드락 문제가 해결되게 된다.
업데이트 문을 간단히 써두었지만 실제 운영하던 서버 코드에서는 해당 원인을 찾는게 쉽지 않았다.
락이 걸린 순간의 업데이트 문과 연관된 테이블을 체크한 이후 해당 테이블에 관한 트랜잭션의 로직들을 확인하면서 의구심이 가는 부분을 확인했고, 그것이 맞았다.
해당 내용에 대해 실제 체크하기 위하여 임시 테이블을 만들고 사용하는 DB 툴인 DBeaver 에서 두개의 세션 스크립트를 열어두고 오토 커밋이 아닌 메뉴얼 커밋을 이용하여 테스트를 진행하였다. 그 결과 위와 같은 원인을 찾을 수 있었다.
인덱스, 제약 조건 등은 데이터의 신뢰성 및 DB의 성능을 위해서는 잘 처리해두어야 하지만 가끔 초기 설계 시 누락되거나, 프로젝트 진행 중에 추가된 컬럼들로 인해 올바르게 정리가 되어있지 않을 때가 존재한다. 하지만 이처럼 올바르게 정의되어지지 않은 테이블을 이와 같은 예상치 못한 문제가 발생할 가능성이 있다는 것을 다시 한 번 생각하게 되었다.
유니크 키를 설정한 해당 서버는 현재까지 데드락 이슈가 발생하지 않고 있다.
'Develop Trouble' 카테고리의 다른 글
| Postgresql Fdw(Foregin Data Wrapper) 테이블 조인시 성능 향상 (2) | 2025.08.04 |
|---|---|
| Oracle to PostgreSQL DB 변환 시 참고 사항 및 차이점 정리 (0) | 2025.02.06 |
| Javascript debugger 가 동작하지 않을 경우 (1) | 2025.02.06 |
| Docker 컨테이너 환경 폰트 추가하기 (0) | 2025.02.05 |
| Java 배열 생성 시의 효율 (0) | 2024.10.24 |