# ๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (1)
๐ ๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (1) - synchronized (opens new window)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (2) - Pessimistic Lock (opens new window)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (3) - Optimistic Lock (opens new window)
๐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋์์ฑ ์ด์ ํด๊ฒฐํ๊ธฐ (4) - Named Lock (opens new window)
์์ฑ์ ์ฌ์ฉ๋ ์์ ์ฝ๋๋ concurrency-synchronized (opens new window)์์ ํ์ธํด๋ณผ ์ ์๋ค.
์๋ฒ ๊ฐ๋ฐ์ ํ๋ค๋ณด๋ฉด ์ฌ๋ฌ ์์ฒญ์์ ๋์์ ๊ณต์ ์์์ ํ์ฉํ๋ ๊ฒฝ์ฐ ๋์์ฑ ์ด์๊ฐ ๋ฐ์ํ ์ ์๋ค. ํนํ Java๋ฅผ ํ์ฉํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฒฝ์ฐ ๋ฉํฐ ์ค๋ ๋๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ์ํด์๋ ๊ณต์ ์์์ ๋ํ ๊ด๋ฆฌ๊ฐ ํ์ํ๋ค.
๋์์ฑ ์ด์ ํด๊ฒฐ์ ์ํด์๋ ๋ค์ํ ๋ฐฉ๋ฒ์ด ์กด์ฌํ๋ค. ์์ฒญ ์ค๋ ๋๊ฐ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋๋ก ์ ํํ๊ฑฐ๋ ๋น๊ด์ ๋ฝ๊ณผ ๋๊ด์ ๋ฝ์ ํ์ฉํ๊ฑฐ๋ ์ธ๋ถ์ ํน์ ์ ์ฅ์์ ๋ฝ์ ๋ํ ๊ด๋ฆฌ๋ฅผ ํตํด ๊ณต์ ์์์ ๋์์ ์ ์ ํ์ง ๋ชปํ๊ฒ ํ๋ ๋ฑ ๋ค์ํ ๋ฐฉ๋ฒ์ ํตํด ๋ง์ ์ ์๋ค. ์ด๋ฒ ์๊ฐ์๋ ๊ทธ ์ค์์๋ ์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ฒจ์์ ์์ฐจ์ ์ผ๋ก ์์์ ์ ๊ทผํ์ฌ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๋ ค ํ๋ค.
# ์์ ์ฝ๋
๋จผ์ ์ด๋ฒ ๊ธ์์ ์ฌ์ฉํ๋ ์์ ์ฝ๋๋ฅผ ์ดํด๋ณด์. ์๋๋ ํน์ ์ํ์ ๋ํ๋ด๊ธฐ ์ํ Product ์ํฐํฐ์ด๋ค. Product๋ ์ด๋ฆ๊ณผ ํ์ ๋ ๊ฐ์๋ฅผ ๋ํ๋ด๋ quantity
๋ฅผ ๊ฐ์ง๊ณ ์๋ค.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private Long name;
@Column(name = "quantity")
private Long quantity;
protected Product() {
}
public Product(final Long name, final Long quantity) {
this.name = name;
this.quantity = quantity;
}
public void decrease(final Long quantity) {
if (this.quantity - quantity < 0) {
throw new RuntimeException("์๋์ 0๊ฐ ๋ฏธ๋ง์ด ๋ ์ ์์ต๋๋ค.");
}
this.quantity -= quantity;
}
// getter...
}
decrease()
๋ฉ์๋๋ฅผ ํตํด ์๋์ ๊ฐ์ ์ํฌ ์ ์๋ค.
Product ์ํฐํฐ ์กฐ์์ ์ํ ProductRepository
์ด๋ค.
public interface ProductRepository extends JpaRepository<Product, Long> {
}
์ด์ ์ํ์ ๊ตฌ๋งคํ๋ค๊ณ ๊ฐ์ ํ๊ณ ์๋์ ๊ฐ์ ์ํค๋ ๋น์ฆ๋์ค ๋ก์ง์ ProductService
์ ์์ฑํ๋ค.
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(final ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public void purchase(final Long id, final Long quantity) {
var foundProduct = getProduct(id);
foundProduct.decrease(quantity);
}
private Product getProduct(final Long id) {
return productRepository.findById(id)
.orElseThrow(NoSuchElementException::new);
}
}
๋น๊ต์ ๊ฐ๋จํ ๋ก์ง์ ๊ฐ์ง๊ณ ์๋ค. ํน์ id๋ฅผ ๊ฐ์ง Product๋ฅผ ์กฐํํ๊ณ ์๋์ ๊ฐ์์ํจ๋ค. ๋ ๊ณผ์ ์ ์ผ๋ จ์ ํธ๋์ญ์
์์ ์ด๋ฃจ์ด์ ธ์ผ ํ๊ธฐ ๋๋ฌธ์ @Transactional
์ ํ์ฉํ๋ค.
์ค๋น๋ ๋๋ฌ๋ค. ์ด์ ๋์์ฑ ์ด์๋ฅผ ๋ฐ์์์ผ ๋ฌธ์ ๋ฅผ ํ์ธํด๋ณธ๋ค. ์๋๋ ๋์์ฑ ์ด์ ํ์ธ์ ์ํ ํ ์คํธ ์ฝ๋์ด๋ค.
@SpringBootTest
@DisplayNameGeneration(ReplaceUnderscores.class)
class ProductServiceTest {
// ...
@Test
void ๋์์_100๊ฐ์_์ํ์_๊ตฌ๋งคํ๋ค() throws InterruptedException {
var product = productRepository.save(new Product("์นํจ", 100L));
var executorService = Executors.newFixedThreadPool(10);
var countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
executorService.submit(() -> process(product, countDownLatch));
}
countDownLatch.await();
var actual = productRepository.findById(product.getId()).orElseThrow();
assertThat(actual.getQuantity()).isEqualTo(0L);
}
private void process(final Product product, final CountDownLatch countDownLatch) {
try {
productService.purchase(product.getId(), 1L);
} finally {
countDownLatch.countDown();
}
}
}
์์ธํ ์ฝ๋ ์ค๋ช
์ ์๋ตํ๋ค. ํต์ฌ์ ์๊ฐ์ ์ผ๋ก 100๊ฐ์ ์์ฒญ์ ๋ณด๋ธ ๋ค 100๊ฐ์ ์์ฒญ์ด ๋ชจ๋ ๋๋ ๋ ๊น์ง await()
ํ๋ค. ์ดํ ์๋์ ์กฐํํ ๋ค ๋ช ๊ฐ์ ์ํ์ด ํ๋งค๋์๋์ง ํ์ธํ๋ค.
์ํ 100๊ฐ๋ฅผ ๊ตฌ๋งคํ๊ธธ ์ํ์ง๋ง ๋ถํํ๊ฒ๋ ์ ํ ์คํธ๋ ์คํจํ๋ค.
100๊ฐ ์ค ๊ณ ์ 11๊ฐ์ ์ํ๋ง ์ ์์ ์ผ๋ก ์๋์ด ์ค์ด๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ์ ์ด๋ฐ ์ผ์ด ๋ฐ์ํ๋ ๊ฒ์ผ๊น?
# ๋ฌธ์ ์์ธ ๋ถ์
๋ฌธ์ ์ ํต์ฌ์ quantity
๋ผ๋ ๊ณต์ ์์
์ ์ฌ๋ฌ ์์ฒญ, ์ฆ ์ฌ๋ฌ ์ค๋ ๋๊ฐ ๋์์ ์ ๊ทผํ ์ ์๋ค๋ ๊ฒ์ด๋ค. ์์ฐจ์ ์ผ๋ก ์ ๊ทผํ์ฌ ์๋์ ๊ฐ์์ํค๊ธธ ๊ธฐ๋ํ์ง๋ง ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์ ์๋์ ์ผ๋ก ๋์ํ์ง ์์
๊ฒ์ด๋ค. ๊ทธ๋ฆผ์ผ๋ก ํํํ๋ฉด ์๋์ ๊ฐ๋ค.
๋๊ฐ์ ์์ฒญ์ด ๋ค์ด์ ์๋์ด 98๋ก ๊ฐ์ํ๊ธธ ๊ธฐ๋ ํ์ง๋ง ์์์ ๋์ ์ ๊ทผํ๋ ์์ ์ ๊ฐ์ ์๋์ ์ฝ๊ฒ ๋๋ฏ๋ก ์
๋ฐ์ดํธ ํ๋ ์์ ์ ๊ฐฑ์ ์์ค
์ด ๋ฐ์ํ๊ฒ ๋๋ค.
# synchronized
์ด๊ฒ์ Java์ synchronized
ํค์๋๋ฅผ ํตํด ์๊ณ ์์ญ
์ ์ค์ ํ์ฌ ํด๊ฒฐํ ์ ์๋ค. synchronized
๋ฅผ ํ์ฉํ๋ฉด ์ฌ๋ฌ ์ค๋ ๋์์ ๊ณต์ ์์์ ์ฌ์ฉํ ๋ ์ต์ด์ ์ ๊ทผํ ์ค๋ ๋๋ฅผ ์ ์ธํ๊ณ
๋๋จธ์ง ์ค๋ ๋๋ ์ ๊ทผํ ์
์๋๋ก ์ ํํ ์ ์๋ค. Java์์๋ Monitor ๋๊ตฌ๋ฅผ ํตํด ๊ฐ์ฒด์ Lock์ ๊ฑธ์ด ์ํธ๋ฐฐ์
๋ฅผ ํ ์ ์๋ค. ์์ธํ ๋ด์ฉ์ ์ํธ๋ฐฐ์ ๊ธฐ๋ฒ์ ๋ํด ์์๋ณด๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
์๊ณ ์์ญ์ด๋, ๋ฉํฐ ์ค๋ ๋ ํ๊ฒฝ์์ ํ๋์ ์ค๋ ๋๋ง ์คํํ ์ ์๋ ์์ญ์ ์ผ์ปซ๋๋ค.
ํต์ฌ์ synchronized
๋ฅผ ์ฌ์ฉํ๋ฉด ์ค์ง ํ๋์ ์ค๋ ๋์์๋ง ์๊ณ ์์ญ
์ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ด๋ค. ์ ์ฉํ๋ฉด ์๋์ ๊ฐ๋ค.
@Service
public class ProductService {
// ...
@Transactional
public synchronized void purchase(final Long id, final Long quantity) {
var foundProduct = getProduct(id);
foundProduct.decrease(quantity);
}
private Product getProduct(final Long id) {
return productRepository.findById(id)
.orElseThrow(NoSuchElementException::new);
}
}
๋ฉ์๋ ์ ์ธ๋ถ์ synchronized
ํค์๋๋ฅผ ์ถ๊ฐํ๋ค. ํด๋น ํค์๋๋ฅผ ํตํด ์ด ๋ฉ์๋๋ฅผ ๊ฐ์ง ๊ฐ์ฒด๋ฅผ ๊ธฐ์ค์ผ๋ก ๋๊ธฐํ ๊ณผ์ ์ ์งํํ๋ค. ProductService
๊ฐ์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฑ๊ธํด ๋ฐฉ์์ผ๋ก
์์ฑ๋๊ธฐ ๋๋ฌธ์ ํด๋น ๊ฐ์ฒด์ ๋ฉ์๋๋ ํ ๋ฒ์ ํ๋์ ์ค๋ ๋๋ง ์ ๊ทผํ ์ ์๋ค๊ณ ์ดํดํ๋ฉด ๋๋ค. ์ด ๋ฐ์๋ synchronized
๋ฅผ ์ด๋์, ์ด๋ค ๋ฉ์๋์ ๋ช
์ํ๋์ ๋ฐ๋ผ ๋ค์ํ๊ฒ ๋์ํ ์ ์๋ค.
์ ์ด์ ๋ชจ๋ ๋์์ฑ ์ด์๋ ํด๊ฒฐ๋์์ ๊ฒ์ด๋ค. ๋ค์ ํ ๋ฒ ์ ํ ์คํธ๋ฅผ ์คํํด๋ณด์.
์์ฝ๊ฒ๋ ์ ํ
์คํธ๋ ์คํจํ๋ค. ๋ถ๋ช
synchronized
๋ฅผ ํตํด ์๊ณ ์์ญ๊น์ง ์ค์ ํ๋๋ฐ ์ ๋์์ฑ ์ด์๊ฐ ๋ฐ์ํ ๊น?
# ๋ฌธ์ ์์ธ ๋ค์ ๋ถ์
๋ฌธ์ ์ ์์ธ์ @Transactional
์ ํน์ํ ๊ตฌ์กฐ ๋๋ฌธ์ด๋ค. Spring์ ๊ธฐ๋ณธ์ ์ผ๋ก ํ๋ก์๋ฅผ ํตํด ๋ถ๊ฐ๊ธฐ๋ฅ์ธ ํธ๋์ญ์
์ ๊ตฌํํ๋ค. ๊ฐ๋จํ pseudocode๋ก ํํํ๋ฉด ์๋์ ๊ฐ๋ค.
public class ProductServiceProxy {
private final ProductService productService;
// ...
public void purchase(final Long id, Long quantity) {
startTransaction(); // 1) ํธ๋์ญ์
์์
stockService.purchase(id, quantity); // 2) ์๊ณ ์์ญ
endTransaction(); // 3 ํธ๋์ญ์
๋
}
}
์ฌ๋ฌ ์ค๋ ๋๋ ๋์์ ํธ๋์ญ์
์ ์์ํ ์ ์๋ค. ์ดํ ์๊ณ ์์ญ์ ์ง์
ํ๋ฉด ์์ฐจ์ ์ผ๋ก purchase()
๋ฉ์๋๋ฅผ ์ํํ ๊ฒ์ด๋ค. ํ์ง๋ง purchase()
๋ ๋ฐ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์๋์ง ์๋๋ค. 3
์
์ ๊ทผํ์ ๋ ๋น๋ก์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์๋๊ธฐ ๋๋ฌธ์ด๋ค. ๊ฒฐ๊ตญ ๋ฐ์ ์ด์ ์ ์๊ณ ์์ญ์ ์ ๊ทผํ๋ ์ค๋ ๋ ๋๋ฌธ์ ๊ฐฑ์ค ์์ค
์ด ๋์ผํ๊ฒ ๋ฐ์ํ๋ ๊ฒ์ด๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ์ ํธ๋์ญ์
์ ๋ฒ์๋ณด๋ค ์๊ณ ์์ญ์ ๋ฒ์๋ฅผ ๋ ๋๊ฒ ๊ฐ์ ธ๊ฐ๋ ๊ฒ์ด๋ค. ProductService
๋ณด๋ค ์์ ๊ณ์ธต์์ ์๊ณ ์์ญ์ ์ง์ ํ ๋ค purchase()
๋ฅผ ํธ์ถํ๋ค.
@Component
public class SynchronizedProductService {
private final ProductService productService;
public SynchronizedProductService(final ProductService productService) {
this.productService = productService;
}
public synchronized void purchase(final Long id, final Long quantity) {
productService.purchase(id, quantity);
}
}
์์ ๊ณ์ธต์ ์ถ๊ฐํ ์ด์ ๋ ๊ฐ์ ๊ฐ์ฒด ๋ด์์ ๋ด๋ถ ํธ์ถ์ ์งํํ ๊ฒฝ์ฐ ํ๋ก์ ๋ก์ง์ ์ ์์ ์ผ๋ก ํ์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค. ์ด์ ๋ํ ํค์๋๋ก ํธ๋์ญ์ ๋ด๋ถ ํธ์ถ์ ๋ํด ์์๋ณด๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
ํ ์คํธ ์ฝ๋๋ ์์ ํ๋ค.
@SpringBootTest
@DisplayNameGeneration(ReplaceUnderscores.class)
class ProductServiceTest {
// ...
@Test
void ๋์์_100๊ฐ์_์ํ์_๊ตฌ๋งคํ๋ค() throws InterruptedException {
var product = productRepository.save(new Product("์นํจ", 100L));
var executorService = Executors.newFixedThreadPool(10);
var countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
executorService.submit(() -> process(product, countDownLatch));
}
countDownLatch.await();
var actual = productRepository.findById(product.getId()).orElseThrow();
assertThat(actual.getQuantity()).isEqualTo(0L);
}
private void process(final Product product, final CountDownLatch countDownLatch) {
try {
synchronizedProductService.purchase(product.getId(), 1L);
} finally {
countDownLatch.countDown();
}
}
}
ํ ์คํธ๊ฐ ์ ์์ ์ผ๋ก ์ํ๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
# ์ ๋ฆฌ
์ง๊ธ ๊น์ง synchronized
ํค์๋๋ฅผ ํตํด ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด์๋ค. synchronized
๋ ์์ฝ๊ฒ ์ ์ฉ์ด ๊ฐ๋ฅํ์ง๋ง ๋ค์ค ์ ํ๋ฆฌ์ผ์ด์
ํ๊ฒฝ์์๋ ์ ํฉํ์ง ์์ ์ ์๋ค. ์ด๊ฒ์
ํ๋์ ์๋ฒ ๋ด์์๋ง ๋์์ฑ ์ด์๋ฅผ ๋ง์์ฃผ๊ธฐ ๋๋ฌธ์ด๋ค. ๊ฒฐ๊ตญ ๋ค์์ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ผ๋ ๊ณต์ ์์์ ์ ๊ทผ์ ์ ์ดํ๋๋ฐ๋ ์ ํฉํ์ง ์์ ๋ฐฉ์์ด๋ค.
๋ง์ฝ ์ค์ผ์ผ ์์์ ์งํํ์ง ์๊ณ ํ๋์ ์๋ฒ์์ ๋ชจ๋ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ์๋ synchronized
๋ ์ข์ ๋์์ด ๋ ์ ์๋ค. ๋ค๋ง @Transactional
ํจ๊ป ์ฌ์ฉํ ๋๋ ํ๋ก์ ๊ตฌ์กฐ์ ๋ํด
์ผ๋ํด๋๊ณ ์ ์ ํ ์ ์ฉํด์ผ ํ๋ค. ๋ค์ ์๊ฐ์๋ ๋น๊ด์ ๋ฝ์ ํตํด ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๋ ค ํ๋ค.
# References.
๐ Java ๋ก ๋๊ธฐํ๋ฅผ ํด๋ณด์! (opens new window)
์ฌ๊ณ ์์คํ
์ผ๋ก ์์๋ณด๋ ๋์์ฑ์ด์ ํด๊ฒฐ๋ฐฉ๋ฒ (opens new window)