# ๐Ÿš— ์ฐจ๊ทผ์ฐจ๊ทผ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ (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๊ฐœ๋ฅผ ๊ตฌ๋งคํ•˜๊ธธ ์›ํ–ˆ์ง€๋งŒ ๋ถˆํ–‰ํ•˜๊ฒŒ๋„ ์œ„ ํ…Œ์ŠคํŠธ๋Š” ์‹คํŒจํ•œ๋‹ค.

img.png

100๊ฐœ ์ค‘ ๊ณ ์ž‘ 11๊ฐœ์˜ ์ƒํ’ˆ๋งŒ ์ •์ƒ์ ์œผ๋กœ ์ˆ˜๋Ÿ‰์ด ์ค„์–ด๋“  ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์™œ ์ด๋Ÿฐ ์ผ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ผ๊นŒ?

# ๋ฌธ์ œ ์›์ธ ๋ถ„์„

๋ฌธ์ œ์˜ ํ•ต์‹ฌ์€ quantity๋ผ๋Š” ๊ณต์œ  ์ž์›์— ์—ฌ๋Ÿฌ ์š”์ฒญ, ์ฆ‰ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์ˆœ์ฐจ์ ์œผ๋กœ ์ ‘๊ทผํ•˜์—ฌ ์ˆ˜๋Ÿ‰์„ ๊ฐ์†Œ์‹œํ‚ค๊ธธ ๊ธฐ๋Œ€ํ•˜์ง€๋งŒ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ์˜๋„์ ์œผ๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๋‹ค. ๊ทธ๋ฆผ์œผ๋กœ ํ‘œํ˜„ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

img.png

๋‘๊ฐœ์˜ ์š”์ฒญ์ด ๋“ค์–ด์™€ ์ˆ˜๋Ÿ‰์ด 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๋ฅผ ์–ด๋””์—, ์–ด๋–ค ๋ฉ”์„œ๋“œ์— ๋ช…์‹œํ•˜๋ƒ์— ๋”ฐ๋ผ ๋‹ค์–‘ํ•˜๊ฒŒ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ž ์ด์ œ ๋ชจ๋“  ๋™์‹œ์„ฑ ์ด์Šˆ๋Š” ํ•ด๊ฒฐ๋˜์—ˆ์„ ๊ฒƒ์ด๋‹ค. ๋‹ค์‹œ ํ•œ ๋ฒˆ ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ด๋ณด์ž.

img.png

์•„์‰ฝ๊ฒŒ๋„ ์œ„ ํ…Œ์ŠคํŠธ๋Š” ์‹คํŒจํ•œ๋‹ค. ๋ถ„๋ช… 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();
        }
    }
}

img.png

ํ…Œ์ŠคํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ˆ˜ํ–‰๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

# ์ •๋ฆฌ

์ง€๊ธˆ ๊นŒ์ง€ synchronized ํ‚ค์›Œ๋“œ๋ฅผ ํ†ตํ•ด ๋™์‹œ์„ฑ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์•˜๋‹ค. synchronized๋Š” ์†์‰ฝ๊ฒŒ ์ ์šฉ์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๋‹ค์ค‘ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ ํ•ฉํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. ์ด๊ฒƒ์€ ํ•˜๋‚˜์˜ ์„œ๋ฒ„ ๋‚ด์—์„œ๋งŒ ๋™์‹œ์„ฑ ์ด์Šˆ๋ฅผ ๋ง‰์•„์ฃผ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ฒฐ๊ตญ ๋‹ค์ˆ˜์˜ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ผ๋Š” ๊ณต์œ  ์ž์›์— ์ ‘๊ทผ์„ ์ œ์–ดํ•˜๋Š”๋ฐ๋Š” ์ ํ•ฉํ•˜์ง€ ์•Š์€ ๋ฐฉ์‹์ด๋‹ค.

๋งŒ์•ฝ ์Šค์ผ€์ผ ์•„์›ƒ์„ ์ง„ํ–‰ํ•˜์ง€ ์•Š๊ณ  ํ•˜๋‚˜์˜ ์„œ๋ฒ„์—์„œ ๋ชจ๋“  ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” synchronized๋Š” ์ข‹์€ ๋Œ€์•ˆ์ด ๋  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค๋งŒ @Transactional ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ํ”„๋ก์‹œ ๊ตฌ์กฐ์— ๋Œ€ํ•ด ์—ผ๋‘ํ•ด๋‘๊ณ  ์ ์ ˆํžˆ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค. ๋‹ค์Œ ์‹œ๊ฐ„์—๋Š” ๋น„๊ด€์  ๋ฝ์„ ํ†ตํ•ด ๋™์‹œ์„ฑ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๋ ค ํ•œ๋‹ค.

# References.

๐Ÿ”’ Java ๋กœ ๋™๊ธฐํ™”๋ฅผ ํ•ด๋ณด์ž! (opens new window)
์žฌ๊ณ ์‹œ์Šคํ…œ์œผ๋กœ ์•Œ์•„๋ณด๋Š” ๋™์‹œ์„ฑ์ด์Šˆ ํ•ด๊ฒฐ๋ฐฉ๋ฒ• (opens new window)

#๋™์‹œ์„ฑ #synchronized
last updated: 12/14/2022, 5:20:01 PM