포스트

락과 트랜잭션 경계 분리: 좌석 예약을 구현하며 배운 실무 패턴

락과 트랜잭션 경계 분리: 좌석 예약을 구현하며 배운 실무 패턴

락과 트랜잭션 경계 분리: 직접 겪어보며 정리한 실무 패턴

좌석 예약 기능을 만들면서 락과 트랜잭션의 경계를 어디에 둬야 하는지 한참 고민했다.

처음에는 @Transactional 메서드 안에서 분산 락을 잡고 푸는 식으로 짰다. 그런데 코드 리뷰를 받고 구조를 다시 뜯어보니, 이 방식이 왜 문제가 되는지 그리고 보통은 어떤 패턴을 쓰는지가 눈에 들어왔다.

1. 문제의 시작: 트랜잭션 안에서 락을 잡아도 될까?

처음 구현은 대략 이런 순서였다.

  • @Transactional 메서드 진입
  • 분산 락 획득
  • 좌석 조회 및 예약 처리
  • 락 해제
  • 트랜잭션 커밋

얼핏 문제없어 보였다. 그런데 Spring 트랜잭션이 프록시 기반으로 동작한다는 사실을 떠올리자 흐름이 다르게 보이기 시작했다.

실제 실행 순서를 따라가 보면 이렇다.

  • 프록시 진입과 동시에 트랜잭션 시작
  • 그 이후에 락 획득 시도
  • 메서드 종료 시점에 트랜잭션 커밋

결국 락을 기다리는 동안 트랜잭션은 이미 열려 있었고, 커밋되기도 전에 락이 풀릴 수 있는 구조였다. 내가 그리던 흐름과는 분명 달랐다.

2. 왜 이 구조가 문제가 될 수 있을까?

2-1. 트랜잭션은 생각보다 비싼 자원

트랜잭션이 열리면 DB 커넥션을 점유하고 MVCC 스냅샷을 유지한다. 여기에 락 대기 시간까지 얹히면 커넥션을 붙잡는 시간이 길어진다. 그만큼 전체 처리량은 떨어지고, 타임아웃이나 데드락 위험도 함께 올라간다.

그래서 실무에서는 자주 이런 원칙을 이야기한다.

트랜잭션은 최대한 짧게 유지해야 한다

이 기준으로 보면 락을 기다리는 시간까지 트랜잭션에 밀어 넣는 건 좋은 선택이 아니었다.

2-2. 분산 락과 트랜잭션의 책임은 다르다

두 개념은 맡은 역할이 서로 다르다. 트랜잭션은 데이터 무결성을 보장하고, 분산 락은 비즈니스 레벨의 동시성을 제어한다. 책임이 다르면 코드 구조도 나뉘는 게 자연스럽다.

1
2
3
4
5
[락을 담당하는 계층]
        ↓
[트랜잭션을 담당하는 계층]
        ↓
[도메인 로직]

이건 취향의 문제가 아니라, 실무에서 반복적으로 쓰이는 패턴에 가깝다는 걸 알게 됐다.

3. 실무에서 많이 쓰이는 구조

3-1. Facade / UseCase + TxService 패턴

내가 택한 구조는 이렇다.

1
2
3
Facade / UseCase (분산 락 획득 및 해제)
        ↓
TxService (@Transactional)

락은 트랜잭션 바깥에서 잡고, 트랜잭션은 그 락 안에서 열어 최대한 짧게 끝낸다. 커밋이 끝난 뒤에야 락을 푼다.

이 방식은 예약, 결제, 재고 도메인에서 실제로 많이 쓴다. 코드 리뷰를 할 때도 의도를 읽어내기 쉽다.

3-2. AOP 기반 분산 락은 왜 조심해야 할까?

어노테이션 기반으로 락을 처리하는 방식도 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.