🛒📦재고시스템으로 알아보는 동시성 이슈 해결 방법
Review
재고 감소 로직의 문제점
Race Condition
Race Condition은 여러 개의 프로세스 또는 스레드가 공유 자원에 동시 접근(변경) 할 때
발생하는 문제로, 실행 순서에 따라 예상치 못한 결과가 발생하는 상태
해결 방법
- 하나의 Thread가 작업이 완료된 이후에, 다른 스레드가 데이터에 접근할 수 있도록 해야 함
해결 방법1의 문제점
- 재고 감소 로직의 문제점인 Race Condition을 해결하기 위해 첫 번째 해결 방법으로
자바 synchronized 키워드를 사용했으나, synchronized는 몇 가지 한계점을 갖는다.
synchronizedd의 한계
- 실제 운영 서비스에서는 대부분 스케일 아웃(Scale Out)을 통해 여러 대의 서버를 실행하고,
로드밸런싱을 통해 부하를 분산하여 서비스를 제공한다.
synchronized는 하나의 프로세스 내에서만 동기화를 보장하므로
다중 서버 환경에서 동기화를 보장할 수 없다.
✨ 해결 방법2 -MySQL Lock 활용
🔒 1. Pessimistic Lock(비관적 락)
- 트랜잭션이 데이터에 접근할 때, 다른 트랜잭션이 동시에 접근하지 못하도록 선점적으로 락을 거는 방식
- 실제 데이터에 직접적으로 락을 거는 방식
- 다른 트랜잭션은 락이 해제되기 전까지 데이터를 읽을 수 없음
특징
- 데이터 충돌을 피하기 위해 먼저 락을 걸고 작업을 진행
- 일반적으로
Exclusive Lock(X Lock)을 사용해 다른 트랜잭션의 Read/Write을 방지 - 배타적 락
- 충돌이 빈번한 환경에서 유용(은행 계좌이체, 재고 관리)
- 락을 통해 업데이트를 제어하므로 데이터 정합성이 보장
단점
- 락이 오래 유지될 경우 동시성 저하 - 성능 저하
- 데드락(Deadlock) 발생 가능성 존재
사용 예시
FOR UPDATE는 해당 행에 X LOCK을 걸어 다른 트랜잭션의 Read/Write을 할 수 없도록 함
BEGIN;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
UPDATE orders SET status = 'CONFIRMED' WHERE id = 1;
COMMIT;
🔒 2. Optimistic Lock(낙관적 락)
- 데이터를 읽고 수정할 때까지 락을 걸지 않고, 수정 시점에서 충돌 여부를 확인
- 실제로 락을 이용하지 않고 버전을 이용해 데이터 정합성을 맞춤
특징
- 트랜잭션이 데이터를 읽고 수정할 때, 다른 트랜잭션이 변경하지 않았는지 확인 후 업데이트
- 읽은 버전에 수정사항이 생겼을 경우 application에서 다시 읽은 후 작업 수행
- 일반적으로
버전번호 또는 타임스탬프를 활용해 변경 여부를 확인
- 동시성이 높은 환경에서 유용하며, 충돌 가능성이 낮은 경우 적합
단점
- 충돌이 발생하면 다시 데이터를 읽고 재시도해야 함
사용 예시(Java + JPA)
- 데이터 변경 시
version값이 증가하며, 업데이트 시 기존 버전과 비교해 충돌 감지
@Entity
public class Order{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Integer version;
private String status;
}
Order order = orderRepository.findById(1L).orElseThrow();
order.setStatus("CONFIRMED");
orderRepository.save(order);
🔒 3. Named Lock(네임드 락)
- 데이터베이스에서 특정한 이름을 가진 락을 획득해 동시성 제어
- 이름을 가진 메타데이터 락
Pessimistic Lock은 row, table 단위지만 Named Lock은 메타데이터를 락킹
- 먼 소리임?
NamedLock 상세 설명
NamedLock은 특정 이름을 기반으로 동기화하는 락으로,
같은 이름을 가진 락을 획득하려는 스레드 간에 동기화를 보장하는 동시성 제어 방식
- 일반적으로
synchronized나 ReentrantLock은 객체 자체에 대한 락을 걸지만,
NamedLock은 이름을 기반으로 동기화
- 같은 이름을 락을 사용한 스레드는 동시에 실행되지 않고, 순차적으로 실행
- 분산 시스템(여러 서버 인스턴스)에서도 같은 이름의
NamedLock을 사용하면
동일한 자원에 대한 동기화를 보장할 수 있다.
특징
- 트랜잭션과 별개로 락을 걸 수 있음(특정 리소스를 보호할 때 사용)
- 주로 MySQL의
GET_LOCK을 이용
- 락을 걸 때 테이블의 특정 행이 아니라, 특정한 명칭(리소스 단위)에 락을 거는 방식
단점
- 락을 획득하지 못하면 대기하거나 재시도 로직 필요
- 트랜잭션 종료 시 락 자동 해제❌, 락을 걸고 해제하는 별도 로직이 필요
사용 예시(MySQL)
SELECT GET_LOCK('order_lock', 5);
UPDATE orders SET status = 'CONFIRMED' WHERE id = 1;
SELECT RELEASE_LOCK('order_lock');