같이 스터디하는 사람이 예약시스템을 만든다.
좌석 예약 시스템(가게id, 년월일, 시작시간~종료시간, 좌석번호)
1. 동시에 같은 자리에 예약 경쟁하는 경우
2. 동시간에 같은 좌석인데 시작시간이랑 종료시간이 묘하게 다른데 예약이 겹치는 경우
동시성 문제가 발생할 것 같으니 고려해야한다고 했다.
❌ 좌석 예약은 충돌 가능성 높아서 대부분 비관적 락이 적합다고 말했는데
스터디원은 동시성 문제에 대한 해결책으로
- redis로 앞에서 예약을 확인하고
- DB에서는 where절로 update, insert
- 를 하기떄문에 비관적락이 필요없다고 말하는 것이었다.
그래서 비관적 락이 필요없다고 하는 것에 대해서 설명 해주기 위해, 정리하는 글이다.
✅ 전제: 좌석 예약 시스템 (멀티 서버 + 멀티 스레드 환경)
- 주요 키: 가게ID, 예약일, 시작시간, 종료시간, 좌석번호
- 동시에 여러 명이 같은 좌석/시간에 예약 요청 가능
✅ 문제 1: 같은 자리에 동시에 예약 요청
- 사용자 A, B가 동일한 좌석에 같은 시간 요청
- 선착순 한 명만 예약 성공해야 함 → 동시성 제어 필요
✅ 문제 2: 시간 겹치는 예약
- A가 14:00 ~ 15:00 예약 중
- B가 14:30 ~ 15:30을 예약하면 부분적으로 겹침
- 시간 조건에 따라 충돌 여부 판단 필요
- Java Code 에서 비관적락, 낙관적락
- DB에서 비관적락, 낙관적락
- OS에서 비관적락, 낙관적락

