database는 근본적을 여러사람이 한 데이터를 건디리는 구조이기 때문에 충돌이 일어날 수 밖에 없다 (transaction isolation이 SERIALIZABLE 이지 않는 이상)
아주 기본적인 테이블을 예시로 들어보자
mysql> CREATE TABLE products (
-> id BIGINT AUTO_INCREMENT PRIMARY KEY,
-> stock INT
-> );
Query OK, 0 rows affected (0.04 sec)
mysql> insert into products(stock) values (10);
Query OK, 1 row affected (0.01 sec)
mysql> select * from products;
+----+-------+
| id | stock |
+----+-------+
| 1 | 10 |
+----+-------+
1 row in set (0.00 sec)
그리고 race condition 이 일어날 만한 상황을 재현해보자.
두 thread가 재고를 줄일려고 하는 상황이고 왼쪽은 commit을 하기 직전이다.

근데 재수없게도 왼쪽이 commit을 하기 전 오른쪽이 transaction을 시작한다.

이 경우 왼쪽이 commit을 하기 전까지 오른쪽은 기다려야 한다. 왜냐하면 왼쪽이 update를 위해 lock을 걸었기 때문이다.
그리고 이때 왼쪽이 commit을 하면 오른쪽도 update를 드디어 할 수 있다.

그리고 오른쪽도 이제 commit을 하면 정상적으로 상품 재고가 8개인 것을 확인 할 수 있다.

위 예시를 보면 Race Condtion을 DB가 잘 막아주는 거 같지만 함정이 있다. 간단한 Spring service code를 보자.
public void decreaseProduct(Long productId) {
Product product = productRepo.findById(productId).get();
Long stock = product.getStock();
product.setStock(stock - 1);
productRepo.save(product);
}
이때 우리는
이는 위와 다르다
위에서는 update문 한 명령에서 상품 재고를 읽고 줄였지만 여기서는 두 명령이 필요하다.
시연을 통해 살펴보자.
위와 마찬가지로 왼쪽과 오른쪽 둘다 상품 재고를 줄일려고 한다.
이때 우리는 Spring인척 하고 상품 재고를 조회한뒤 조회한 재고를 바탕으로 재고를 줄인다.

이 때 오른쪽은 위와 마찬가지로 왼쪽의 transaction이 끝나기전 줄일려고 한다. 이때 오른쪽은 왼쪽의 transaction을 볼 수 없으므로 재고 9가 아닌 10을 본다. 그렇기 때문에 오른쪽도 '아, 현재 재고가 10이니까 9로 줄이면 되겠네' 하고 9로 업데이트 하려고 한다.

결국 왼쪽, 오른쪽 둘다 두번 재고를 1 줄였음에도 재고는 8이 아닌 9이다.
