포스트

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

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

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

이번에 좌석 예약 기능을 구현하면서, 락과 트랜잭션의 경계를 어떻게 잡아야 하는지에 대해 다시 한 번 정리할 기회가 있었다.

처음에는 단순히 @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 기반 분산 락은 왜 조심해야 할까?

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

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