💡 실무에서 선택 기준
상황 | 추천 락 방식 |
계좌 이체, 재고 감소 | 비관적 락 (정확성 최우선) |
게시글 좋아요, 읽기 많은 통계 | 낙관적 락 (충돌 드묾, 성능 중요) |
실시간 동시성 높은 CPU 처리 | 낙관적 락 (CAS 기반 lock-free 처리) |
멀티스레드 접근이 심한 Java 객체 | 비관적 락 (synchronized) 또는 StampedLock |
✅ 1. Java Code에서의 비관적 락 vs 낙관적 락
🔒 비관적 락 (Pessimistic Lock)
synchronized (lockObj) {
// 공유 자원 접근
}
- 환경: 멀티스레드 환경에서 synchronized, ReentrantLock 등 사용
- 특징:
- 자원을 미리 잠금해서 다른 스레드의 접근을 차단
- 락을 잡는 동안 다른 스레드는 대기
- 유리한 상황:
- 경합이 잦고, 데이터 충돌 가능성이 높은 경우
✅ 낙관적 락 (Optimistic Lock)
AtomicInteger count = new AtomicInteger(0);
count.compareAndSet(expected, update); // CAS 연산
- 환경: AtomicInteger, Compare-And-Swap(CAS) 기반
- 특징:
- 충돌이 없다고 낙관하고 먼저 접근
- 마지막에 변경 여부 확인(CAS) 후 충돌 시 다시 시도
- 유리한 상황:
- 충돌 가능성이 낮고, 재시도가 성능에 큰 영향을 주지 않는 경우
✅ 2. DB에서의 비관적 락 vs 낙관적 락
🔒 비관적 락 (Pessimistic Lock)
SELECT * FROM account WHERE id = 1 FOR UPDATE;
- 환경: RDBMS (PostgreSQL, MySQL 등)에서 SELECT ... FOR UPDATE
- 특징:
- 읽은 데이터를 즉시 잠금 → 다른 트랜잭션은 대기하거나 실패
- 트랜잭션 커밋 전까지 다른 접근 차단
- 유리한 상황:
- 동시 수정이 빈번하고 충돌 시 비용이 큰 경우
- 예: 은행 계좌 이체, 재고 차감
✅ 낙관적 락 (Optimistic Lock)
UPDATE account SET balance = 100, version = 2 WHERE id = 1 AND version = 1;
@Version
private Long version;
- 환경: 주로 JPA/Hibernate에서 @Version 활용
- 특징:
- 업데이트 시점에 버전 번호 비교 → 충돌 시 실패
- 트랜잭션 중 락을 잡지 않음
- 유리한 상황:
- 읽기 많고 쓰기 적은 환경, 충돌 가능성이 낮은 경우
- 예: 게시글 조회/수정, 통계 입력 등
✅ 3. 운영체제(OS)에서의 비관적 락 vs 낙관적 락
🔒 비관적 락
- 환경: 커널 수준의 mutex, semaphore, spinlock
- 사용 예:
- pthread_mutex_lock(), semaphore_wait() 등
- 특징:
- 자원 선점 → 다른 프로세스/스레드는 대기
- 커널 스케줄러 관여
- 유리한 상황:
- 공유 자원에 대한 충돌이 자주 발생하는 저수준 병렬 처리
- 예: 디바이스 접근, 공유 메모리 쓰기 등
✅ 낙관적 락
- 환경: 사용자 공간에서의 CAS(CPU 명령어 레벨)
- 사용 예:
- x86의 cmpxchg (compare-and-swap) 명령어 사용
- Java의 Unsafe.compareAndSwapInt() 등도 내부적으로 사용
- 특징:
- 자원을 잠그지 않고, 마지막에 변경 여부 확인
- 충돌 시 재시도 (busy-wait 방식 가능)
- 유리한 상황:
- 경합이 적은 고성능 처리
- 예: lock-free queue, atomic counter
🔍 질문 1. Redis에서 예약 체크하고 예약 처리 → 비관적 락
“Redis로 application에서 예약체크를 하고 처리” → 이것은 Application 레벨의 "비관적 락"으로 볼 수 있어요.
✅ 설명
- 예를 들어 Redis의 SETNX(Lock)나 Redisson의 tryLock() 등을 사용해서
- seat:{storeId}:{date}:{seatId} 같은 키로 락을 걸고
- 그동안 다른 요청은 대기하거나 실패 처리
- 이건 미리 락을 선점하고 다른 트래픽을 막는 → 비관적 락(Pessimistic Lock) 방식
- 락을 걸고 → 예약 가능성 체크 → 예약 → 락 해제
if (redissonClient.getLock(key).tryLock(3, 1, TimeUnit.SECONDS)) {
try {
// 예약 가능 여부 확인 + 처리
} finally {
lock.unlock();
}
}
🔍 질문 2. DB에서 WHERE 조건으로 insert/update → 비관적 락
정확히는, 트랜잭션 안에서 "조건 체크 + insert/update"를 동시에 수행하면서 다른 트랜잭션의 진입을 막는다면 → DB 차원에서의 비관적 락이라고 볼 수 있어요.
SELECT * FROM reservation
WHERE store_id = ? AND seat_id = ? AND date = ?
AND (
(start_time < :endTime AND end_time > :startTime)
)
FOR UPDATE;
@Lock(LockModeType.PESSIMISTIC_WRITE)
Reservation findReservationBySeatAndTime(...)
- 이 쿼리는 해당 좌석의 예약 중 겹치는 시간대가 있는지 확인하면서,
- 동시에 해당 row를 락 걸기(FOR UPDATE) → 다른 트랜잭션 진입 방지
방식 | 장점 | 단점 | 추천 상황 |
🔐 Redis Lock (Application 레벨 비관적 락) |
빠르고 분산 락 가능 | 락 해제 실패, 복잡한 장애 처리 | 서버 여러 대일 때, 빠른 응답 원할 때 |
🔒 DB Lock (SELECT FOR UPDATE) |
트랜잭션 내에서 처리 일관성 보장 | 성능 병목 발생 가능, 락 경합 | 충돌 가능성 높고, 데이터 정확성이 중요할 때 |
🍀 낙관적 락 (@Version) | 락 없이 빠름, 스케일러블 | 충돌 발생 시 재시도 필요 | 충돌 가능성 낮고, 성능 우선일 때 |
🔹 단일 서버
- 서버 인스턴스가 1대인 경우
- 즉, 동시 접근이 모두 같은 JVM 내에서 처리됨
- 이 경우 애플리케이션 락(Redis 등)보다 DB 트랜잭션으로 제어하는 게 더 간단하고 안전할 수 있음
🔹 트랜잭션 중요
- “한 번의 요청에서 예약 가능 여부 체크 → 예약 등록”까지가 꼭 묶여야만 하는 경우
- 예: 중간에 누군가 선점하면 절대 안 됨 → 원자성이 필수
즉, 이 모든 과정이 하나의 트랜잭션으로 묶여 있어야 한다면
→ DB 트랜잭션 + FOR UPDATE가 가장 확실한 방법입니다
✅ DB 트랜잭션 + SELECT FOR UPDATE 예시
@Transactional
public void reserveSeat(UUID storeId, LocalDate date, int seatNum, LocalTime start, LocalTime end) {
// 겹치는 예약이 있는지 조회하며 락을 건다
List<Reservation> existing = reservationRepository.findConflictingReservationsWithLock(
storeId, date, seatNum, start, end
);
if (!existing.isEmpty()) {
throw new RuntimeException("이미 예약된 시간입니다");
}
// 없다면 예약 생성
reservationRepository.save(new Reservation(...));
}
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT r FROM Reservation r WHERE r.storeId = :storeId AND r.date = :date AND r.seatNum = :seatNum AND r.startTime < :endTime AND r.endTime > :startTime")
List<Reservation> findConflictingReservationsWithLock(...);
✅ 이 방법이 유리한 경우?
서버 1대 | 동시성 문제 범위가 JVM 내 → DB 트랜잭션이면 충분 |
락 분산 불필요 | Redis 안 써도 됨, 단순 |
처리 순서 매우 중요 | 한 트랜잭션 안에서 "체크 → 등록" 해야 함 |
✅ 반대로 Redis 분산 락이 필요한 경우?
서버가 여러 대 (수평 확장) | JVM이 서로 달라 락 공유 불가능 → Redis로 분산 락 필요 |
처리 속도 중요 | DB 락보다 빠름 (단, 정확도는 낮아질 수 있음) |
ReentrantLock이 비관적 락인 이유
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 🔒 락 획득
try {
count++;
} finally {
lock.unlock(); // 🔓 반드시 해제
}
}
public int getCount() {
return count;
}
// 테스트용
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("최종 카운트: " + counter.getCount()); // 2000이어야 함
}
}
✅ 비관적 락
락을 잡고 시작
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 🔒 먼저 락을 선점
try {
// 공유 자원 처리 (임계 구역)
} finally {
lock.unlock(); // 🔓 락 해제
}
- ReentrantLock은 임계 구역(critical section)에 진입하기 전에 락을 먼저 잡습니다
- 이 락은 다른 스레드가 접근하는 걸 아예 막아버립니다
- 즉, 다른 스레드는 락이 풀릴 때까지 기다려야 함
✅ 낙관적 락
락을 선점하지 않음
AtomicInteger count = new AtomicInteger(0);
while (true) {
int current = count.get();
if (count.compareAndSet(current, current + 1)) {
break;
}
// 실패 시 다시 시도
}
- 여기선 락 없이 진행하다가 마지막에 값이 바뀌었는지 확인하고 충돌 시 재시도
- 이게 낙관적(lock-free) 철학
'학습 기록 (Learning Logs) > Today I Learned' 카테고리의 다른 글
정렬 (0) | 2025.03.26 |
---|---|
원자성 (0) | 2025.03.25 |
레디스 pub/sub (0) | 2025.03.24 |
✨ Operating Systems: Three Easy Pieces ✨ - 개요 (0) | 2025.03.23 |
머신러닝 (0) | 2025.03.21 |