# ๐Ÿš— ์ฐจ๊ทผ์ฐจ๊ทผ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ (3)

๐Ÿš— ์ฐจ๊ทผ์ฐจ๊ทผ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ (1) - synchronized (opens new window)
๐Ÿš— ์ฐจ๊ทผ์ฐจ๊ทผ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ (2) - Pessimistic Lock (opens new window)
๐Ÿ‘‰ ๐Ÿš— ์ฐจ๊ทผ์ฐจ๊ทผ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ (3) - Optimistic Lock (opens new window)
๐Ÿš— ์ฐจ๊ทผ์ฐจ๊ทผ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ (4) - Named Lock (opens new window)

์ž‘์„ฑ์— ์‚ฌ์šฉ๋œ ์˜ˆ์ œ ์ฝ”๋“œ๋Š” concurrency-optimistic-lock (opens new window)์—์„œ ํ™•์ธํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.

# ๋‚™๊ด€์  ๋ฝ

๋‚™๊ด€์  ๋ฝ์ด๋ž€ ํŠธ๋žœ์žญ์…˜ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ๋‚™๊ด€์ ์œผ๋กœ ๊ฐ€์ •ํ•œ๋‹ค. ์ถ”๊ฐ€์ ์ธ version์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ถฉ๋Œํ•  ๊ฒฝ์šฐ ๋กค๋ฐฑ์„ ์ง„ํ–‰ํ•œ๋‹ค. ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ์ถฉ๋Œ์ด ์ผ์–ด๋‚˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋™์‹œ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ์ด์ ์ด ๋งŽ๋‹ค.

# @Version

๋‚™๊ด€์  ๋ฝ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ์ง€๋งŒ ๋Œ€ํ‘œ์ ์œผ๋กœ๋Š” @Version ์• ๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค. @Version์€ ๋‚™๊ด€์  ๋ฝ ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์—”ํ„ฐํ‹ฐ ํด๋ž˜์Šค์˜ ๋ฒ„์ „ ํ•„๋“œ ๋˜๋Š” ์†์„ฑ์„ ์ง€์ •ํ•œ๋‹ค. ์ด ๋ฒ„์ „์€ ๋ณ‘ํ•ฉ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜๊ณ  ๋‚™๊ด€์  ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ ๋œ๋‹ค. @Version์€ ํด๋ž˜์Šค๋‹น ํ•˜๋‚˜์˜ ์†์„ฑ ๋˜๋Š” ํ•„๋“œ๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. ๋‘˜ ์ด์ƒ์˜ ๋ฒ„์ „ ๋˜๋Š” ์†์„ฑ ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

@Version์€ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”์— ๋งคํ•‘๋˜์–ด์•ผ ํ•œ๋‹ค. ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”์ด ์•„๋‹Œ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ๋งคํ•‘ํ•  ์ˆ˜ ์—†๋‹ค. @Version์ด ์ง€์›๋˜๋Š” type์€ int, Integer, short , long, Long, java.sql.Timestamp์ด๋‹ค. @Version์ด ์‚ฌ์šฉ๋œ ์—”ํ‹ฐํ‹ฐ๋Š” ์ˆ˜์ •๋  ๋•Œ ์ž๋™์œผ๋กœ ๋ฒ„์ „์ด ์ฆ๊ฐ€ํ•˜๋ฉฐ ์ˆ˜์ •ํ•  ๋•Œ ์กฐํšŒ ์‹œ์ ๊ณผ ๋ฒ„์ „์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฒ„์ „์€ ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ • ์‹œ์ ์— ์ตœ์‹ ํ™”๋˜๋ฉฐ JPA์— ์˜ํ•ด ์ž๋™์ ์œผ๋กœ ๊ด€๋ฆฌ๋œ๋‹ค.

update product
set name     = ?,
    quantity = ?,
    version  = ?
where id = ?
  and version = ?

# ๋‚™๊ด€์  ๋ฝ ์ ์šฉ

์˜ˆ์ œ๋Š” ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ด์ „์— ์ž‘์„ฑํ•œ Product ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ™œ์šฉํ•œ๋‹ค. ๋จผ์ € @Version์ด๋ผ๋Š” ์• ๋…ธํ…Œ์ด์…˜์„ ํ™œ์šฉํ•˜์—ฌ version ๊ด€๋ จ ์นผ๋Ÿผ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "quantity")
    private Long quantity;

    @Version
    private Long version;

    // ...
}

version์ด๋ผ๋Š” ์นผ๋Ÿผ์ด ์ถ”๊ฐ€์ ์œผ๋กœ ์ƒ์„ฑ๋˜์–ด ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋œ๋‹ค. ์ด๋Ÿฌํ•œ Version ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ตœ์ดˆ์— ์ ์šฉ๋œ ์ปค๋ฐ‹๋งŒ ๋ฐ˜์˜๋˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ๋ฌด์‹œ๋œ๋‹ค.

create table product
(
    id       bigint not null auto_increment,
    name     varchar(255),
    quantity bigint,
    version  bigint,
    primary key (id)
) 

์•„๋ž˜๋Š” ๊ตฌ๋งค์™€ ๊ด€๋ จ๋œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋‹ด๊ธด 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.findByIdWithOptimisticLock(id)
                .orElseThrow(NoSuchElementException::new);
    }
}

์ด์ œ ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.

@SpringBootTest
@DisplayNameGeneration(ReplaceUnderscores.class)
class ProductServiceTest {

    private final ProductRepository productRepository;
    private final ProductService productService;

    @Autowired
    ProductServiceTest(final ProductRepository productRepository,
                       final ProductService productService) {
        this.productRepository = productRepository;
        this.productService = productService;
    }

    @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);
    }
}

img.png

