들어가기 전..
1. 이전 게시글과 이어지는 게시글이므로 프로젝트 환경 및 세팅은 아래 링크에서 확인해주세요!
2. 전체 코드는 아래의 Git에서 확인 가능합니다.
지난 게시글에서는 레이스 컨디션이 무엇이고 어떻게 해결하는지를 알아보았다!
해결 방법으로는 데이터에 한 개의 스레드만 접근이 가능하도록 하면 된다라는 것!
목표
- 자바에서 지원하는 synchronized로 해결하기
- @Transactional의 동작방식에 대해 알아보기
- synchronized의 문제점 알아보기
synchronized 사용하기
@Transactional
public synchronized void decrease(Long id, Long quantity){
/* 1 재고 확인
2 재고 감소
3 갱신 값 저장
*/
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
- 기존 메소드에 synchronized 선언
결과
=> 실패
왜 그럴까?
이유 : @Transactional 어노테이션의 동작방식 때문
@Transactional 의 동작방식
- 스프링에서는 @Transactional Annotation을 이용하면 우리가 만든 클래스를 래핑한 클래스를 새로 만들어 실행하게 된다.
- 트랜잭션을 시작한 후에 메소드를 호출하고, 메소드 실행이 종료가 된다면 트랜잭션을 종료하게 된다. 문제는 여기서 발생한다.
- 트랜잭션 종료 시점에 데이터베이스에 업데이트를 하는데, 실제 데이터베이스가 업데이트 되기전에 다른 Thread가 해당 메소드를 호출할 수 있다.
- 그렇게 되면 다른 Thread는 갱신되기 전에 값을 가져가서 이전과 동일한 문제가 발생하는 것이다.
=> 따라서 이번 예제에서는 @Transactional을 주석처리 후 테스트 케이스를 실행한다.
결과
=> 성공!
그러나 문제가 있음
synchronized 의 문제점
- 자바의 synchronized는 하나의 프로세스 안에서만 보장이 된다.
- 서버의 1대일 때 데이터 접근을 서버가 1대만 해서 괜찮지만 서버가 2대 혹은 그 이상일 경우 데이터의 접근을 여러 대에서 할 수 있다.
- 예를 들어 서버 1에서 10시에 재고감소 로직을 시작하고, 10시 5분에 종료하게 된다고 가정해보자
- 그렇게 되면 서버2에서 10시에서 10시 5분 사이에 갱신되지 않은 재고를 가져가서 새로운 값으로 갱신할 수 있게 된다.
- synchronized는 각 프로세스 안에서 보장이 되기 때문에 결국 여러 스레드에서 동시에 데이터에 접근을 할 수 있게 되면서 레이스 컨디션이 발생하게 된다.
- 실제 운영 중인 서비스는 대부분 2대 이상의 서버를 사용하기 때문에 실무에서 synchronized는 거의 사용하지 않느다.
그럼 어떻게 해결을 해야 할까?
MySQL이 지원해주는 방법으로 레이스 커디션을 해결할 수 있다.
투비 컨티뉴 ...!!