๐ก ์ต์์ฉ๋์ ์ฌ๊ณ ์์คํ ์ผ๋ก ์์๋ณด๋ ๋์์ฑ์ด์ ํด๊ฒฐ๋ฐฉ๋ฒ ๊ฐ์๋ฅผ ๋ฃ๊ณ ์ ๋ฆฌํ ๋ด์ฉ์ ๋๋ค.
๋ฐฐ๊ฒฝ
์ง๋ ๋์์ฑ์ด์ ํด๊ฒฐ๋ฐฉ๋ฒ (1/3) - ๋์์ฑ ์ด์์ Application Level๋ก ํด๊ฒฐํ๊ธฐ ๊ธ์์ ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์ ๊ฐ๋ฐํ ๋ ๋ฐ์ํ ์ ์๋ ๊ฒฝํฉ ์ํ(Race condition)๋ฅผ ๋น๋กฏํ ๋์์ฑ ์ด์๋ฅผ ์์๋ณด์๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ๊ณต์ ๋ฐ์ดํฐ์ ์ ๊ทผํ ๋ ํ ๋ฒ์ ํ ์ค๋ ๋๋ง ์ ๊ทผํ๊ณ ์์
ํ ์ ์๋๋ก ์ ์ด๋ฅผ ํด์ผ ํ๋ค๋ ์ฌ์ค์ ์์๊ณ ์๋ฐ์ synchronized ํค์๋๋ฅผ ๋จผ์ ์ ์ฉํ์ฌ ํด๊ฒฐํ์๋ค. ํ์ง๋ง ์๋ฐ์ synchronized ํค์๋๋ ํ ํ๋ก์ธ์ค ๋ด์์๋ง ์ค๋ ๋ ์์ ์ฑ์ ์ ๊ณตํ๋ฉฐ ์คํ๋ง์ @Transactional ์ด๋
ธํ
์ด์
๊ณผ ํจ๊ป ์ฌ์ฉํ๊ธฐ์ ํ๊ณ๊ฐ ์์๋ค.
๊ฒฐ๊ตญ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณ๋์ ๋๊ธฐํ ๋ฉ์ปค๋์ฆ์ ํ์์ฑ์ ๋๊ผ๊ณ , ์ด๋ฒ ๊ธ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ ๊ณตํ๋ Lock์ ํ์ฉํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด ๋ณธ๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฝ(Database Lock)
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋ฝ(Lock)์ด๋ผ๋ ์ฅ์น๋ก ๋ฐ์ดํฐ์ ์ ๊ทผ ์ ์ฝ์ ๋์ด ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ๋ง์ถ๋ ๋ค์ํ ๋ฐฉ๋ฒ์ ์ ๊ณตํ๋๋ฐ, ๋ํ์ ์ผ๋ก ๋น๊ด์ ๋ฝ(Pessmistic Lock), ๋๊ด์ ๋ฝ(Optimistic LocK), ๋ค์๋ ๋ฝ(Named Lock)์ด ์๋ค. ๊ฐ๊ฐ์ ํน์ฑ์ ๋ํด ๊ฐ๋ตํ ์ดํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
๋น๊ด์ ๋ฝ(Pessimistic Lock)
์ค์ ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์ด๋ธ ํน์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ก์ฐ(Row)์ Lock์ ๊ฑธ์ด์ ๋ฐ์ดํฐ ์ ๊ทผ ์ ์ฝ์ ๋์ด ์ ํฉ์ฑ์ ๋ง์ถ๋ ๋ฐฉ๋ฒ์ด๋ค. ์ด๋ฌํ ๋ฐฉ์์ผ๋ก Exclusive lock์ ๊ฑธ๊ฒ ๋๋ฉด ๋ค๋ฅธ ํธ๋์ญ์ ์์๋ Lock์ด ํด์ ๋๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ๊ฐ ์ ์๋ค.
๊ฐ์ฅ ์ง๊ด์ ์ผ๋ก(๋ฐ์ดํฐ์ ์ ๊ทผ ์์ฒด๋ฅผ ์ ํ) ์ ๊ทผ ์ ์ฝ์ ๋ ์ ์์ง๋ง, ๋ ๊ฐ ์ด์์ ์ค๋ ๋๊ฐ ์๋ก ๋ฝ์ ์ป๊ธฐ ์ํด ๋ฌดํ์ ๋๊ธฐํ๋ ๋ฐ๋๋ฝ(Deadlock, ๊ต์ฐฉ์ํ)์ ๋น ์ง ์ ์์ผ๋ฏ๋ก ์ฃผ์ํด์ ์ฌ์ฉํด์ผ ํ๋ค.
๋๊ด์ ๋ฝ(Optimistic Lock)
์ค์ ๋ก ๋ฐ์ดํฐ ์์ฒด์ ๋ฝ(Lock)์ ๋๋ ๊ฒ์ด ์๋๋ผ ๋ฐ์ดํฐ์ ๋ฒ์ (Version)์ ์ด์ฉํ์ฌ ์ ํฉ์ฑ์ ๋ง์ถ๋ ๋ฐฉ๋ฒ์ด๋ค. ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ๋ ๋ฐ์ดํฐ์ ๋ฒ์ ์ ํจ๊ป ์กฐํํ๋ค. ๊ทธ๋ฆฌ๊ณ ์
๋ฐ์ดํธํ๊ธฐ ์ ์ ์
๋ฐ์ดํธํ๋ ค๋ ๋ฐ์ดํฐ์ ๋ฒ์ ์ด ์ฒ์ ์กฐํํ์ ๋น์ ๋ฒ์ ๊ณผ ๋์ผํ์ง ํ์ธํ๋ค.
๋ง์ผ ๋ฒ์ ์ด ๊ฐ๋ค๋ฉด ์ ๋ฐ์ดํธ์ ์ฑ๊ณตํ๊ณ , ๋ฒ์ ์ด ๋ค๋ฅด๋ค๋ฉด ์ ๋ฐ์ดํธ์ ์คํจํ๋ค.
์ด ๊ฒฝ์ฐ ์ ํ๋ฆฌ์ผ์ด์ ๋จ์์ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํด์ ์ ๋ฐ์ดํธ๋ฅผ ์ฌ์ํํ๋ ๋ก์ง์ ์ง์ผํ๋ค.
๋ค์๋ ๋ฝ(Named Lock)
์ด๋ฆ์ ๊ฐ์ง Metadata lock๊ธฐ๋ฒ์ด๋ค.
์ด๋ฆ์ ๊ฐ์ง Lock์ ํ ์ธ์ ์ด ํ๋ํ ํ ํด์ ํ๊ธฐ ์ ๊น์ง ๋ค๋ฅธ ์ธ์ ์ ์ด Lock์ ํ๋ํ ์ ์๋ค.
์ฃผ์ํ ์ ์ Transaction์ด ์ข ๋ฃ๋ ๋ Lock์ด ์๋์ผ๋ก ํด์ ๋์ง ์๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ๋ช ๋ น์ด๋ก ํด์ ๋ฅผ ์ํํด์ผ ํ๋ค.
ํน์ TTL(Time to live)๋ฅผ ๋์ด ์ ์ ์๊ฐ์ด ๋๋๋ฉด ํด์ ํ๋๋ก ๊ฐ๋ฐ์๊ฐ ๋ณ๋๋ก ์ค์ ํด์ผ ํ๋ค.
Pessimistic Lock๊ณผ ์ ์ฌํ๋ Pessimistic Lock์ ๋ก์ฐ๋ ํ
์ด๋ธ ๋จ์๋ก ๋ฝ์ ๊ฑฐ๋ ๋ฐ๋ฉด ๋ค์๋ ๋ฝ์ metadata lock์ ๋ํด ๋ฝ์ ๊ฑด๋ค๋ ์ฐจ์ด๊ฐ ์๋ค.
Pessimistic Lock ํ์ฉํด ๋ณด๊ธฐ
์์ ์ค๋ช ํ 3๊ฐ์ง ๋ฝ ๊ธฐ๋ฒ(๋น๊ด์ ๋ฝ, ๋๊ด์ ๋ฝ, ๋ค์๋ ๋ฝ)์ ์ง์ ์ฌ๊ณ ๊ด๋ฆฌ ์์คํ ์ ์ ์ฉํ์ฌ Race condition์ ํด๊ฒฐํด ๋ณธ๋ค.
๋จผ์ ๋น๊ด์ ๋ฝ(Pessimistic Lock)์ ๋จผ์ ์ ์ฉํด ๋ณธ๋ค.
์์ ์ค๋ช
ํ๋ฏ ๋๊ด์ ๋ฝ์ ์ค์ ํ
์ด๋ธ ํน์ Row์ ๋ฝ์ ๊ฑธ์ด ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ ์ ํฉ์ฑ์ ๋ง์ถ๋ค.
์ค์ ๋ฐ์ดํฐ์ Exclusive Lock์ ๊ฑธ๋ฉด ๋ค๋ฅธ ํธ๋์ญ์
์์๋ ํด๋น ๋ฐ์ดํฐ์ ๋ํด Lock์ ํ๋ํ๊ณ ์์
ํ๊ธฐ ์ ๊น์ง ๋๊ธฐํด์ผ ํ๋ค.
๋น๊ด์ ๋ฝ(Pessimistic Lock)์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ํ์๋ผ์ธ ๋ณ๋ก ๋ ๊ฐ์ ์ค๋ ๋๊ฐ ๊ณต์ ๋ฐ์ดํฐ์ธ Stock์ ์ด๋ป๊ฒ ์ ๊ทผํ์ฌ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๋์ง ๊ทธ ๊ณผ์ ์ ์ดํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
StockRepository
์ฌ๊ณ ๋ก์ฐ์ ๋ํด Lock์ ๊ฑธ๊ณ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๊ธฐ ์ํด findByIdWithPessimisticLock() ๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
์ด๋ Spring Data JPA์์ ์ ๊ณตํ๋ @Lock ์ด๋
ธํ
์ด์
์ ํ์ฉํ์ฌ LockModeType์ PESSIMISTIC_WRITE๋ก ๋์ด ๊ฐ๋จํ๊ฒ Pessimistic Lock์ ๊ตฌํํ ์ ์๋ค.
public interface StockRepository extends JpaRepository<Stock, Long> {
// Spring Data Jpa์์๋ @Lock ์ด๋
ธํ
์ด์
์ ํ์ฉํ์ฌ ์์ฝ๊ฒ Pessimistic Lock์ ๊ตฌํํ ์ ์๋ค.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
}
PESSIMISTIC_WRITE
JPA(Java Persistence API)์์ ์ ๊ณตํ๋ ๋น๊ด์ ๋ฝ(Pessimistic Locking) ์ค์ ์ด๋ ธํ ์ด์ ์ ํน์ ์ํฐํฐ์ ๋ํด ์ ๊ธ์ ๊ฑธ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค๊น์ง ์ ํํ๋ค. MySQL์์๋ SELECT ... FOR UPDATE ์ฟผ๋ฆฌ๋ฅผ ํตํด ์ฐ๊ธฐ ๋ฝ(Write Lock)์ ์ค์ ํ๋ค. ํ ์์ค(Row Level)์ ๋ฝ์๋ Shared Lock(sLock-๊ณต์ ๋ฝ)๊ณผ Exclusive Lock(xLock - ๋ฒ ํ ๋ฝ)์ด ์กด์ฌํ๋ค.
- ๊ณต์ ๋ฝ (sLock): ์ฝ๊ธฐ ์ ์ฉ ๋ฝ์ผ๋ก, ๋ค๋ฅธ ํธ๋์ญ์ ์ด ์ฝ์ ์ ์์ง๋ง ์์ ์ ๋ถ๊ฐ๋ฅ
- ๋ฒ ํ ๋ฝ (xLock): ์ฐ๊ธฐ ๋ฝ์ผ๋ก, ๋ค๋ฅธ ํธ๋์ญ์ ์ด ํด๋น ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฑฐ๋ ์ธ ์ ์๊ฒ ํ๋ค. Spring Data JPA์์๋ LockModeType.PESSIMISTIC_WRITE๋ก ์ค์ ํ ์ ์์ต๋๋ค.
๋น๊ด์ ๋ฝ์ ๋ชฉ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ํ ๋ณ๊ฒฝ ๊ฐ๋ฅ์ฑ์ด ์์ ๋ ์ฆ์ ์ ๊ธ์ ๊ฑธ์ด, ๋ค๋ฅธ ํธ๋์ญ์ ์์ ํด๋น ๋ฐ์ดํฐ๋ฅผ ์์ ํ์ง ๋ชปํ๊ฒ ํ๋ ๊ฒ์ด๋ค. ํธ๋์ญ์ ์ด ์๋ฃ๋ ๋๊น์ง ์ ๊ธ์ด ์ ์ง๋๋ฉฐ, ๋ง์ฝ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๋จผ์ ๋ฝ์ ๊ฐ์ง ๊ฒฝ์ฐ ์์ฒญ๋ ํธ๋์ญ์ ์ ๋๋ฝ๋์ง ์๊ณ ๋๊ธฐ ์ํ์ ๋ค์ด๊ฐ๋ค.
PessimisticLockStockService
์ฌ๊ณ ๊ฐ์ ๋ก์ง์ ์ํํ ๋ decrease() ๋ฉ์๋์์ Pessimistic Lock์ ์ฌ์ฉํ๋ ๋ก์ง์ ๊ตฌํํ๋ค.
์ด๋ฅผ ํตํด ์ด์ ๊ณผ ๋ฌ๋ฆฌ ๋ฝ์ ๊ฑธ๊ณ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ์ ์๋ค.
@Service
public class PessimisticLockStockService {
private final StockRepository stockRepository;
public PessimisticLockStockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity) {
// ๋ฝ์ ๊ฑธ๊ณ ์ฌ๊ณ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค.
Stock stock = stockRepository.findByIdWithPessimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
}
StockServiceTest
๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ๋ PessimisticLockStockService ํด๋์ค์ decrease() ๋ฉ์๋๋ฅผ ํ
์คํธํ๋ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
ํ
์คํธ ์ฝ๋๋ฅผ ์๋ก ์์ฑํ ํ์ ์์ด ๊ธฐ์กด์ ํ
์คํธ ์ฝ๋์์ @Autowired๋ก StockService ๋์ PessimisticLockStockService ํด๋์ค๋ฅผ ์์กด๊ด๊ณ๋ก ์ฃผ์
ํ๋ฉด ๋๋ค.
@SpringBootTest
class StockServiceTest {
@Autowired
//private StockService stockService;
private PessimisticLockStockService stockService;
@Autowired
private StockRepository stockRepository;
@BeforeEach
public void before() {
stockRepository.saveAndFlush(new Stock(1L, 100L));
}
@AfterEach
public void after() {
stockRepository.deleteAll();
}
@Test
public void ๋์์_100๊ฐ์_์์ฒญ() throws InterruptedException {
// 100๊ฐ์ ์ฐ๋ ๋ ์ฌ์ฉ(๋ฉํฐ์ค๋ ๋)
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 {
// CountDownLatch 1 ๊ฐ์
latch.countDown();
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
assertEquals(0, stock.getQuantity());
}
}
ํ ์คํธ ๊ฒฐ๊ณผ
ํ ์คํธ๊ฐ ์ฑ๊ณตํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
๋ก๊ทธ๋ฅผ ํ์ธํด ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์กฐํ ์ฟผ๋ฆฌ๋ฅผ ํธ์ถํ ๋ select for update ์ฟผ๋ฆฌ๋ฅผ ํธ์ถํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด ๋ถ๋ถ์ ๋ฐ์ดํฐ์ ๋ํด Lock์ ๊ฑธ๊ณ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ๋ถ๋ถ์ ํด๋นํ๋ค.
์ฆ, ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ๋ค๋ ๊ฒ์ ์กฐํ ์์ ์ select for update ์ฟผ๋ฆฌ๋ฅผ ํธ์ถํ์ฌ ์กฐํํ๋ ค๋ ๋ฐ์ดํฐ์ ๋ํด Lock์ ๊ฑด๋ค๋ ๊ฒ์ ์ ์ ์๋ค.
์ด์ฒ๋ผ Pessimistic Lock์ ์กฐํ์์ ์ Lock์ ํตํด ์ ๋ฐ์ดํธ๋ฅผ ์ ์ดํ์ฌ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ๋ณด์ฅํ๋ค.
ํ์ง๋ง ๋ฐ์ดํฐ ์กฐํ ์์ ์ ๋ณ๋์ Lock์ ์ก์๋ฒ๋ฆฌ๊ธฐ ๋๋ฌธ์ ์ถฉ๋์ด ๋น๋ฒํ๊ฒ ๋ฐ์ํ์ง ์๋ ์ํฉ์์๋ ๋ถํ์ํ ๋๊ธฐ ์๊ฐ์ผ๋ก ์ฑ๋ฅ ์ ํ๊ฐ ๋ํ๋ ์ ์๋ค.
(๋ฐ๋ก ๋ค์์ ์ดํด๋ณผ) Optimistic Lock์ ์ถฉ๋์ด ์ผ์ด๋๋ ๊ฒฝ์ฐ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ์กฐํํด์ผ ํ๋ฏ๋ก ์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋๋ค๋ฉด ์ฑ๋ฅ์ด ์ ํ๋๋ค. ๋ฐ๋ฉด ์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋๋ ๋ฐ์ดํฐ์ธ ๊ฒฝ์ฐ Pessimistic Lock์ Optimistic Lock์ ๋นํด ๋ฐ์ด๋ ์ฑ๋ฅ์ ์ ๊ณตํ๋ค.
Optimistic Lock ํ์ฉํด ๋ณด๊ธฐ
์ด๋ฒ์๋ ๋๊ด์ ๋ฝ(Optimistic Lock)์ผ๋ก ๋์์ฑ์ ์ ์ดํ์ฌ ์ ํฉ์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด ๋ณธ๋ค.
๋๊ด์ ๋ฝ(Optimistic Lock)์ ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฝ์ ์ด์ฉํ์ง ์๊ณ version์ ์ด์ฉํ์ฌ ์ ํฉ์ฑ์ ๋ง์ถ๋ ๋ฐฉ๋ฒ์ด๋ค.
์ ์ด๋ฏธ์ง๋ ์๋ฒ 1๊ณผ ์๋ฒ 2๊ฐ version์ด 1์ธ ๋ฐ์ดํฐ๋ฅผ ๋์์ ์กฐํํ๋ ์ํฉ์ด๋ค.
์๋ฒ 1์ด ๋จผ์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๋ค๊ณ ํ์. ์ด๋ ์ฌ๊ณ ๋ง ๋ณ๊ฒฝํ๋ ๊ฒ ์๋๋ผ version์ ๊ฐ๋ ๋ณ๊ฒฝํ๋ค.
์ด๋ฐ ๊ฒฝ์ฐ server2๋ version์ด 1์ธ ๋ฐ์ดํฐ๋ฅผ update ํ๋ค๋ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฌ์ง๋ง ์ด๋ฏธ ์๋ฒ 1์ด version์ ๊ฐ๋ ๋ณ๊ฒฝํ์ฌ update ์ฟผ๋ฆฌ๊ฐ ์คํจํ๊ณ ๋ง๋ค. ์ด๋ ๊ฒ ์ ๋ฐ์ดํธ๊ฐ ์คํจํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ฒจ์์ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ํ ์ ๋ฐ์ดํธ๋ฅผ ์ํํ๋ ๋ก์ง์ ์ง์ผํ๋ค.
Stock
๋๊ด์ ๋ฝ์ ๋ ์ฝ๋(๋ก์ฐ)์ ๋ฒ์ ์ ํ์ธํ์ฌ ์ถฉ๋์ ๊ฐ์งํ๋ ๋ฐฉ์์ผ๋ก ๋์ํ๋ค.
์ด๋ฅผ ๊ตฌํํ๊ธฐ ์ํด Stock ํด๋์ค์ version ํ๋๋ฅผ ์ถ๊ฐํ๊ณ ํด๋น ํ๋์ @Version ์ด๋ ธํ ์ด์ ์ ๋์ด ํน์ ๋ ์ฝ๋์ ๋ํด ์์ ์ด ์ผ์ด๋ ๋๋ง๋ค ์๋์ผ๋ก ์ฆ๊ฐํ๊ณ ์ถฉ๋ ๊ฐ์ง์ ์ฌ์ฉํ ์ ์๋๋ก ํ๋ค.
@Entity
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() {
}
public Stock(Long productId, Long quantity) {
this.productId = productId;
this.quantity = quantity;
}
public Long getQuantity() {
return quantity;
}
public void decrease(Long quantity){
if(this.quantity - quantity < 0){
throw new RuntimeException("์ฌ๊ณ ๋ 0๊ฐ ๋ฏธ๋ง์ด ๋ ์ ์์ต๋๋ค.");
}
// ํ์ฌ ์๋์ ๊ฐฑ์
this.quantity -= quantity;
}
}
์ด๋ @Version ์ด๋ ธํ ์ด์ ์ jakarta.persistence๋ฅผ import ํ๋ค.
Stock ํ
์ด๋ธ์ ์กฐํํด ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด version ์ปฌ๋ผ์ด ์ถ๊ฐ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
@Version
Spring JPA์์ @Version ์ด๋ ธํ ์ด์ ์ ๋๊ด์ ๋ฝ(Optimistic Locking)์ ๊ตฌํํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
๋๊ด์ ๋ฝ์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๋ ค ํ ๋, ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ํด ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋์๋์ง ํ์ธํ์ฌ ๋์์ฑ์ ์ ์ดํ๋ ๋ฐฉ์์ด๋ค.
- @Version ํ๋ ๊ด๋ฆฌ: @Version ์ด๋ ธํ ์ด์ ์ ๋ถ์ธ ํ๋์ ๊ฐ์ ์ํฐํฐ๊ฐ ์ ๋ฐ์ดํธ๋ ๋๋ง๋ค ์๋์ผ๋ก ์ฆ๊ฐํ์ฌ, ๋ณ๊ฒฝ๋ ์ด๋ ฅ์ด ๋ฐ์๋๋ค.
- ๋์ ์์ ์๋: ๋์ผํ ์ํฐํฐ๋ฅผ ์์ ํ๋ ค๋ ๋ ํธ๋์ญ์ ์ค ๋จผ์ ์์ํ ํธ๋์ญ์ ์ด ์ํฐํฐ๋ฅผ ์์ ํ๊ณ ์ปค๋ฐํ๋ฉด, @Version ๊ฐ์ด ์ฆ๊ฐํ๋ค. ์ดํ ์์๋ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๋ฐ์ดํฐ ์์ ์ ์ด์ ์ ์ฝ์๋ @Version ๊ฐ๊ณผ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ด ๋ฌ๋ผ์ง๋ฏ๋ก OptimisticLockingFailureException์ด ๋ฐ์ํ๋ค. ์ด ๋ฐฉ์์ผ๋ก ๋์ผ ๋ฐ์ดํฐ ์ ๊ทผ ์ ํธ๋์ญ์ ๊ฐ ์ถฉ๋์ ๋ฐฉ์งํ ์ ์๋ค.
๋๊ด์ ์ ๊ธ์ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ ๊ณ ๋ คํด ์์ธ ์ฒ๋ฆฌ ๋ก์ง์ ์ถ๊ฐํด์ผ ํ๋ค.
์๋ฅผ ๋ค์ด, ์ฌ๊ณ ์ ๊ฐ์ ์คํจ ์ ์ฌ์๋ํ๊ฑฐ๋ ์ฌ์ฉ์์๊ฒ ์๋ฌ ๋ฉ์์ง๋ฅผ ํ์ํ๋ ๋ฐฉ์์ผ๋ก ๋์ํ ์ ์๋ค.
๋๊ด์ ๋ฝ์ ์์ธ ์ข ๋ฅ
- javax.persistence.OptimisticLockException (JPA)
- org.hibernate.staleObjectStateException (Hibernate)
- org.springframework.orm.ObjectOptimisticLockingFailureException (Spring)
Spring ๊ธฐ๋ฐ์ JPA์์ ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ๊ฒ ๋๋ฉด ์ถฉ๋ ์ Hibernate๋ StaleStateException์ ๋ฐ์์ํค๋ฉฐ, Spring์ ์ด๋ฅผ OptimisticLockingFailureException์ผ๋ก ๊ฐ์ธ์ ์๋ตํ๋ค.
๋๊ด์ ๋ฝ์ ์ด๋ฌํ ๋ฐฉ์์ผ๋ก Race Condition์ ๋ฐฉ์งํ์ง๋ง, ์์ธ ์ฒ๋ฆฌ๊ฐ ๋ฏธํกํ ๊ฒฝ์ฐ ์์ ์ด ์ ์์ ์ผ๋ก ์งํ๋์ง ์๋๋ค. ๋ฐ๋ผ์, ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ ๋๋ ์ถฉ๋ ๋ฐ์ ์ ์ฌ์๋ํ๊ฑฐ๋ ์ฌ์ฉ์์๊ฒ ์ค๋ฅ๋ฅผ ์๋ฆฌ๋ ๋ฑ์ ์ ์ ํ ์์ธ ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ค.
StockRepository
Optimistic Lock์ ์ฌ์ฉํ๋ findByIdWithOptimisticLock() ๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
์ด๋์๋ @Lock ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋๋ฐ ์ด๋ฒ์๋ LockModeType๋ฅผ OPTIMISTIC๋ก ๋๋ค.
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(LockModeType.OPTIMISTIC) // Optimistic Lock์ ์ํจ
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(Long id);
}
OptimisticLockStockService
findByIdWithOptimisticLock()๋ฅผ ํธ์ถํ์ฌ ์ฌ๊ณ ๋ฅผ ์กฐํํ๊ณ ์ ๋ฐ์ดํธํ๋ ์๋น์ค ํด๋์ค๋ฅผ ์ถ๊ฐํ๋ค.
@Service
public class OptimisticLockStockService {
private final StockRepository stockRepository;
public OptimisticLockStockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity) {
// Optimistic Lock์ผ๋ก ์ฌ๊ณ (Stock) ์กฐํ
Stock stock = stockRepository.findByIdWithOptimisticLock(id);
// ์ฌ๊ณ ์๋ ๊ฐ์
stock.decrease(quantity);
stockRepository.save(stock);
}
}
OptimisticLockStockFacade
๋๊ด์ ๋ฝ(Optimistic Lock)์ ์คํจํ์ ๋ ์ฌ์๋๋ฅผ ํด์ผ ํ๋ฏ๋ก ์ฌ์๋ ๋ก์ง์ด ์ถ๊ฐ๋ก ํ์ํ๋ค.
์ ๋ฐ์ดํธ๋ฅผ ์คํจํ์ ๋ ์ฌ์๋๋ฅผ ํด์ผ ํ๋ฏ๋ก while๋ฌธ์ผ๋ก optimisticLockStockService.decrease(id, quantity)์ ๊ฐ์ธ์ค๋ค.
์๋ ๊ฐ์์ ์คํจํ๊ฒ ๋๋ฉด Thread.sleep(50)์ ํตํด 50ms ์ดํ์ ์ฌ์๋๋ฅผ ํ๋ค.
์ ์์ ์ผ๋ก update๊ฐ ๋๋ฉด break๋ฅผ ํตํด while๋ฌธ์ ๋น ์ ธ๋์จ๋ค.
@Service
public class OptimisticLockStockFacade {
private OptimisticLockStockService optimisticLockStockService;
public OptimisticLockStockFacade(OptimisticLockStockService optimisticLockStockService) {
this.optimisticLockStockService = optimisticLockStockService;
}
public void decrease(Long id, Long quantity) throws InterruptedException {
while (true) {
try {
optimisticLockStockService.decrease(id, quantity);
break;
} catch (Exception e) {
Thread.sleep(50);
}
}
}
}
ํ ์คํธ ์ฝ๋
@SpringBootTest
class OptimisticLockStockFacadeTest {
@Autowired
private OptimisticLockStockFacade optimisticLockStockFacade;
@Autowired
private StockRepository stockRepository;
@BeforeEach
public void insert() {
Stock stock = new Stock(1L, 100L);
stockRepository.saveAndFlush(stock);
}
@AfterEach
public void delete() {
stockRepository.deleteAll();
}
@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 {
optimisticLockStockFacade.decrease(1L, 1L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
latch.countDown();
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
// 100 - (100 * 1) = 0
assertEquals(0, stock.getQuantity());
}
}
์คํํด ๋ณด๋ฉด ํ
์คํธ๊ฐ ์ฑ๊ณตํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ฐธ๊ณ ๋ก ์์์ ๊ฐ์ด ์ถฉ๋์ด ์์ฃผ ๋ฐ์ํ๋ ๊ฒฝ์ฐ 50ms ๋๊ธฐํ๊ณ ์ฌ์๋(๋ค์ ์ฌ๊ณ ๋ฅผ ์กฐํํ์ฌ ์
๋ฐ์ดํธ)ํ๋ ๋ก์ง์ด ์์ด Pessmistic Lock์ ๋นํด ์๋๊ฐ ๋๋ฆฌ๋ค.
Pessmistic Lock์ผ๋ก ๊ตฌํํ์ ๋ ํ ์คํธ ์ํ ์๋๋ ๋ค์๊ณผ ๊ฐ๋ค. (์ฐธ๊ณ )
๋๊ด์ ๋ฝ(Optimistic Lock)์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ง์ ๋ฝ์ ๊ฑธ์ง ์์ผ๋ฏ๋ก ์ฑ๋ฅ์ ์ด์ ์ด ์๋ค.
ํ์ง๋ง ์์์ ๊ฐ์ด ์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋๊ฑฐ๋ ๋น๋ฒํ ๊ฒ์ด๋ผ๊ณ ์์๋๋ค๋ฉด Pessimistic Lock์ ๋นํด ์ฑ๋ฅ์ด ์ ํ๋๋ค.(์ด ๊ฒฝ์ฐ Pessimistic Lock์ ์ฌ์ฉํ๋ค.) ๋ํ, ์คํจํ์ ๋ ์ฌ์๋ ๋ก์ง์ ๊ฐ๋ฐ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ฒจ์ ์ง์ ์์ฑํด์ฃผ์ด์ผ ํ๊ธฐ ๋๋ฌธ์ ์ถ๊ฐ ์์ ์ด ํ์ํ๋ค.
Named Lock ํ์ฉํด ๋ณด๊ธฐ
๋ง์ง๋ง์ผ๋ก Named Lock์ ํ์ฉํ์ฌ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด ๋ณธ๋ค.
Named Lock์ ์ด๋ฆ์ ๊ฐ์ง ๋ฉํ ๋ฐ์ดํฐ(Meta data) ๋ฝ์ด๋ค.
์ด๋ฆ์ ๊ฐ์ง ๋ฝ์ ํ๋ํ ํ ํด์ ํ ๋๊น์ง ๋ค๋ฅธ ์ธ์ ์ ์ด ๋ฝ์ ํ๋ํ ์ ์๊ฒ ๋๋ค.
์ฃผ์ํ ์ ์ ์์ ์ดํด๋ณธ Pessimistic Lock๊ณผ Optimistic Lock๊ณผ ๋ฌ๋ฆฌ ํธ๋์ญ์
์ด ์ข
๋ฃ๋ ๋๋ฝ์ ์๋์ผ๋ก ๋ฐ๋ฉํ์ง ์๋๋ค. ๋ฐ๋ผ์ ๊ฐ๋ฐ์๊ฐ ๋ณ๋์ ๋ช
๋ น์ด๋ก Lock์ ๋ฐ๋ฉํ๋ ๋ก์ง์ ์์ฑํด์ฃผ์ด์ผ ํ๋ค. ํน์ ์ ์ ์๊ฐ์ด ๋๋๋ฉด ๋ฝ์ด ํด์ ๋๋๋ก ํ์์์์ ๊ฑธ์ด์ผ ํ๋ค.
์ด์ ์ ์ดํด๋ดค๋ ๋น๊ด์ ๋ฝ(Pessimistic Lock)์ Stock์ ๋ํด์ ๋ฝ์ ๊ฑธ์๋ค๋ฉด Named Lock์ Stock์ ๋ฝ์ ๊ฑธ์ง ์๊ณ ๋ณ๋์ ๊ณต๊ฐ์ Lock์ ๊ฑด๋ค. Mysql์์๋ getLock ๋ช ๋ น์ด๋ก Named Lock์ ํ๋ํ๊ณ releaseLock ๋ช ๋ น์ด๋ก ํด์ ํ ์ ์๋ค.
์ ์ด๋ฏธ์ง์์ Session-1์ด ‘1’์ด๋ผ๋ ์ด๋ฆ์ผ๋ก Lock์ ๊ฑธ๋ฉด ๋ค๋ฅธ ์ธ์ ์ธ Session-2๋ Session-1์ด ‘1’์ด๋ผ๋ ์ด๋ฆ์ Lock์ ๋ฐ๋ฉํด์ผ ์ด๋ฅผ ํ๋ํ ์ ์๋ค. (๋ ๋ฒ์งธ ์ธ์์ธ 1000์ ํ์์์์ ๊ฑธ๊ธฐ ์ํ ์๊ฐ์ ํด๋นํ๋ค.)
์ค์ต์ ๋ค์ด๊ฐ๊ธฐ ์ ์ฃผ์ ์ฌํญ์ด ์๋ค.
๊ฐ์ datasource๋ฅผ ์ฌ์ฉํ๋ฉด ์ปค๋ฅ์ ํ์ด ๋ถ์กฑํด์ง๋ ํ์์ผ๋ก ์ธํด์ ๋ค๋ฅธ ์๋น์ค์๋ ์ํฅ์ ๋ผ์น ์ ์๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ค๋ฌด์์๋ datasource๋ฅผ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๋ ๊ฒ ์ข๋ค.
์๋ ์ค์ต์์๋ ๊ฐ์ datasource๋ฅผ ์ฌ์ฉํ๋ค.
LockRepository
Named Lock์ ์ํ ๋ฆฌํฌ์งํ ๋ฆฌ๋ฅผ ๊ตฌํํ๋ค.
public interface LockRepository extends JpaRepository<Stock, Long> {
@Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
void getLock(String key);
@Query(value = "select release_lock(:key)", nativeQuery = true)
void releaseLock(String key);
}
ํธ์๋ฅผ ์ํด JPA์ Native Query ๊ธฐ๋ฅ์ ํ์ฉํ์ฌ Named Lock์ ์ป๋ getLock ๋ฉ์๋์ ๋ฐ๋ฉํ๋ releaseLock์ ๊ตฌํํ๋ค.
๋ํ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋์ผํ DataSource๋ก Named Lock์ ๊ตฌํํ๋ค.
(์์ ๋งํ ๊ฒ์ฒ๋ผ ์ค๋ฌด์์๋ Named Lock์ด ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋์ผํ DataSource๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด ์ปค๋ฅ์ ํ(Connection Pool)์ด ๋ถ์กฑํด์ ธ ๋ค๋ฅธ ์๋น์ค์ ์ํฅ์ ์ค ์ ์๋ค. ๋ฐ๋ผ์ DataSource๋ฅผ ๋ถ๋ฆฌํ์ฌ ์ฌ์ฉํ๋ ๊ฒ์ ์ถ์ฒํ๋ค.)
NamedLockStockFacade
์ค์ ๋น์ฆ๋์ค ๋ก์ง ์ ํ๋ก Named Lock์ ํ๋ํ๊ณ ๋ก์ง ์ํ ํ ํด์ ๋ฅผ ์ํํ๋ ํด๋์ค์ด๋ค.
๋น์ฆ๋์ค ๋ก์ง(์ฌ๊ธฐ์๋ decrease) ์ํ ์ ํ๋ก getLock๊ณผ releaseLock์ ํธ์ถํ์ฌ Named Lock์ ํ๋/๋ฐ๋ฉํ๋ค.
์ด๋ Lock ์ด๋ฆ์ row์ id๋ก ์ง์ ํ์๋ค.
@Component
public class NamedLockStockFacade {
private final LockRepository lockRepository;
private final StockService stockService;
public NamedLockStockFacade(LockRepository lockRepository, StockService stockService) {
this.lockRepository = lockRepository;
this.stockService = stockService;
}
@Transactional
public void decrease(Long id, Long quantity){
try {
// id ๊ฐ์ ๊ธฐ์ค์ผ๋ก Named Lock ํ๋
lockRepository.getLock(id.toString());
// ์ฌ๊ณ ๊ฐ์ ๋ก์ง
stockService.decrease(id, quantity);
} finally {
// ๋ชจ๋ ๋น์ฆ๋์ค ๋ก์ง์ ์ํํ๊ณ ๋๋ฉด lock ํด์
lockRepository.releaseLock(id.toString());
}
}
}
StockService
์ ์ฒด ๋ก์ง์ Pessimistic Lock, Optimistic Lock๊ณผ ๋์ผํ๋ค.
์ฐจ์ด์ ์ผ๋ก๋ ์ฌ๊ณ ๊ฐ์๋ฅผ ์ํ decrease() ๋ฉ์๋์ Propagation ์์ค์ REQUIRES_NEW๋ก ์ง์ ํ๋ค.
๋ง์ฝ ๋ถ๋ชจ์ ๊ฐ์ ํธ๋์ญ์
์์ Named Lock ์กฐํ ๋ก์ง์ ์ฌ์ฉํ๋ค๋ฉด ๋น์ฆ๋์ค ๋ก์ง ์ํ ์ค์ ์์ธ๊ฐ ๋ฐ์ํ์ฌ ์คํจํ๋ ๊ฒฝ์ฐ, ๋น์ฆ๋์ค ๋ก์ง์ ๋กค๋ฐฑํ๋ฉด์ Lock ํด์ ๊น์ง ๋กค๋ฐฑ๋์ด ๋ฒ๋ฆฐ๋ค. Lock์ ์ฌ์ ํ ์ด ์ค๋ ๋๊ฐ ํ๋ํ ์ํ๋ก ๋จ์์๊ฒ ๋๊ณ , ๋ค๋ฅธ ์ค๋ ๋๋ Lock์ ํ๋ํ๊ธฐ ์ํด ๋ฌดํ์ ๋๊ธฐ์ ๋น ์ง๊ฒ ์๋ค. ๋ฐ๋ผ์ ๋น์ฆ๋์ค ๋ก์ง์ Named Lock์ ํ๋ํ๋ ๋ถ๋ชจ์ ๋ณ๋์ ํธ๋์ญ์
์์ ์ํํ๋ค. ์ด๋ฅผ ์ํด Propagation ์์ค์ REQUIRES_NEW๋ก ์ง์ ํ๋ค.
@Service
public class StockService {
// StockService : ์ฌ๊ณ (Stock)์ ๋ํ CRUD ๊ธฐ๋ฅ์ ํตํ ๋น์ฆ๋์ค ๋ก์ง ์ ๊ณต
private final StockRepository stockRepository;
public StockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
// ์ฌ๊ณ ๊ฐ์ ๋ก์ง ๊ตฌํ
// Named Lock์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ๋ถ๋ชจ์ ๋ณ๋์ ํธ๋์ญ์
์์ ์ํ๋์ด์ผํ๋ฏ๋ก Propagation ์์ค์ REQUIRES_NEW๋ก ์ง์ ํ๋ค.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public synchronized void decrease(Long id, Long quantity) {
// Stock ์กฐํ
Optional<Stock> stock = stockRepository.findById(id);
// ์ฌ๊ณ ๊ฐ์
stock.orElseThrow(() -> new RuntimeException("ํด๋นํ๋ ์ฌ๊ณ ๊ฐ ์์ต๋๋ค."))
.decrease(quantity);
// ๊ฐฑ์ ๋ ๊ฐ์ ์ ์ฅ
stockRepository.save(stock.get());
}
}
application.yml
๊ฐ์ datasource๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ปค๋ฅ์ ํ ์ฌ์ด์ฆ๋ฅผ ๋ณ๊ฒฝํ๋ค.
spring:
jpa:
hibernate:
ddl-auto: create
show-sql: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
...
hikari:
maximum-pool-size: 40
...
NamedLockStockFacadeTest
NamedLockStockFacade์ ํ ์คํธํ์ฌ Named Lock์ด ์ ๋์ํ์ฌ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๊ณ ์ฌ๊ณ ์๋์ ์ฑ๊ณต์ ์ผ๋ก ์ฐจ๊ฐํ๋์ง ํ ์คํธํ๋ค.
@SpringBootTest
class NamedLockStockFacadeTest {
@Autowired
private NamedLockStockFacade namedLockStockFacade;
@Autowired
private StockRepository stockRepository;
// ๊ฐ ํ
์คํธ๊ฐ ์คํ๋๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ
์คํธ ๋ฐ์ดํฐ ์์ฑ
@BeforeEach
public void before() {
stockRepository.saveAndFlush(new Stock(1L, 100L));
}
// ๊ฐ ํ
์คํธ๋ฅผ ์คํํ ํ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ
์คํธ ๋ฐ์ดํฐ ์ญ์
@AfterEach
public void after() {
stockRepository.deleteAll();
}
@Test
public void ๋์์_100๊ฐ์_์์ฒญ() throws InterruptedException {
// when
// 100๊ฐ์ ์ฐ๋ ๋ ์ฌ์ฉ(๋ฉํฐ์ค๋ ๋)
int threadCount = 100;
// ExecutorService : ๋น๋๊ธฐ๋ก ์คํํ๋ ์์
์ ๊ฐ๋จํ๊ฒ ์คํํ ์ ์๋๋ก ์๋ฐ์์ ์ ๊ณตํ๋ API
ExecutorService executorService = Executors.newFixedThreadPool(32);
// CountDownLatch : ์์
์ ์งํ์ค์ธ ๋ค๋ฅธ ์ค๋ ๋๊ฐ ์์
์ ์๋ฃํ ๋๊น์ง ๋๊ธฐํ ์ ์๋๋ก ๋์์ฃผ๋ ํด๋์ค
CountDownLatch latch = new CountDownLatch(threadCount);
// 100๊ฐ์ ์์
์์ฒญ
for(int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
namedLockStockFacade.decrease(1L, 1L);
} finally {
latch.countDown();
}
});
}
latch.await();
// then
Stock stock = stockRepository.findById(1L).orElseThrow();
assertEquals(0, stock.getQuantity());
}
}
ํ ์คํธ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ํ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์์
์ค๋ ๋ ์ค ํ๋์ธ pool-1-thread-4์ ๋ก๊ทธ๋ฅผ ์ถ์ ํด ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ํ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
1. Named Lock ํ๋
2. ๋น์ฆ๋์ค ๋ก์ง ์ํ(์ฌ๊ณ ์ฐจ๊ฐ)
3. Named Lock ํด์
Named Lock์ ์ฃผ๋ก ๋ถ์ฐ๋ฝ(Distributed lock)์ ๊ตฌํํ ๋ ์ฌ์ฉํ๋ค.
๋ํ ํ์์์์ ๊ตฌํํ๊ธฐ ๊น๋ค๋ก์ด Pessimistic Lock๊ณผ ๋ฌ๋ฆฌ Named Lock์ ์์ฝ๊ฒ ์ด๋ฅผ ๊ตฌํํ ์ ์๋ค.
์ด์ธ์๋ ๋ฐ์ดํฐ ์ฝ์
์ ์ ํฉ์ฑ์ด ์ค์ํ ๊ฒฝ์ฐ์๋ ์ฌ์ฉํ ์ ์๋ค.
ํ์ง๋ง ํธ๋์ญ์
์ข
๋ฃ ์ Lock์ ๊ผญ ํด์ ํด์ค์ผ ํ๋ฏ๋ก ์ฃผ์ํด์ ์ฌ์ฉํ๋ค. ๋ฐ๋ผ์ ์ค์ ๋ก๋ ๊ตฌํ๋ฐฉ๋ฒ์ด ๋ณต์กํด์ง ์ ์๋ค.
Spring JPA Lock ์ต์ (LockModeType)
LockModeType์ค๋ช
NONE | ๋ฝ ์ฌ์ฉ ์ ํจ (๊ธฐ๋ณธ๊ฐ) |
OPTIMISTIC | ๋๊ด์ ๋ฝ ์ฌ์ฉ ๋ฐ์ดํฐ ์์ ์ ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ํด ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ ๋๋์ง ํ์ธ @Version์ผ๋ก ๋ฒ์ ์ปฌ๋ผ ๊ด๋ฆฌ |
OPTIMISTIC_FORCE_INCREMENT | ๋๊ด์ ๋ฝ ์ฌ์ฉ ๋ฝ์ด ๊ฑธ๋ฆฐ ์ํฐํฐ Version์ ๊ฐ์ ๋ก ์ฆ๊ฐ์์ผ ๋ค๋ฅธ ํธ๋์ญ์ ์์ ํด๋น ์ํฐํฐ๋ฅผ ์ฝ์๋ ์ถฉ๋์ ๋ฐ์์ํด |
PESSIMISTIC_READ | ๋น๊ด์ ๋ฝ ์ฌ์ฉ ์ํฐํฐ์ ๋ฝ์ ๊ฑด ํธ๋์ญ์ ์ด ์๋ฃ๋ ๋๊น์ง ๋ค๋ฅธ ํธ๋์ญ์ ์์ ํด๋น ์ํฐํฐ ๋ณ๊ฒฝ ๋ฐฉ์ง ๋ค๋ฅธ ํธ๋์ญ์ ์์๋ ์ฝ๊ธฐ ๊ฐ๋ฅ |
PESSIMISTIC_WRITE | ๋น๊ด์ ๋ฝ ์ฌ์ฉ ์ํฐํฐ์ ๋ฝ์ ๊ฑด ํธ๋์ญ์ ์ด ์๋ฃ๋ ๋๊น์ง ๋ค๋ฅธ ํธ๋์ญ์ ์์ ํด๋น ์ํฐํฐ๋ฅผ ์ฝ๊ฑฐ๋ ์ธ ์ ์๊ฒ ํจ |
PESSIMISTIC_FORCE_INCREMENT | ๋น๊ด์ ๋ฝ ์ฌ์ฉ ๋ฝ์ด ๊ฑธ๋ฆฐ ์ํฐํฐ Version์ ๊ฐ์ ๋ก ์ฆ๊ฐ์์ผ ๋ค๋ฅธ ํธ๋์ญ์ ์์ ํด๋น ์ํฐํฐ๋ฅผ ์ฝ์๋ ์ถฉ๋์ ๋ฐ์์ํด |
ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ๋ ๋ฒจ ๊ด๋ฆฌ (@Transactional์ isolation ๋ ๋ฒจ)
@Transactional์ isolation ์์ฑ์ ์ฌ์ฉํ์ฌ ํธ๋์ญ์ ๊ฐ ๋์์ฑ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํ ์ ์๋ค.
๊ฒฉ๋ฆฌ ๋จ๊ณ (Isolation Level)
Isolation Level์ค๋ช
READ_UNCOMMITTED | ๊ฐ์ฅ ๋ฎ์ ๊ฒฉ๋ฆฌ ์์ค. ๋ค๋ฅธ ํธ๋์ญ์ ์์ ์์ง ์ปค๋ฐ๋์ง ์์ ๋ณ๊ฒฝ ๋ด์ฉ์ ์ฝ์ ์ ์์ Dirty Read, Non-Repeatable Read, Phantom Read ๋ชจ๋ ๋ฐ์ ๊ฐ๋ฅ READ_UNCOMMITED๋ ๊ธฐ๋ณธ ๊ฒฉ๋ฆฌ ์์ค์ด๋ฉฐ, ์ถ๊ฐ ์ ๋ณด ์์ด๋ ํธ๋์ญ์ ์ฒ๋ฆฌ |
READ_COMMITTED | ๋ค๋ฅธ ํธ๋์ญ์
์์ ์ปค๋ฐ๋ ๋ณ๊ฒฝ ๋ด์ฉ๋ง ์ฝ์ ์ ์์ Dirty Read๋ ๋ฐฉ์งํ์ง๋ง, Non-Repeatable Read์ Phantom Read๋ ๋ฐ์ ๊ฐ๋ฅ SELECT ์ฟผ๋ฆฌ์ FOR UPDATE ํค์๋๋ฅผ ์ถ๊ฐํ์ฌ ๋ฝ์ ๊ฑด๋ค. ์ด๋ฅผ ํตํด ๋ค๋ฅธ ํธ๋์ญ์ ์ด ํด๋น ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ์ง ๋ชปํ๊ฒ ํ๋ค. |
REPEATABLE_READ | ๊ฐ์ ํธ๋์ญ์
๋ด์์ ์ฌ๋ฌ ๋ฒ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ๋ ํญ์ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฅ Dirty Read์ Non-Repeatable Read๋ ๋ฐฉ์งํ์ง๋ง, Phantom Read๋ ๋ฐ์ ๊ฐ๋ฅ SELECT ์ฟผ๋ฆฌ์ FOR UPDATE ํค์๋๋ฅผ ์ถ๊ฐํ์ฌ, ๋ฝ์ ๊ฑด ๋ฐ์ดํฐ๊ฐ ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ํด ๋ณ๊ฒฝ๋์ง ์๋๋ก ํจ ํธ๋์ญ์ ์์ ์์ ์ ๋ฐ์ดํฐ ์ํ๋ฅผ ๊ธฐ๋กํ์ฌ, ํธ๋์ญ์ ์งํ ์ค์ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํด๋ ์ํฅ์ ๋ฐ์ง ์๋๋ก ํจ (ReadView) |
SERIALIZABLE | ๊ฐ์ฅ ๋์ ๊ฒฉ๋ฆฌ ์์ค ํธ๋์ญ์ ๋ค์ ์์ฐจ์ ์ผ๋ก ์คํํ์ฌ ๋์์ฑ ๋ฌธ์ ๋ฅผ ์์ ํ ๋ฐฉ์ง ๋ชจ๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๋ฉฐ, ๊ฐ์ฅ ์์ ํ์ง๋ง ์ฑ๋ฅ ์ ํ๊ฐ ๋ฐ์ํ ์ ์์ SELECT ์ฟผ๋ฆฌ์ FOR UPDATE ํค์๋๋ฅผ ์ถ๊ฐํ์ฌ, ๋ฝ์ ๊ฑด ๋ฐ์ดํฐ๊ฐ ๋ค๋ฅธ ํธ๋์ญ์
์ ์ํด ๋ณ๊ฒฝ๋์ง ์๋๋ก ํจ
ํธ๋์ญ์
์์ ์์ ์ ๋ฐ์ดํฐ ์ํ๋ฅผ ๊ธฐ๋กํ์ฌ, ํธ๋์ญ์
์งํ ์ค์ ๋ค๋ฅธ ํธ๋์ญ์
์ด ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํด๋ ์ํฅ์ ๋ฐ์ง ์๋๋ก ํจ (ReadView)
๋ชจ๋ ์ฟผ๋ฆฌ์ SERIALIZABLE ์ต์
์ ์ถ๊ฐํ์ฌ, ๋ชจ๋ ์ฟผ๋ฆฌ๊ฐ ์์ฐจ์ ์ผ๋ก ์คํ
|
์์ ์ฝ๋
@Service
public class IsolationService {
// READ_UNCOMMITTED
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void processReadUncommitted() {
// ์ปค๋ฐ๋์ง ์์ ๋ฐ์ดํฐ๋ ์ฝ๊ธฐ ๊ฐ๋ฅ
// Dirty Read, Non-Repeatable Read, Phantom Read ๊ฐ๋ฅ
}
// READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processReadCommitted() {
// ์ปค๋ฐ๋ ๋ฐ์ดํฐ๋ง ์ฝ๊ธฐ ๊ฐ๋ฅ
// Dirty Read ๋ฐฉ์ง, Non-Repeatable Read์ Phantom Read ๊ฐ๋ฅ
}
// REPEATABLE_READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processRepeatableRead() {
// ๊ฐ์ ํธ๋์ญ์
๋ด์์ ์ฝ์ ๋ฐ์ดํฐ๋ ํญ์ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฅ
// Dirty Read์ Non-Repeatable Read ๋ฐฉ์ง, Phantom Read ๊ฐ๋ฅ
}
// SERIALIZABLE
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processSerializable() {
// ํธ๋์ญ์
์์ฐจ์ ์คํ, ๋ชจ๋ ๋์์ฑ ๋ฌธ์ ๋ฐฉ์ง
// ๊ฐ์ฅ ๋์ ์์ค์ ๊ฒฉ๋ฆฌ, ์ฑ๋ฅ ์ ํ ๋ฐ์ ๊ฐ๋ฅ
}
}
์ง๊ธ๊น์ง ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ ๊ณตํ๋ 3๊ฐ์ง ๋ฐฉ์์ผ๋ก ๋์์ฑ ์ด์์ ๋ํด ์์๋ดค๋ค.
๋ค์์ ๋ ๋์ค(Redis)๋ก ๋ถ์ฐ ๋ฝ(Distributed Lock)์ ๊ตฌํํ์ฌ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํด ๋ณธ๋ค.
์ฐธ๊ณ
https://velog.io/@lsb156/JPA-Optimistic-Lock-Pessimistic-Lock
https://sjiwon-dev.tistory.com/20