์•ˆํƒ€๊น๊ฒŒ๋„ ๋‚™๊ด€์  ๋ฝ์€ WHERE ์กฐ๊ฑด์— ์˜ํ•ด ์ˆ˜์ •ํ•  ๋Œ€์ƒ์ด ์—†๋Š” ๊ฒฝ์šฐ ๋ฒ„์ „์ด ์ฆ๊ฐ€ ํ–ˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ์˜ˆ์™ธ๋ฅผ ๋˜์ง„๋‹ค.

Spring Data JPA๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ JPA์— ๋Œ€ํ•œ ๊ตฌํ˜„์ฒด๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ hibernate๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค. ์ด ์˜ˆ์™ธ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ hibernate์—์„œ ๋˜์ง€๋Š” StaleStateException ์˜ˆ์™ธ๋ฅผ Spring์˜ HibernateJpaDialect์˜ convertHibernateAccessException() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด org.springframework.dao ๊ณ„์ธต์—์„œ ์ ์ ˆํ•œ ์˜ˆ์™ธ( DataAccessException ํƒ€์ž…)๋กœ ๋ณ€ํ™˜๋œ๋‹ค.

package org.springframework.orm.jpa.vendor;

// ...
public class HibernateJpaDialect extends DefaultJpaDialect {
    // ...
    protected DataAccessException convertHibernateAccessException(HibernateException ex) {
        // ...
        if (ex instanceof StaleStateException) {
            return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex);
        }
        // ...
    }
}

๊ฒฐ๊ตญ ๋‚™๊ด€์  ๋ฝ์€ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๊ธฐ ๋•Œ๋ฌธ์— ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์žฌ์‹œ๋„ ํ•˜๋Š” ๋กœ์ง์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ ์ถฉ๋Œ์ด ์ผ์–ด๋‚œ ์‹œ์ ์— ๋˜์ง€๋Š” ์˜ˆ์™ธ๋ฅผ ์บ์น˜ํ•˜์—ฌ ๋‹ค์‹œ ๋กœ์ง ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๊ฒƒ์ด๋‹ค.

@Component
public class OptimisticLockProductFacade {

    private final ProductService productService;

    public OptimisticLockProductFacade(final ProductService productService) {
        this.productService = productService;
    }

    public void purchase(final Long id, final Long quantity) throws InterruptedException {
        while (true) {
            try {
                productService.purchase(id, quantity);
                break;
            } catch (final ObjectOptimisticLockingFailureException e) {
                Thread.sleep(100);
            }
        }
    }
}

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

img.png

์„ฑ๊ณต์ ์œผ๋กœ ํ†ต๊ณผํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

# ์ถ”ํ›„ ์•Œ์•„๋ณผ ๊ฒƒ

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Lock(LockModeType.OPTIMISTIC)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Optional<Product> findByIdWithOptimisticLock(final Long id);
}

์œ„์™€ ๊ฐ™์ด @Lock(LockModeType.OPTIMISTIC)์„ ์‚ฌ์šฉํ•˜์—ฌ ์กฐํšŒ ์ฟผ๋ฆฌ์— ๋‚™๊ด€์  ๋ฝ์„ ๋ช…์‹œํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด version์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•œ SELECT ์ฟผ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€๋œ๋‹ค.

select version as version_
from product
where id = ?

์ด ์ฟผ๋ฆฌ์˜ ์šฉ๋„๋Š” ๋ฌด์—‡์ผ๊นŒ? ๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด ๋ฒ„์ „์ด ์ง€์ •๋œ ๊ฐ์ฒด์— ๋Œ€ํ•ด @Lock(LockModeType.OPTIMISTIC) ์œ ํ˜•์˜ ์ž ๊ธˆ์„ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ Drity Read ์™€ Non-repeatable read๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ์กฐ์‹ฌํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์–ธ๊ธ‰๋˜์–ด ์žˆ๋‹ค. ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•ด ์•„์ง ๋ช…ํ™•ํ•˜๊ธฐ ์ดํ•ด๊ฐ€ ๋˜์ง€ ์•Š์•„ ๋šœ๋ ทํ•œ ์ ์šฉ ์‚ฌ๋ก€๊ฐ€ ๋– ์˜ค๋ฅด์ง€ ์•Š๋Š”๋‹ค.

# ์ •๋ฆฌ

๋‚™๊ด€์  ๋ฝ์€ @Version์ด๋ผ๋Š” ์• ๋…ธํ…Œ์ด์…˜์œผ๋กœ ์†์‰ฝ๊ฒŒ ์ ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ๋˜ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ง์ ‘ ๋ฝ์„ ๊ฑฐ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ์ƒ์— ์ด์ ์ด ๋งŽ๋‹ค.

ํ•˜์ง€๋งŒ UPDATE๊ฐ€ ์‹คํŒจ ํ–ˆ์„ ๋•Œ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๋Š” ์žฌ์‹œ๋„ ๋กœ์ง์„ ์ง์ ‘ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค. ๋งŒ์•ฝ ์ถฉ๋Œ์ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๋‚™๊ด€์  ๋ฝ์€ ์ข‹์€ ํ•ด๊ฒฐ์ฑ…์ด ์•„๋‹ ์ˆ˜ ์žˆ๋‹ค.

๋‹ค์Œ ์‹œ๊ฐ„์—๋Š” ์ถ”๊ฐ€์ ์ธ DataSource๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ณ„๋„์˜ ๊ณต๊ฐ„์— ๋ฝ์„ ๊ด€๋ฆฌํ•˜๋Š” Named Lock์„ ํ†ตํ•ด ๋™์‹œ์„ฑ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด๋ ค ํ•œ๋‹ค.

# References.

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

#๋™์‹œ์„ฑ #๋‚™๊ด€์  ๋ฝ
last updated: 12/14/2022, 5:20:01 PM