본문 바로가기

학습 기록 (Learning Logs)/Today I Learned

원자성

원자성(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(...);