원자성(Atomicity)
트랜잭션의 핵심 속성 중 하나로,
"모든 작업이 전부 수행되거나, 전혀 수행되지 않아야 한다"는 원칙입니다.
즉, 조회 → 검증 → 저장이 하나의 불가분한 단위로 실행되어야
중간에 다른 스레드나 사용자에게 "틈"을 주지 않습니다.
https://cutewonny.tistory.com/entry/transaction
transaction
공통 질문 출처:https://www.youtube.com/watch?v=sLJ8ypeHGlMtransaction?- 단일한 논리적인 작업 단위(a single logical unit of work)- 논리적인 이유로 여러 sql문을 단일 작업으로 묶어서, 나눠질 수 없도록 만듦- t
cutewonny.tistory.com
하나의 코드에
조회, 예약 추가가 있는 경우, 원자성이 보장되지 않는다.
public void reserveSeat(ReserveRequest request) {
// 1. 겹치는 예약 있는지 조회
List<Reservation> existing = reservationRepository.findByStoreIdAndDateAndSeatAndTime(...);
if (!existing.isEmpty()) {
throw new RuntimeException("이미 예약됨");
}
// 2. 예약 추가
reservationRepository.save(new Reservation(...));
}
겉보기에 하나의 메서드에서 조회 + 저장이 모두 있어 보이지만...
❌ 트랜잭션이 없다면?
동작 시나리오 | 결과 |
사용자 A가 예약 조회 | 예약 없음 ✅ |
바로 직후 사용자 B도 예약 조회 | 예약 없음 ✅ |
A가 예약 저장 | 성공 🟢 |
B도 예약 저장 | 성공 😱 → 중복 예약 발생!! |
→ 즉, 경합 상황에서 둘 다 조회할 땐 예약이 없었지만, 동시에 저장하는 순간 문제가 발생함
✅ 해결 방법: 트랜잭션 + 비관적 락
반드시 조회 + 저장은 트랜잭션 내에서 락을 통해 동시성 제어해야 합니다.
❗️조회 + 저장을 함수로 나누면 생기는 문제 -> 경쟁 조건(Race Condition)
public boolean isReservable(...) {
List<Reservation> existing = repository.findConflictingReservations(...);
return existing.isEmpty();
}
public void saveReservation(...) {
repository.save(new Reservation(...));
}
if (isReservable(request)) {
saveReservation(request);
}
- 위 코드는 조회 → 저장이 동시에 이뤄진 다른 사용자 요청과 충돌할 수 있다
- [A 사용자] isReservable() → true
- [B 사용자] isReservable() → true
- [A 사용자] saveReservation()
- [B 사용자] saveReservation() → 충돌 발생 (중복 저장)
⚠️ 잘못된 분리 방식 (트랜잭션 없이 각각 호출)
// Controller → Service
// 이런 식으로 따로따로 호출하면
reservationService.isReservable(request);
reservationService.saveReservation(request);
- ❌ 이건 트랜잭션이 쪼개져 있어서 원자성이 깨지고,
- 동시성 문제 발생 가능성이 매우 높아져요
🔐 해결 방법 1: 트랜잭션 안에서 묶기
@Service
public class ReservationService {
@Transactional
public void reserve(ReserveRequest request) {
if (!isReservable(request)) {
throw new RuntimeException("이미 예약된 좌석입니다.");
}
saveReservation(request);
}
public boolean isReservable(ReserveRequest request) {
return repository.findConflictingReservations(...).isEmpty();
}
public void saveReservation(ReserveRequest request) {
repository.save(...);
}
}
✅ 위 구조는 reserve()가 트랜잭션 범위이므로 원자성 보장됩니다.
🔒 해결 방법 2: 비관적 락까지 적용
✅ @Transactional + @Lock
@Transactional
public void reserveSeat(ReserveRequest request) {
// 비관적 락으로 조회
List<Reservation> existing = reservationRepository.findConflictingReservationsWithLock(...);
if (!existing.isEmpty()) {
throw new RuntimeException("이미 예약된 좌석입니다.");
}
reservationRepository.save(new Reservation(...));
}
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT r FROM Reservation r WHERE ...시간 겹침 조건...")
List<Reservation> findConflictingReservationsWithLock(...);
'학습 기록 (Learning Logs) > Today I Learned' 카테고리의 다른 글
개인 채팅, 단체 채팅 : 테이블 설계와 Kafka 토픽 설계 (0) | 2025.03.27 |
---|---|
정렬 (0) | 2025.03.26 |
비관적락, 낙관적락 (0) | 2025.03.25 |
레디스 pub/sub (0) | 2025.03.24 |
✨ Operating Systems: Three Easy Pieces ✨ - 개요 (0) | 2025.03.23 |