
비관적 락 지옥에서 벗어나 단순 조회를 통해 동시성을 완전히 정복한 카카오헤어샵(여기어때)의 트러블슈팅 이야기
동시성 이슈가 많은 대용량 비동기 이벤트 환경에서 단순하게 도입한 비관적 락이 데드락이라는 거대한 부메랑으로 돌아온 원인을 진단하고 극복한 기록입니다. InnoDB의 행 락 매커니즘인 S-Lock과 X-Lock의 동작 방식을 파헤치며 문제의 본질이 락 제어 방식이 아닌 '잘못 설계된 데이터 구조'에 있음을 보여줍니다. 무작정 락을 걸기보다 불필요한 비즈니스 갱신을 지워 락을 최소화하는 근본적인 우회 설계법을 공유합니다.
동작하지도 않는 비관적 락 때문에 머리를 싸매고 있거나, 트랜잭션 롤백과 데드락 경고로 고통받는 주니어/시니어 백엔드 개발자들에게 정독을 추천합니다. 무조건적인 분산 락, 낙관적 락 도입을 고민하기 전 비즈니스 모델 수준에서 락의 필요성 자체를 해소하는 시니어 수준의 관점을 배울 수 있습니다.
비동기 이벤트(주문, 이용완료, 취소)가 유저 단위로 동시에 인입되면서 데이터 유실 및 중복을 방지하기 위해 비관적 락을 도입했으나, 동시성 테스트 과정에서 데드락이 발생했습니다. 원인은 중복 키 검사용 `INSERT IGNORE`로 인해 잡히는 S-Lock과 이후 `FOR UPDATE`에 필요한 X-Lock 간의 승격 과정 중 충돌 및 두 로직 간 락 획득 순서 불일치(ABBA 패턴)였습니다.
기존에 유저 테이블에서 관리하던 불필요한 이벤트 카운터 컬럼들을 제거하여 락이 유발되던 '핫 로우(Hot Row)'를 제거하고, 관계 테이블 조회를 통해 값을 실시간 계산하도록 구현을 변경했습니다. 또한 신규 데이터 생성 작업을 `REQUIRES_NEW` 전파 속성을 가진 별도의 트랜잭션으로 격리하여 락의 수명을 크게 단축하고, 비즈니스 흐름 전반의 락 획득 순서를 일관되게 단방향으로 통일했습니다.
동일한 조건의 동시성 테스트에서 데드락이 더 이상 재현되지 않는 정합성을 확보했습니다. 불필요하게 핫 로우에 가해지던 쓰기 비용과 긴 트랜잭션 대기 시간을 완전히 제거하여 안정적이고 멱등한 메시지 소비가 가능해졌습니다.
Trade-off
`REQUIRES_NEW` 트랜잭션 격리 방식을 사용함에 따라 기존 트랜잭션의 커넥션을 유지한 채 추가 커넥션을 소비하므로 커넥션 풀의 고갈 위험이 커집니다. 또한, 해당 트랜잭션에서 분리되어 반환된 엔티티는 준영속 상태가 되므로 이후 외부 트랜잭션에서 곧바로 상태를 수정할 수 없고 존재 여부 및 식별 확인 용도로만 한정적으로 사용해야 합니다.
공유 락이라고 불리며, 여러 트랜잭션이 동일한 행을 동시에 읽을 수 있도록 허용하는 락 유형입니다. 그러나 다른 트랜잭션에서 이 행을 수정(X-Lock 획득)하는 것은 불가하기 때문에, 한 행에 대해 둘 이상의 트랜잭션이 동시에 S-Lock을 확보하고 서로 갱신(X-Lock 승격)을 기다리면 필연적으로 데드락이 일어납니다.
복수의 공유 자원을 관리하는 시스템에서 서로 다른 트랜잭션들이 서로 다른 순서로 자원 락을 선점하려고 할 때 발생하는 전형적인 교착 패턴입니다. 트랜잭션 1이 A를 잠그고 B를 기다릴 때, 트랜잭션 2가 B를 잠그고 A를 기다리면서 영원히 순환 대기에 머무르게 됩니다.
Spring의 트랜잭션 관리에서 제공하는 설정으로, 이미 진행 중인 활성 트랜잭션이 있어도 이를 잠시 일시 중단시키고 완전히 새롭고 독립된 물리적 DB 트랜잭션을 시작 및 커밋합니다.




