여러 스레드 혹은 프로세스가 동시에 같은 자원에 접근하거나 수정할려고 할 때, 예상치 못한 결과를 발생하는 문제를 말합니다.
예제에서 사용한 기술 스택
package BaekGwa.ConcurrencyIssue.domain.item.service;
//imports
@SpringBootTest
class ItemServiceImplV1Test {
@Autowired
private ItemServiceImplV1 itemService;
@Autowired
private ItemRepository itemRepository;
@BeforeEach
public void init() {
NewItem newItem = new NewItem("상품A", 1000L, 100L);
itemService.RegisterItem(newItem);
}
@AfterEach
public void clear() {
itemRepository.deleteAll();
}
@Test
void 단일_구매_요청() {
BuyItem ItemA = new BuyItem("상품A", 1L);
Boolean isSuccess = itemService.BuyItem(ItemA);
Item findItem = itemRepository.findAllByName("상품A");
assertEquals(isSuccess, true);
assertEquals(99, findItem.getStock());
}
@Test
void 다중_구매_요청() throws InterruptedException {
int threadCount = 100;
ExecutorService es = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
es.submit(() -> {
BuyItem ItemA = new BuyItem("상품A", 1L);
Boolean isSuccess = itemService.BuyItem(ItemA);
assertEquals(isSuccess, true);
});
}
es.shutdown();
es.awaitTermination(10, TimeUnit.SECONDS);
Item findItem = itemRepository.findAllByName("상품A");
assertEquals(0, findItem.getStock());
}
}
itemService
의 BuyItem
메서드로 실행 된다Fail
되었다.synchronized
키워드를 사용해서, 여러 스레드에서 한번에 접근 할 수 없는 메서드로 만들어 주면 된다. @Transactional
@Override
public synchronized Boolean buyItem(BuyItem buyItem) {
try {
//검증 로직
//금액 검증
//상품 검증
//서비스 로직
return true;
} catch (Exception e) {
log.error("구매 실패. Message = {}", e.getMessage() + e.getCause());
return false;
}
}
@Transactional
의 비밀에 숨겨져 있다.@Transactional
과 synchronized 키워드
를 병행하여 사용에 문제가 발생하여 다른 방법을 찾아야 된다.Pessimistic Lock
에 대해서 먼저 확인해 보겠습니다.@Lock(LockModeType.PESSIMISTIC_WRITE)
Annnotaion을 JPA 메서드에 걸어주면 됩니다.@Repository
public interface ItemRepository extends JpaRepository<Item, Long> {
~~기존코드~~
@Lock(LockModeType.PESSIMISTIC_WRITE)
Item findUseLockByName(String name);
}
@Transactional
을 통한 트랜젝션 관리가 필요합니다.트랜잭션범위
에서 획득, 유지, 반납을 진행하기 때문에 트랜잭션설정은 필수 입니다.
👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏
Update
가 실시 될때마다, version의 값을 1씩 증가한다. 아래 사진 참조version
값과, DB의 version
값이 다를 경우, Exception을 발생시켜, 테이터 정합성 문제 혹은, 동시성 문제가 발생 했다는 신호를 주게 된다.OptimisticLockException
이 발생 된다.OptimisticLockException
이 나올 경우, 무한 반복 시켜, 문제를 동시성 문제가 발생하지 않을때까지 무한 반복 하도록 한다.Facade Pattern
을 사용하는 경우도 많다.@Entity
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "item")
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//이름
@Column(nullable = false, name = "name")
private String name;
//가격
@Column(nullable = false, name = "price")
private Long price;
//재고 수량
@Column(nullable = false, name = "stock")
private Long stock;
@Version
private Long version;
public void changeStock(Long amount){
this.stock = this.stock - amount;
}
}
package BaekGwa.ConcurrencyIssue.domain.item.service;
imports~~
@Service
@RequiredArgsConstructor
@Slf4j
public class ItemServiceImplV5 implements ItemService {
private final ItemRepository itemRepository;
@Override
public Boolean RegisterItem(NewItem newItem) {
~~~~~~~
}
@Transactional
@Override
public Boolean buyItem(BuyItem buyItem) throws InterruptedException {
while (true) {
try {
//검증 로직
//금액 검증
//상품 검증
//서비스 로직
return true;
} catch (OptimisticLockException e) {
Thread.sleep(50);
} catch (Exception e) {
log.error("구매 실패. Message = {}", e.getMessage() + e.getCause());
return false;
}
}
}
}
@Version
Annotation에 의해 version column이 관리가 된다. hibernate 에 의해서 관리가 되는 것이므로, 다음과 같이 새로운 객체를 생성해서 save를 하려고 하면, 다음과 같은 Detached Entity Error 발생한다. @Transactional
@Override
public Boolean buyItem(BuyItem buyItem) throws InterruptedException {
while (true) {
try {
~~~~
//저장 로직
itemRepository.save(Item
.builder()
.id(findItem.getId())
.name(findItem.getName())
.stock(findItem.getStock() - buyItem.getAmount())
.price(findItem.getPrice())
.build());
~~~
findItem.changeStock(buyItem.getAmount());
itemRepository.save(findItem);
~~~
👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏
실시간 충돌 방지
: 데이터에 접근하는 동안 락을 걸어 충돌을 방지합니다.락 유지
: 데이터에 락을 걸면 다른 트랜잭션은 락이 해제될 때까지 기다려야 합니다.성능
: 높은 동시성 환경에서는 성능 저하가 발생할 수 있습니다. 여러 트랜잭션이 대기 상태가 될 수 있습니다.성능
: 데이터 읽기 시 락을 걸지 않으므로 높은 성능을 유지할 수 있습니다.교착 상태 방지
: 락을 사용하지 않기 때문에 교착 상태의 위험이 줄어듭니다.설명
: 읽기 락을 설정합니다. 데이터가 읽히는 동안 다른 트랜잭션에서 데이터의 수정은 허용되지 않습니다. 그러나 데이터의 읽기는 허용됩니다.용도
: 데이터를 읽는 동안 다른 트랜잭션이 해당 데이터를 수정하지 못하도록 할 때 사용됩니다. (일부 JPA 구현에서는 지원되지 않을 수 있습니다.)설명
: 쓰기 락을 설정합니다. 데이터가 읽히는 동안 다른 트랜잭션에서 해당 데이터를 읽거나 수정할 수 없습니다.용도
: 데이터를 수정하는 동안 다른 트랜잭션이 해당 데이터에 접근할 수 없도록 할 때 사용됩니다.설명
: 낙관적 락을 설정합니다. 데이터에 버전 필드를 사용하여 충돌을 감지합니다. 데이터가 수정될 때 버전이 변경되며, 트랜잭션이 커밋될 때 버전 값이 변경되었으면 예외를 발생시킵니다.용도
: 데이터 충돌이 드물고 성능이 중요한 상황에서 사용됩니다. 데이터의 버전 관리를 통해 충돌을 검출합니다.설명
: 낙관적 락을 사용하되, 데이터의 버전 필드를 강제로 증가시킵니다. 이는 데이터가 읽히더라도 버전이 증가하도록 하여 데이터 수정이 일어난 것으로 간주합니다.용도
: 데이터 수정 시 버전 증가가 필수적이고, 충돌 감지를 더 강력하게 적용할 필요가 있는 경우에 사용됩니다.설명
: 비관적 읽기 락을 설정합니다. 데이터가 읽히는 동안 다른 트랜잭션에서 해당 데이터를 수정할 수 없도록 합니다. 그러나 다른 트랜잭션에서도 데이터를 읽을 수 있습니다.용도
: 데이터 읽기 시 다른 트랜잭션의 수정이 발생하지 않도록 보장할 필요가 있을 때 사용됩니다.설명
: 비관적 쓰기 락을 설정합니다. 데이터가 읽히는 동안 다른 트랜잭션에서 해당 데이터의 읽기 및 수정을 모두 차단합니다.용도
: 데이터를 수정하는 동안 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 할 때 사용됩니다.설명
: 비관적 락을 사용하면서도 데이터의 버전을 강제로 증가시킵니다. 데이터가 수정될 때 버전이 증가하며, 이는 강제로 트랜잭션의 수정이 일어난 것으로 간주됩니다.용도
: 비관적 락을 사용하되, 데이터 수정 시 버전 필드를 강제로 업데이트하여 데이터 충돌 감지를 보강할 필요가 있는 경우에 사용됩니다.