๋์์ฑ ๋ฌธ์
ํ๋์ ๋ฐ์ดํฐ์ ์ฌ๋ฌ ์ค๋ ๋๊ฐ ๋์์ ์ ๊ทผํ๋ฉด์ ์๊ธฐ๋ ๋ฌธ์ ๋ฅผ ๋์์ฑ ๋ฌธ์ ๋ผ๊ณ ํ๋ค.
ํ๋์ ์ธ์ ์ด ๋ฐ์ดํฐ๋ฅผ ์์ ์ค์ผ๋, ๋ค๋ฅธ ์ธ์ ์์ ์์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํด ๋ก์ง์ ์ฒ๋ฆฌํจ์ผ๋ก์จ ๋ฐ์ดํฐ์ ์ ํฉ์ฑ์ด ๊นจ์ง๊ฒ ๋๋ค.
๋์์ฑ ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ
๋์์ฑ ๋ฌธ์ ๋ ํ๋์ ์ธ์ ์ด ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ ๋์, ๋ค๋ฅธ ์ธ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ์ง ๋ชปํ๊ฒ ํจ์ผ๋ก์ ํด๊ฒฐํ ์ ์๋ค. ์ธ์ ์ ๋ ๋ฆฝ์ ์ธ ์คํ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ผ๋ก ๋ค์ ์ธ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์๋ค.
1. Application ๋ ๋ฒจ์์ synchronized ์ฌ์ฉ
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค์ Lock ์ฌ์ฉ
3. Redis์ Lock ์ฌ์ฉ
์ฌ๊ณ ๊ฐ์ ๋ก์ง์ ํตํด ๋์์ฑ ๋ฌธ์ ๋ฅผ ์ดํด๋ณด๊ณ ํด๊ฒฐํด๋ณด์.
๋ชฉ์ฐจ
0. ์ฌ๊ณ ์์คํ ๊ธฐ๋ณธ ๋ก์ง
1. ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์ด์
2. ๊ฒฝ์ ์ํ(Race Condition) ํด๊ฒฐ ๋ฐฉ๋ฒ
1. java synchronized
2. MySQL Lock
1. Pessimistic Lock
2. Optimistic Lock
3. Named Lock
3. Redis Lock
1. Lettuce Lock
2. Redisson Lock
3. ์ ๋ฆฌ
์ฌ๊ณ ์์คํ ๊ธฐ๋ณธ ๋ก์ง
- Stock.java : ์ํฐํฐ
- StockRepository.java : ๋ ํฌ์งํ ๋ฆฌ
- StockService.java : ์ฌ๊ณ ๊ฐ์ ๋ก์ง
Stock.java
@Entity
@Getter
@NoArgsConstructor
public class Stock {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
@Version
private Long version; //Optimistic Lock์์๋ง ์ฌ์ฉ
public Stock(Long productId, Long quantity) {
this.productId = productId;
this.quantity = quantity;
}
public void decrease(Long quantity) {
if (this.quantity - quantity < 0) {
throw new RuntimeException("์ฌ๊ณ ๋ 0๊ฐ ๋ฏธ๋ง์ด ๋ ์ ์์ต๋๋ค.");
}
this.quantity -= quantity;
}
}
StockService.java
@Service
@RequiredArgsConstructor
public class StockService {
private final StockRepository stockRepository;
public void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
}
์ ์ฝ๋๋ ์ฝ๊ฒ ๋ ์ฌ๋ฆด ์ ์๋ ์ฌ๊ณ ๊ฐ์ ๋ก์ง์ผ๋ก ์คํํ๋ฉด ์๋ฌด๋ฐ ๋ฌธ์ ๊ฐ ์๋ค. ๊ทธ๋ฌ๋ ๋ฉํฐ ์ค๋ ๋ ์ํฉ์์ ์์๊ณผ ๋ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ ๋ํ๋๋ค.
StockServiceTest.java
์ฌ๋ฌ ์ค๋ ๋๋ก ๋์์ ์ฌ๊ณ ๋ฅผ ๊ฐ์์ํค๋ ํ ์คํธ
- ExecutorService : ๋ณ๋ ฌ ์์ ์ ์ฌ๋ฌ ๊ฐ์ ์์ ์ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ ๊ณต๋๋ JAVA ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- CountDownLatch : ์ด๋ค ์ฐ๋ ๋๊ฐ ๋ค๋ฅธ ์ค๋ ๋์์ ์์ ์ด ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆด ์ ์๋๋ก ํด์ฃผ๋ ํด๋์ค
@Test
public void ๋์์_100๊ฐ์_์์ฒญ() throws InterruptedException {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
stockService.decrease(1L, 1L);
} finally {
latch.countDown();
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
// 100 - (1 * 100) = 0
assertEquals(0, stock.getQuantity());
}
100๊ฐ์ ์ค๋ ๋๊ฐ ๋์์ ์ฌ๊ณ ๋ฅผ ํ๋ ๊ฐ์์ํค๋ ํ ์คํธ ์ฝ๋๋ค. ์ฐ๋ฆฌ๋ ์ค๋ ๋ ํ๋๋น ํ๋์ ์ฌ๊ณ ๋ฅผ ๊ฐ์์ํค๋ฉฐ ๋ง์ง๋ง์ ๋จ์ ์ฌ๊ณ ์๋ 0์ด๊ธธ ๊ธฐ๋ํ๋ค. ๊ทธ๋ฌ๋ ์ฝ๋๋ฅผ ์คํ์์ผ๋ณด๋ฉด ๋จ์ ์ฌ๊ณ ์๋์ 0๋ณด๋ค ๋ง์ ํ ์คํธ๋ ์คํจํ๋ค.
์ด๋ ๋ ์ด์ค ์ปจ๋์ (Race Condition)์ด ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ด๋ค. ๋ ์ด์ค ์ปจ๋์ ์ด๋ ์ฌ๋ฌ ๊ฐ์ ํ๋ก์ธ์ค๊ฐ ๊ณต์ ์์์ ๋์ ์ ๊ทผํ ๋ ์คํ ์์์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ์ด ๋ฌ๋ผ์ง ์ ์๋ ํ์์ด๋ค.
1. ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์ ๋ ์ด์ค ์ปจ๋์ ์ด ๋ฐ์ํ๋ ์ด์
์์ ์์ ์์
๋ฉํฐ ์ค๋ ๋๋ก ์์ ์ ํ ๋, ์๋ ๊ทธ๋ฆผ์ฒ๋ผ ๋ฐ์ดํฐ์ ์์ฐจ์ ์ผ๋ก ์ ๊ทผํ์ฌ ์ฌ๊ณ ๋ฅผ ์ฒ๋ฆฌํ๊ธธ ๊ธฐ๋ํ๋ค.
Thread-1 | Stock | Thread-2 |
select * from stock where id = 1 |
{id: 1, quantity: 5} | |
update set quantity = 4 from stock where id = 1 |
{id: 1, quantity: 4} | |
{id: 1, quantity: 4} | select * from stock where id = 1 |
|
{id: 1, quantity: 3} | update set quantity = 3 from stock where id = 1 |
์ค์ ์์ ์์
๊ทธ๋ฌ๋ ์ค์ ๋ก๋ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋์์ ๋ณ๊ฒฝํ๋ ค ํ๊ธฐ ๋๋ฌธ์ ์ฌ๊ณ ๊ฐ์ ์์ ์ด ๋๋ฝ๋ ์ ์๋ค.
Thread-1 | Stock | Thread-2 |
select * from stock where id = 1 |
{id: 1, quantity: 5} | |
{id: 1, quantity: 5} | select * from stock where id = 1 |
|
update set quantity = 4 from stock where id = 1 |
{id: 1, quantity: 4} | |
{id: 1, quantity: 4} | update set quantity = 4 from stock where id = 1 |
2. ๋ ์ด์ค ์ปจ๋์ ํด๊ฒฐ ๋ฐฉ๋ฒ
๋ ์ด์ค ์ปจ๋์ ์ ํด๊ฒฐํ๊ธฐ ์ํด์ ๊ณต์ ์์์ ๋ํด ํ๋์ ์ค๋ ๋๋ง ์ ๊ทผํ ์ ์๊ฒ๋ ์ ํํ๋ฉด ๋๋ค.
(1) synchronized ์ด์ฉํ๊ธฐ
synchronized๋ java์์ ์ฌ์ฉํ๋ ์ค๋ ๋ ๋๊ธฐํ์ ๋ํ์ ์ธ ๋ฐฉ๋ฒ
synchronized ํค์๋๋ฅผ ์ด์ฉํ๋ฉด ํด๋น ๋ฉ์๋๋ฅผ ํ๋ฒ์ ํ ์ค๋ ๋์ฉ ์ํํ๋๋ก ๋ณด์ฅํ๋ค.
synchronized ์ฌ์ฉ ์ ์ฃผ์ํ ์ : @Transactional์ ๋ถ์ด์ง ๋ง ๊ฒ(๐), stock์ ๊ฐฑ์ ํ ๋ save๊ฐ ์๋ saveAndFlush๋ฅผ ์ธ ๊ฒ(๐)
public synchronized void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
๐ฅsynchronized์ ๋ฌธ์ ์
- synchronized๋ ํ๋์ ํ๋ก์ธ์ค ์์์๋ง ๋ณด์ฅ๋๋ค. ๋ฐ๋ผ์ ์๋ฒ๊ฐ ์ฌ๋ฌ ๋์ธ ๊ฒฝ์ฐ, synchronized๋ฅผ ์ฌ์ฉํ๋ฉด ๋ ๋ค์ race condition์ด ๋ฐ์ํ๊ฒ ๋๋ค.(๋ฉํฐ ํ๋ก์ธ์ค)
- ์ค๋ฌด์์ ๋๋ถ๋ถ ๋ ๋ ์ด์์ ์๋ฒ๋ฅผ ์ฌ์ฉํ๋ฏ๋ก synchronized๋ ์ฌ์ฉํ์ง ์๋๋ค.
(2) ๋ฐ์ดํฐ๋ฒ ์ด์ค(MySQL)์ Lock ์ด์ฉํ๊ธฐ
1. Pessimistic Lock
- ์ค์ ๋ก ๋ฐ์ดํฐ์ Lock์ ๊ฑธ์ด์ ์ ํฉ์ฑ์ ๋ง์ถ๋ ๋ฐฉ๋ฒ
- exclusive lock์ ๊ฑธ๊ฒ ๋๋ฉด ๋ค๋ฅธ ํธ๋์ญ์ ์์ lock์ด ํด์ ๋๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ๊ฐ ์ ์๊ฒ ๋๋ค.
- ์์ ์์ฒญ์ ๋ฐ๋ฅธ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ ์์ํ๊ณ ๋ฝ์ ๊ฑฐ๋ ๋น๊ด์ ๋ฝ ๋ฐฉ์์ด๋ค.
- ๋ฐ๋๋ฝ์ด ๊ฑธ๋ฆด ์ ์๊ธฐ ๋๋ฌธ์ ์ฃผ์ํ์ฌ ์ฌ์ฉํด์ผ ํ๋ค.
2. Optimistic Lock
- ์ค์ ๋ก Lock์ ์ด์ฉํ์ง ์๊ณ ๋ฒ์ ์ ์ด์ฉํจ์ผ๋ก์จ ์ ํฉ์ฑ์ ๋ง์ถ๋ ๋ฐฉ๋ฒ
- ๋จผ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ํ, update๋ฅผ ์ํํ ๋ ํ์ฌ ๋ด๊ฐ ์ฝ์ ๋ฒ์ ๊ณผ ์ผ์นํ๋์ง๋ฅผ ํ์ธํ์ฌ ์ ๋ฐ์ดํธ๋ฅผ ์ํํ๋ค.
- ๋ด๊ฐ ์ฝ์ ๋ฒ์ ์์ ์์ ์ฌํญ์ด ์๊ธด ๊ฒฝ์ฐ, application์์ ๋ค์ ์ฝ์ด ์์
์ ์ํํด์ผ ํ๋ค.
โ Server 1์ด version 1์ ๋ช
์ํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฐ๋ค.
โก version 1 ์ฟผ๋ฆฌ๋ฅผ ์ฒ๋ฆฌํ๋ฉฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค version์ 2๊ฐ ๋๋ค.
โข Server 2๊ฐ version 1์ ๋ช ์ํ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฐ๋ค. ํ์ง๋ง ๋ฒ์ ์ด ์ผ์นํ์ง ์์ ์ ๋ฐ์ดํธ์ ์คํจํ๋ค.
โฃ ์ฟผ๋ฆฌ๊ฐ ์คํจํ์ผ๋ฏ๋ก Server 2๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฒ์ ์ ๋ค์ ์กฐํํ ํ version์ ์์ ํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฐ๋ค.
3. Named Lock
- ์ด๋ฆ์ ๊ฐ์ง metadata locking (๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ฒด์ ๋ํ ๋์ ์ก์ธ์ค๋ฅผ ๊ด๋ฆฌํ๊ณ ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ๋ณด์ฅํ๋ ์ ๊ธ)
- ์ด๋ฆ์ ๊ฐ์ง lock์ ํ๋ํ ํ ํด์ ํ ๋๊น์ง ๋ค๋ฅธ ์ธ์ ์ ์ด lock์ ํ๋ํ ์ ์๋ค.
- ํธ๋์ญ์ ์ด ์ข ๋ฃ๋ ๋ lock์ด ์๋์ผ๋ก ํด์ ๋์ง ์๋๋ค. ๋ฐ๋ผ์ ๋ณ๋์ ๋ช ๋ น์ด๋ฅผ ์ํํ๊ฑฐ๋ ์ ์ ์๊ฐ์ด ๋๋์ผ ํด์ ๋๋ค.
1. Pessimistic Lock ์ฌ์ฉํ๊ธฐ
StockRepository.java
public interface StockRepository extends JpaRepository<Stock, Long> {
//Lock์ ๊ฑธ๊ณ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฉ์๋
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
}
- ๋ด์ฉ ์ถ๊ฐ ์์ -