락과 트랜잭션 경계 분리: 좌석 예약을 구현하며 배운 실무 패턴
락과 트랜잭션 경계 분리: 좌석 예약을 구현하며 배운 실무 패턴
락과 트랜잭션 경계 분리: 직접 겪어보며 정리한 실무 패턴
이번에 좌석 예약 기능을 구현하면서, 락과 트랜잭션의 경계를 어떻게 잡아야 하는지에 대해 다시 한 번 정리할 기회가 있었다.
처음에는 단순히 @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 라이센스를 따릅니다.