#Backend

데드락을 해결하려다, 락을 줄이게 된 이야기

데드락을 해결하려다, 락을 줄이게 된 이야기
01

Summary

데드락을 해결하기 위한 최고의 비법은 바로 '락을 거는 이유' 자체를 없애는 것이다

비관적 락 지옥에서 벗어나 단순 조회를 통해 동시성을 완전히 정복한 카카오헤어샵(여기어때)의 트러블슈팅 이야기

동시성 이슈가 많은 대용량 비동기 이벤트 환경에서 단순하게 도입한 비관적 락이 데드락이라는 거대한 부메랑으로 돌아온 원인을 진단하고 극복한 기록입니다. InnoDB의 행 락 매커니즘인 S-Lock과 X-Lock의 동작 방식을 파헤치며 문제의 본질이 락 제어 방식이 아닌 '잘못 설계된 데이터 구조'에 있음을 보여줍니다. 무작정 락을 걸기보다 불필요한 비즈니스 갱신을 지워 락을 최소화하는 근본적인 우회 설계법을 공유합니다.

  • 01중복 생성 방지를 위한 INSERT IGNORE 구문이 묵시적으로 S-Lock을 획득하여 FOR UPDATE와 충돌하며 발생한 데드락 로그 분석
  • 02레저 티켓 체크아웃 등의 여러 비즈니스 경로에서 발생하는 락 획득 순서 차이(ABBA 패턴)로 인한 교착 상태 트러블슈팅
  • 03유저 테이블 내 누적 카운터를 과감하게 제거하고, Step 테이블 실시간 조회를 통해 Hot Row에 쏠리는 쓰기 병목 해결
  • 04REQUIRES_NEW 트랜잭션 격리를 활용해 무거운 S-Lock이 유지되는 수명을 아주 짧게 단축
  • 05JPA 1차 캐시 불일치 상황을 극복하기 위해 EntityManager.refresh를 사용해 영속성 컨텍스트를 동기화한 세부 구현 디테일

RECOMMENDATION

동작하지도 않는 비관적 락 때문에 머리를 싸매고 있거나, 트랜잭션 롤백과 데드락 경고로 고통받는 주니어/시니어 백엔드 개발자들에게 정독을 추천합니다. 무조건적인 분산 락, 낙관적 락 도입을 고민하기 전 비즈니스 모델 수준에서 락의 필요성 자체를 해소하는 시니어 수준의 관점을 배울 수 있습니다.

The Problem

비동기 이벤트(주문, 이용완료, 취소)가 유저 단위로 동시에 인입되면서 데이터 유실 및 중복을 방지하기 위해 비관적 락을 도입했으나, 동시성 테스트 과정에서 데드락이 발생했습니다. 원인은 중복 키 검사용 `INSERT IGNORE`로 인해 잡히는 S-Lock과 이후 `FOR UPDATE`에 필요한 X-Lock 간의 승격 과정 중 충돌 및 두 로직 간 락 획득 순서 불일치(ABBA 패턴)였습니다.

The Solution

기존에 유저 테이블에서 관리하던 불필요한 이벤트 카운터 컬럼들을 제거하여 락이 유발되던 '핫 로우(Hot Row)'를 제거하고, 관계 테이블 조회를 통해 값을 실시간 계산하도록 구현을 변경했습니다. 또한 신규 데이터 생성 작업을 `REQUIRES_NEW` 전파 속성을 가진 별도의 트랜잭션으로 격리하여 락의 수명을 크게 단축하고, 비즈니스 흐름 전반의 락 획득 순서를 일관되게 단방향으로 통일했습니다.

The Result

동일한 조건의 동시성 테스트에서 데드락이 더 이상 재현되지 않는 정합성을 확보했습니다. 불필요하게 핫 로우에 가해지던 쓰기 비용과 긴 트랜잭션 대기 시간을 완전히 제거하여 안정적이고 멱등한 메시지 소비가 가능해졌습니다.

Trade-off

`REQUIRES_NEW` 트랜잭션 격리 방식을 사용함에 따라 기존 트랜잭션의 커넥션을 유지한 채 추가 커넥션을 소비하므로 커넥션 풀의 고갈 위험이 커집니다. 또한, 해당 트랜잭션에서 분리되어 반환된 엔티티는 준영속 상태가 되므로 이후 외부 트랜잭션에서 곧바로 상태를 수정할 수 없고 존재 여부 및 식별 확인 용도로만 한정적으로 사용해야 합니다.

03

Key Concepts

Concept · 01

InnoDB S-Lock (Shared Lock)

공유 락이라고 불리며, 여러 트랜잭션이 동일한 행을 동시에 읽을 수 있도록 허용하는 락 유형입니다. 그러나 다른 트랜잭션에서 이 행을 수정(X-Lock 획득)하는 것은 불가하기 때문에, 한 행에 대해 둘 이상의 트랜잭션이 동시에 S-Lock을 확보하고 서로 갱신(X-Lock 승격)을 기다리면 필연적으로 데드락이 일어납니다.

  • INSERT IGNORE 문이 중복 키(Unique Key)를 검증하는 과정에서 해당 행에 자동으로 S-Lock을 점유하는 특징 때문에 문제가 되었습니다.
  • S-Lock을 잡은 채 연이어 FOR UPDATE(X-Lock 필요)를 수행하면서 두 스레드가 서로의 공유 락 해제를 기다리는 상황이 발생했습니다.
Concept · 02

ABBA 데드락 패턴

복수의 공유 자원을 관리하는 시스템에서 서로 다른 트랜잭션들이 서로 다른 순서로 자원 락을 선점하려고 할 때 발생하는 전형적인 교착 패턴입니다. 트랜잭션 1이 A를 잠그고 B를 기다릴 때, 트랜잭션 2가 B를 잠그고 A를 기다리면서 영원히 순환 대기에 머무르게 됩니다.

  • 이벤트 로직은 '유저 상태'에서 '주문 단계' 순으로 잠갔으나, 특정 티켓 구매 경로는 역방향으로 잠그고 있어 데드락을 유발했습니다.
  • 모든 비즈니스 흐름이 항상 일관되게 '유저 상태 -> 주문 단계' 순으로만 락을 요청하게 함으로써 이 패턴을 깨뜨렸습니다.
Concept · 03

트랜잭션 전파 속성 REQUIRES_NEW

Spring의 트랜잭션 관리에서 제공하는 설정으로, 이미 진행 중인 활성 트랜잭션이 있어도 이를 잠시 일시 중단시키고 완전히 새롭고 독립된 물리적 DB 트랜잭션을 시작 및 커밋합니다.

  • 신규 유저 생성 단계에서 불필요하게 오래 잠기는 S-Lock의 가동 범위를 최소화하기 위해 독립적인 트랜잭션으로 분리했습니다.
  • 메시지 처리 수명과 생성 락의 유효 수명을 완벽히 격리함으로써 락 전파에 따른 락 경합 가능성을 대폭 하향했습니다.