고동시성 웹 애플리케이션의 데이터베이스 교착 상태 탐색
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
현대 웹 애플리케이션 환경에서 동시성은 단순한 기능이 아니라 근본적인 요구 사항입니다. 수많은 동시 구매를 처리하는 전자상거래 플랫폼부터 실시간으로 업데이트되는 소셜 미디어 피드에 이르기까지, 애플리케이션은 끊임없이 여러 사용자 요청을 처리합니다. 이러한 병렬성은 풍부한 사용자 경험을 제공하지만, 데이터베이스 교착 상태라는 중대한 문제를 야기하기도 합니다. 이러한 악의적인 시나리오는 시스템 운영을 중단시키고 성능을 저하시키며 궁극적으로는 사용자 경험을 저해할 수 있습니다. 따라서 교착 상태를 이해하고 효과적으로 완화하는 것은 단순한 기술적 세부 사항을 넘어 강력하고 확장 가능한 웹 서비스를 구축하는 데 중요한 역할을 합니다. 이 블로그 게시물에서는 데이터베이스 교착 상태를 명확히 설명하고, 그 원인, 탐지 방법, 그리고 예방 및 해결을 위한 실용적인 전략을 탐구하여 고동시성 웹 애플리케이션이 응답성과 안정성을 유지하도록 보장할 것입니다.
교착 상태 이해 및 완화
교착 상태를 효과적으로 처리하려면 먼저 관련 핵심 개념을 명확하게 이해해야 합니다.
핵심 용어
- 교착 상태(Deadlock): 두 개 이상의 트랜잭션이 서로가 필요로 하는 잠금을 해제할 때까지 무한정 기다리는 상태입니다. 다리를 건너야 하는 두 사람이 있지만, 각자 상대방이 먼저 움직여야만 움직일 수 있다고 상상해 보세요. 아무도 건너지 못할 것입니다.
- 잠금(Lock): 데이터베이스 관리 시스템(DBMS)이 데이터에 대한 동시 접근을 관리하는 데 사용하는 메커니즘입니다. 트랜잭션이 데이터를 읽거나 수정해야 할 때, 다른 트랜잭션이 방해하는 것을 방지하기 위해 해당 데이터에 대한 잠금을 획득합니다.
- 트랜잭션(Transaction): 단일하고 분리할 수 없는 일련의 작업으로 처리되는 하나 이상의 작업을 포함하는 논리적 단위입니다. 트랜잭션은 전체적으로 완료(커밋)되거나 전혀 영향을 미치지 않아야(롤백) 합니다.
- 동시성 제어(Concurrency Control): 트랜잭션의 동시 실행 결과가 올바른지 확인하는 데 사용되는 메커니즘 집합입니다. 잠금은 동시성 제어의 기본 도구입니다.
- 격리 수준(Isolation Level): 한 트랜잭션이 다른 동시 트랜잭션의 영향으로부터 어느 정도 격리되어야 하는지를 정의합니다. 다양한 격리 수준은 일관성과 동시성 간의 다양한 절충안을 제공합니다.
교착 상태 발생 원인
교착 상태는 일반적으로 네 가지 필수 조건, 즉 코프만(Coffman) 조건이 충족될 때 발생합니다.
- 상호 배제(Mutual Exclusion): 적어도 하나의 리소스는 공유 불가능한 모드로 유지되어야 합니다. 한 번에 하나의 프로세스만 리소스를 사용할 수 있습니다.
- 점유 및 대기(Hold and Wait): 하나 이상의 리소스를 보유하고 있는 프로세스가 다른 프로세스가 보유한 추가 리소스를 획득하기 위해 기다립니다.
- 선점 불가(No Preemption): 리소스를 보유한 프로세스로부터 강제로 빼앗을 수 없으며, 획득한 프로세스가 자발적으로 해제해야 합니다.
- 순환 대기(Circular Wait): 프로세스 A, B, C, ... 집합이 순환 방식(A는 B를 기다리고, B는 C를 기다리고, C는 A를 기다리는)으로 서로를 기다립니다.
전자상거래 애플리케이션에서 주문과 해당 재고를 동시에 업데이트하는 일반적인 시나리오를 고려해 보겠습니다.
트랜잭션 A (주문 업데이트 후 재고 업데이트):
BEGIN TRANSACTION;
UPDATE Orders SET status = 'processed' WHERE order_id = 123;
(Orders
행 123에 대한 잠금 획득)UPDATE Products SET stock = stock - 1 WHERE product_id = 456;
(Products
행 456에 대한 잠금 획득 시도)
**트랜잭션 B (재고 업데이트 후 주문 업데이트):
BEGIN TRANSACTION;
UPDATE Products SET stock = stock - 1 WHERE product_id = 456;
(Products
행 456에 대한 잠금 획득)UPDATE Orders SET last_updated = NOW() WHERE order_id = 123;
(Orders
행 123에 대한 잠금 획득 시도)
트랜잭션 A가 트랜잭션 B가 Products
행 456에 대한 잠금을 획득하기 직전에 Orders
행 123에 대한 잠금을 획득하고, 각 트랜잭션이 다른 트랜잭션의 잠금을 획득하려고 시도하면 순환 대기가 발생합니다. 둘 다 더 이상 진행할 수 없습니다. 데이터베이스의 교착 상태 탐지기가 결국 이 상황을 식별하고 일반적으로 하나의 트랜잭션을 '희생자'로 선택하여 사이클을 끊기 위해 롤백합니다.
교착 상태 식별
데이터베이스 시스템은 교착 상태를 탐지하고 보고하는 메커니즘을 제공합니다.
- 데이터베이스 로그: 대부분의 관계형 데이터베이스는 교착 상태 이벤트를 기록합니다. 예를 들어, MySQL에서는
innodb_print_all_deadlocks
를 활성화하거나 (SHOW ENGINE INNODB STATUS
확인) 교착 상태에 관련된 SQL 문과 보유/요청된 잠금을 포함한 자세한 정보를 확인할 수 있습니다. SQL Server는sys.dm_tran_locks
및sys.dm_os_wait_stats
동적 관리 뷰를 가지고 있으며, SQL Server Profiler 또는 확장 이벤트를 통해 교착 상태 그래프 이벤트를 제공합니다. PostgreSQL은 서버 로그에 교착 상태를 보고합니다. - 애플리케이션 모니터링: APM(애플리케이션 성능 모니터링) 솔루션과 같은 도구는 종종 교착 상태로 인해 롤백된 트랜잭션을 표시할 수 있지만, 데이터베이스 수준의 세분화된 세부 정보를 제공하지 못할 수도 있습니다.
다음은 MySQL 교착 상태 로그 항목의 예(간략화됨)입니다.
------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-10-27 10:30:05 0x7f0b5c000700
*** (1) TRANSACTION:
TRANSACTION 251846, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 14, OS thread handle 140660424578816, query id 23 localhost root updating
UPDATE Orders SET status = 'processed' WHERE order_id = 123
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 4 n bits 72 index `PRIMARY` of table `testdb`.`Products` trx id 251846 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 251847, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 15, OS thread handle 140660424578816, query id 24 localhost root updating
UPDATE Products SET stock = stock - 1 WHERE product_id = 456
*** (2) HOLDS THE FOLLOWING LOCKS:
RECORD LOCKS space id 25 page no 4 n bits 72 index `PRIMARY` of table `testdb`.`Products` trx id 251847 lock_mode X locks rec
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 24 page no 3 n bits 72 index `PRIMARY` of table `testdb`.`Orders` trx id 251847 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (1)
이 출력은 두 트랜잭션, (1)과 (2)를 명확하게 보여주며, 현재 작업, 보유하고 있는 잠금, 기다리고 있는 잠금을 보여주고 순환 종속성을 확인합니다. 트랜잭션 (1)이 희생자로 선택되어 롤백됩니다.
예방 및 해결 전략
교착 상태를 처리하는 가장 좋은 방법은 예방하는 것입니다. 발생한다면, 강력한 해결 전략을 갖는 것이 중요합니다.
예방 전략:
-
일관된 잠금 순서: 가장 효과적인 전략입니다. 모든 트랜잭션이 미리 결정된 일관된 순서로 리소스에 대한 잠금을 획득하도록 합니다. 앞서 설명한 전자상거래 예에서,
Orders
와Products
에 대한 잠금이 필요한 모든 트랜잭션이 항상Orders
잠금을 먼저 획득하고 그런 다음Products
잠금을 획득한다면 순환 대기가 형성될 수 없습니다.예시 (일관된 잠금 순서):
-- 트랜잭션 A BEGIN TRANSACTION; UPDATE Orders SET status = 'processed' WHERE order_id = 123; UPDATE Products SET stock = stock - 1 WHERE product_id = 456; COMMIT; -- 트랜잭션 B BEGIN TRANSACTION; UPDATE Orders SET last_updated = NOW() WHERE order_id = 789; -- 다른 주문 UPDATE Products SET stock = stock - 1 WHERE product_id = 101; COMMIT; -- 만약 트랜잭션 B도 order_id 123과 product_id 456이 필요하다면: BEGIN TRANSACTION; -- 항상 주문 잠금을 먼저 획득 UPDATE Orders SET status = 'shipped' WHERE order_id = 123; -- 그 다음 상품 잠금 획득 UPDATE Products SET stock = stock - 1 WHERE product_id = 456; COMMIT;
이러한 일관된 순서는 간단한 A가 B를 기다리고 B가 A를 기다리는 시나리오의 가능성을 제거합니다.
-
짧은 트랜잭션: 트랜잭션을 가능한 한 짧고 간결하게 유지합니다. 트랜잭션이 잠금을 보유하는 시간이 짧을수록 교착 상태가 발생할 창이 작아집니다. 트랜잭션 내에서 사용자 상호 작용이나 외부 API 호출을 피하십시오.
-
낮은 격리 수준 (주의해서 사용):
Serializable
과 같은 높은 격리 수준은 더 강력한 일관성을 보장하지만, 더 많은 잠금을 획득하고 더 오래 보유하여 교착 상태 가능성을 높입니다.READ COMMITTED
또는REPEATABLE READ
와 같은 낮은 수준은 교착 상태 빈도를 줄일 수 있지만, 비반복 읽기 또는 팬텀 읽기와 같은 다른 동시성 문제를 야기할 수 있습니다. 애플리케이션의 일관성 요구 사항을 충족하는 가장 낮은 격리 수준을 선택하십시오. -
SELECT FOR UPDATE
현명하게 사용: 동일한 트랜잭션 내에서 나중에 수정할 데이터를 읽을 때 명시적으로 행을 잠급니다. 이는 다른 트랜잭션이 해당 행을 수정하는 것을 방지하여 교착 상태로 이어질 수 있는 읽기-수정-쓰기 충돌을 피합니다.예시 (
SELECT FOR UPDATE
):BEGIN TRANSACTION; SELECT stock FROM Products WHERE product_id = 456 FOR UPDATE; -- 즉시 행 잠금 -- ... 계산 수행 ... UPDATE Products SET stock = new_stock WHERE product_id = 456; COMMIT;
-
인덱스 최적화: 제대로 인덱싱된 테이블은 특정 행이나 범위를 테이블 수준 잠금으로 확대하지 않고 더 효율적으로 위치하고 잠글 수 있도록 합니다. 이는 잠금의 범위와 기간을 줄여 교착 상태 가능성을 낮춥니다.
해결 전략 (교착 상태 발생 시):
복잡하고 동시성이 높은 시스템에서는 예방 조치에도 불구하고 간혹 교착 상태가 발생할 수 있습니다.
-
재시도 로직: 이것이 가장 일반적이고 효과적인 애플리케이션 수준 전략입니다. 트랜잭션이 교착 상태 희생자로 선택되어 롤백되면, 애플리케이션은 교착 상태 오류를 catch(예: 직렬화 가능한 트랜잭션 실패에 대한 SQLSTATE
40001
또는 특정 RDBMS 드라이버 오류)하고 전체 트랜잭션을 재시도해야 합니다. 무한 루프를 방지하기 위해 약간의 지연과 제한된 횟수의 재시도를 구현하십시오.예시 (Python with SQLAlchemy):
from sqlalchemy.exc import OperationalError import time def perform_transaction_with_retry(session, operation, max_retries=5, initial_delay=0.1): retries = 0 while retries < max_retries: try: session.begin_nested() # 중첩 트랜잭션의 경우, 또는 최상위 트랜잭션의 경우 session.begin() operation(session) session.commit() return except OperationalError as e: # 교착 상태 관련 오류 코드 확인 (예: MySQL 1213) if 'deadlock' in str(e).lower() or e.orig.args[0] == 1213: # MySQL 특정 session.rollback() retries += 1 print(f"Deadlock detected, retrying... (Attempt {retries})") time.sleep(initial_delay * (2 ** (retries - 1))) # 지수 백오프 else: session.rollback() raise # 다른 작동 오류 다시 발생 except Exception: session.rollback() raise raise Exception("Transaction failed after multiple retries due to deadlock.") def update_order_and_product(session, order_id, product_id, quantity): # 일관된 잠금 순서 보장: 먼저 Orders, 다음 Products session.execute( text("UPDATE Orders SET status = 'processing' WHERE id = :order_id"), {'order_id': order_id} ) session.execute( text("UPDATE Products SET stock = stock - :quantity WHERE id = :product_id"), {'product_id': product_id, 'quantity': quantity} ) # 사용법 # with Session() as session: # perform_transaction_with_retry(session, lambda s: update_order_and_product(s, 123, 456, 1))
-
외부 잠금 관리 (고급/분산 시스템): 마이크로서비스 아키텍처 또는 고도로 분산된 시스템에서는 때때로 애플리케이션 수준 잠금(예: Redis 또는 ZooKeeper 사용)이 데이터베이스를 사용하기 전에 중요 리소스에 대한 접근을 직렬화하는 데 사용됩니다. 이는 동시성 제어 책임을 이동시키지만, 자체적인 복잡성과 단일 실패 지점을 도입합니다. 일반적으로 데이터베이스 중심의 교착 상태 문제에는 과도합니다.
신중한 트랜잭션 설계와 강력한 재시도 메커니즘을 결합하면 고동시성 웹 애플리케이션에 대한 교착 상태의 영향을 크게 줄일 수 있습니다.
결론
고동시성 웹 애플리케이션에서 지속적인 문제인 데이터베이스 교착 상태는 궁극적으로 해결 가능한 문제입니다. 잠금에 대한 순환 대기를 특히 강조하는 근본 원인을 이해하고, 일관된 잠금 순서, 짧은 트랜잭션, 신중한 격리 수준 사용과 같은 사전 예방적 조치를 구현함으로써 개발자는 발생 빈도를 크게 줄일 수 있습니다. 또한, 잘 설계된 애플리케이션 수준 재시도 메커니즘은 불가피하게 발생하는 간헐적인 교착 상태를 우아하게 처리하여 시스템 탄력성과 원활한 사용자 경험을 보장하는 데 중요합니다. 교착 상태 관리를 숙달하는 것은 현대 트래픽의 압력을 견딜 수 있는 확장 가능하고 안정적인 웹 애플리케이션을 구축하는 데 있어 중요한 지표입니다.