๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/Java

์ž๋ฐ”์—์„œ ๋™์‹œ์„ฑ์„ ํ•ด๊ฒฐํ•˜๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•๊ณผ Redis์˜ ๋ถ„์‚ฐ๋ฝ

by ์•ˆ์ฃผํ˜• 2023. 1. 8.

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

 

[OS] ํ”„๋กœ์„ธ์Šค ๋™๊ธฐํ™”(Process Synchronization)

์„œ๋ก  ํ˜‘๋ ฅ์  ํ”„๋กœ์„ธ์Šค๋Š” ์‹œ์Šคํ…œ ๋‚ด์—์„œ ์‹คํ–‰ ์ค‘์ธ ๋‹ค๋ฅธ ํ”„๋กœ์„ธ์Šค์˜ ์‹คํ–‰์— ์˜ํ–ฅ์„ ์ฃผ๊ฑฐ๋‚˜ ์˜ํ–ฅ์„ ๋ฐ›๋Š” ํ”„๋กœ์„ธ์Šค์ž…๋‹ˆ๋‹ค. ํ˜‘๋ ฅ์  ํ”„๋กœ์„ธ์Šค๋Š” ๋…ผ๋ฆฌ ์ฃผ์†Œ ๊ณต๊ฐ„(์ฆ‰, ์ฝ”๋“œ ๋ฐ ๋ฐ์ดํ„ฐ)์„ ์ง์ ‘ ๊ณต์œ ํ•˜๊ฑฐ

dkswnkk.tistory.com

๊ณต์œ ์ž์›์— ๋Œ€ํ•ด ๋™์‹œ์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ ‘๊ทผํ•˜์—ฌ ์ƒ๊ธฐ๋Š” ๊ฒฝ์Ÿ ์ƒํ™ฉ(race condition)์„ ์šฐ๋ฆฌ๋Š” ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ผ๊ณ ๋„ ํ•˜๋ฉฐ, ๋” ์ž์„ธํžˆ๋Š” ๋™์ผํ•œ ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ์— ๋‘ ๊ฐœ ์ด์ƒ์˜ ์Šค๋ ˆ๋“œ, ํ˜น์€ ์„ธ์…˜์—์„œ ๊ฐ€๋ณ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋™์‹œ์— ์ œ์–ดํ•  ๋•Œ ๋‚˜ํƒ€๋Š” ๋ฌธ์ œ๋กœ, ํ•˜๋‚˜์˜ ์„ธ์…˜์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ • ์ค‘์ผ ๋•Œ, ๋‹ค๋ฅธ ์„ธ์…˜์—์„œ ์ˆ˜์ • ์ „์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•จ์œผ๋กœ์จ ๋ฐ์ดํ„ฐ์˜ ์ •ํ•ฉ์„ฑ์ด ๊นจ์ง€๋Š” ๋ฌธ์ œ๋ฅผ ๋งํ•ฉ๋‹ˆ๋‹ค

๋ฐฑ์—”๋“œ ์ฆ‰ ์„œ๋ฒ„๊ฐœ๋ฐœ์ž์—๊ฒŒ ์ด๋Ÿฌํ•œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋Š” ๋งค์šฐ ์ค‘์š”ํ•œ๋ฐ, ์œ„์˜ ํฌ์ŠคํŠธ ์„œ๋ฌธ์• ๋„ ๋‚˜์™€์žˆ๋“ฏ์ด ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๋™์‹œ์„ฑ ๋ฌธ์ œ์˜ ๋Œ€ํ‘œ์ ์ธ ์˜ˆ

  1. ๊ณต์œ  ์ž์›์ธ ์ „์—ญ ๋ณ€์ˆ˜ ์˜ˆ๊ธˆ 10๋งŒ ์›์ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•œ๋‹ค.
  2. ํ”„๋กœ์„ธ์Šค P1์€ ์˜ˆ๊ธˆ 10๋งŒ ์›์„ ํ™•์ธํ•œ ์ƒํ™ฉ์—์„œ ํ”„๋กœ์„ธ์Šค P2๊ฐ€ ์˜ˆ๊ธˆ 5๋งŒ ์›์„ ์ž…๊ธˆํ•˜์—ฌ ์ด 15๋งŒ ์›์˜ ์˜ˆ๊ธˆ์„ ์ €์žฅํ•œ๋‹ค.
  3. ํ•˜์ง€๋งŒ ํ”„๋กœ์„ธ์Šค P1์ž…์žฅ์—์„œ๋Š” ์•„์ง ์˜ˆ๊ธˆ์ด 10๋งŒ ์› ์ด๊ธฐ์— 10๋งŒ ์›์„ ์ถ”๊ฐ€ํ•˜๋”๋ผ๋„ 15 + 10 = 25๋ผ๋Š” ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹Œ 10 + 10 = 20์ด๋ผ๋Š” ์ด์˜ˆ๊ธˆ์ด ์ €์žฅ๋˜๊ฒŒ ๋œ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์ด์ œ ์–ด๋–ป๊ฒŒ ์ด๋Ÿฌํ•œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์„์ง€ ํ•œ๋ฒˆ ์žฌ๊ณ ์‹œ์Šคํ…œ์ด๋ผ๋Š” ๊ฐ„๋‹จํ•œ ๋กœ์ง์„ ์ž‘์„ฑํ•˜์—ฌ ์ •๋ฆฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. 

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • Apple Silicon (M1)
  • Java 11, Spring, JPA, Lombok
  • JUnit5
  • MySQL, Redis

์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๋ชฉ์ฐจ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. ์žฌ๊ณ ์‹œ์Šคํ…œ ๊ธฐ์ดˆ ๋กœ์ง
  2. ๋™์‹œ์„ฑ์„ ๊ณ ๋ คํ•˜์ง€ ์•Š์€ ๋กœ์ง
  3. Java Synchronized ํ‚ค์›Œ๋“œ๋ฅผ ํ™œ์šฉํ•œ ๋™๊ธฐํ™” ๋กœ์ง
  4. DB์—์„œ ์ œ์–ดํ•˜๋Š” ๋ฐฉ๋ฒ•
    1. Pessimistic Lock์„ ํ™œ์šฉํ•œ ๋™๊ธฐํ™” ๋กœ์ง
    2. Optimistic Lock์„ ํ™œ์šฉํ•œ ๋™๊ธฐํ™” ๋กœ์ง
    3. Named Lock์„ ํ™œ์šฉํ•œ ๋™๊ธฐํ™” ๋กœ์ง
  5. Redis๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•
    1. Redis Lettuce์„ ํ™œ์šฉํ•œ ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„
    2. Redis Redission์„ ํ™œ์šฉํ•œ ๋ถ„์‚ฐ ๋ฝ ๊ตฌํ˜„

 

1. ์žฌ๊ณ ์‹œ์Šคํ…œ ๊ธฐ์ดˆ ๋กœ์ง

๋จผ์ € Entity, Service, Repository๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ˆœ์„œ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private Long productId;
    private Long quantity;

    public Stock(final Long id, final Long quantity) {
        this.id = id;
        this.quantity = quantity;
    }

    public void decrease(final Long quantity) {
        if (this.quantity - quantity < 0) {
            throw new RuntimeException("์žฌ๊ณ  ๋ถ€์กฑ");
        }
        this.quantity -= quantity;
    }
    
}
@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;

    @Transactional
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}
public interface StockRepository extends JpaRepository<Stock, Long> {

}

 

2. ๋™์‹œ์„ฑ์„ ๊ณ ๋ คํ•˜์ง€ ์•Š์€ ๋กœ์ง

๋จผ์ € ๋™์‹œ์„ฑ์„ ๊ณ ๋ คํ•˜์ง€ ์•Š์€ ๋กœ์ง ๋จผ์ € ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. 

@SpringBootTest
class StockServiceTest {

    @Autowired
    private StockService stockService;

    @Autowired
    private StockRepository stockRepository;

    @BeforeEach
    public void before() {
        Stock stock = new Stock(1L, 100L);
        stockRepository.saveAndFlush(stock);
    }

    @AfterEach
    public void after() {
        stockRepository.deleteAll();
    }

    @Test
    @DisplayName("race condition ์ผ์–ด๋‚˜๋Š” ํ…Œ์ŠคํŠธ")
    public void ๋™์‹œ์—_100๊ฐœ_์š”์ฒญ() throws InterruptedException {
    
        int threadCount = 100;
        //๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ์ด์šฉ ExecutorService : ๋น„๋™๊ธฐ๋ฅผ ๋‹จ์ˆœํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋˜๋ก ํ•ด์ฃผ๋Š” java api
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        //๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ์ˆ˜ํ–‰์ด ์™„๋ฃŒ๋  ๋•Œ ๊นŒ์ง€ ๋Œ€๊ธฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” API - ์š”์ฒญ์ด ๋๋‚ ๋•Œ ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆผ
        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();
        assertEquals(0L, stock.getQuantity());

    }
}

์ดˆ๊ธฐ์— ProductId๊ฐ€ 1์ธ ์žฌํ’ˆ์˜ ์ˆ˜๋Ÿ‰์„ 100๊ฐœ๋กœ ์„ธํŒ…ํ•˜์—ฌ DB์— ์„ธํŒ…์‹œ์ผฐ๊ณ , ExecutorService๋ฅผ ํ†ตํ•ด 32๊ฐœ์˜ ๊ณ ์ •๋œ ์Šค๋ ˆ๋“œํ’€์„ ์ƒ์„ฑํ•˜์—ฌ submit๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ๋กœ ๋™์ž‘ํ•˜์—ฌ ์žฌ๊ณ ๋ฅผ ๊ฐ์†Œ์‹œํ‚ค๋Š” ๋กœ์ง์„ ์ˆ˜ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

ExecutorService์™€ CountDownLatch์— ๋Œ€ํ•ด ์ƒ์†Œํ•˜์‹  ๋ถ„๋“ค์„ ์œ„ํ•ด ๊ฐ„๋žตํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

ExecutorService

  • ExecutorService๋ž€, ๋ณ‘๋ ฌ ์ž‘์—… ์‹œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ž‘์—…์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์ œ๊ณต๋˜๋Š” JAVA API์ด๋‹ค.
  • ExecutorService๋Š” ์†์‰ฝ๊ฒŒ ThreadPool์„ ๊ตฌ์„ฑํ•˜๊ณ  Task๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
  • Executors๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ExecutorService ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋ฉฐ, ์Šค๋ ˆ๋“œ ํ’€์˜ ๊ฐœ์ˆ˜ ๋ฐ ์ข…๋ฅ˜๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

CountDownLatch

  • CountDownLatch๋ž€, ์–ด๋–ค ์Šค๋ ˆ๋“œ๊ฐ€ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ์ž‘์—…์ด ์™„๋ฃŒ๋  ๋•Œ ๊ฐ€์ง€ ๊ธฐ๋‹ค๋ฆด ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ํด๋ž˜์Šค์ด๋‹ค.
  • CountDownLatch๋ฅผ ์ด์šฉํ•˜์—ฌ, ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ๊ฐ€ ๋‚จ์€ ํšŸ์ˆ˜์˜ ์ž‘์—…์„ ๋ชจ๋‘ ์™„๋ฃŒํ•œ ํ›„, ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋„๋ก ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
  • CountDownLatch ์ž‘๋™์›๋ฆฌ
    1. new CountDownLatch(10); ์˜ ํ˜•์‹์œผ๋กœ Latch ํ•  ๊ฐœ์ˆ˜๋ฅผ ์ง€์ •ํ•œ๋‹ค.
    2. countDown()์„ ํ˜ธ์ถœํ•˜๋ฉด Latch์˜ ์นด์šดํ„ฐ๊ฐ€ 1๊ฐœ์”ฉ ๊ฐ์†Œํ•œ๋‹ค.
    3. await()์€ Latch์˜ ์นด์šดํ„ฐ๊ฐ€ 0์ด ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.

์œ„ ๋กœ์ง์˜ ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‹คํŒจ๋กœ ๋‚˜์˜ต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ๋กœ์ง ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ

100๊ฐœ์˜ ์žฌ๊ณ ๋ฅผ 1๊ฐœ์”ฉ 100๋ฒˆ ๊ฐ์†Œ์‹œ์ผฐ๋˜ ๋งŒํผ ์ˆ˜ํ–‰๊ฒฐ๊ณผ 0์ด๋ผ๋Š” ์žฌ๊ณ ์˜ ์ˆ˜๋Ÿ‰์„ ๊ธฐ๋Œ€ํ–ˆ์œผ๋‚˜ 96์ด๋ผ๋Š” ํ„ฐ๋ฌด๋‹ˆ์—†๋Š” ์ˆ˜๋Ÿ‰์ด ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค. ์ด์œ ๋Š” ์„œ๋‘์— ๋ช…์‹œํ–ˆ๋˜ ์€ํ–‰ ์ž…์ถœ๊ธˆ ์˜ˆ์‹œ์ธ race condition(๊ฒฝ์Ÿ์ƒํ™ฉ)์ด ์ผ์–ด๋‚ฌ๊ธฐ ๋•Œ๋ฌธ์ธ๋ฐ, ๋‘ ๊ฐœ ์ด์ƒ์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ๊ณต์œ  ์ž์›์— ๋Œ€ํ•ด์„œ ๋™์‹œ์— ๋ณ€๊ฒฝํ•˜๋ ค๊ณ  ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ž…๋‹ˆ๋‹ค.

race condition์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ทผ๋ณธ์ ์œผ๋กœ ๊ณต์œ ์ž์›์— ๋Œ€ํ•ด ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ์ž‘์—…์„ ์™„๋ฃŒํ•œ ํ›„์— ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

3. Synchronized ํ‚ค์›Œ๋“œ๋ฅผ ํ™œ์šฉํ•œ ๋™๊ธฐํ™” ๋กœ์ง

๋จผ์ € Synchronizedํ‚ค์›Œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;
    
    /**
     * synchronized ์‚ฌ์šฉ์‹œ @Transactional๊ณผ ๋™์‹œ์— ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋œ๋‹ค.
     * Synchronized๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š” ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋ฅผ ํ•œ ์“ฐ๋ ˆ๋“œ์—์„œ๋งŒ ๋Œ๋ฆฌ๊ธฐ ์œ„ํ•ด์„œ๋‹ค.
     * ํ•˜์ง€๋งŒ, ํŠธ๋žœ์žญ์…˜์ด ๊ฐ™์ด ์ •์˜๊ฐ€ ๋˜์–ด์žˆ๋‹ค๋ฉด ์ฒซ ๋ฒˆ์งธ ์“ฐ๋ ˆ๋“œ๊ฐ€ ๋๋‚˜๊ธฐ ์ „ ๋‘ ๋ฒˆ์งธ ์“ฐ๋ ˆ๋“œ๊ฐ€ ๋ฐœ๋™ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
     */
    public synchronized void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}
    @Test
    @DisplayName("synchronized ์‚ฌ์šฉ")
    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();
        assertEquals(0L, stock.getQuantity());
    }

ํ…Œ์ŠคํŠธ ๋กœ์ง ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ

์œ„์™€ ๊ฐ™์ด ๊ฐ„๋‹จํ•˜๊ฒŒ Service๋กœ์ง์—์„œ ๋ฉ”์„œ๋“œ์— synchronized ํ‚ค์›Œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , @Transactional์„ ์ œ๊ฑฐํ•จ์œผ๋กœ์จ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • synchronized๋ฅผ ์ด์šฉํ•˜๋ฉด ํ˜„์žฌ ์ ‘๊ทผํ•˜๊ณ  ์žˆ๋Š” ๋ฉ”์„œ๋“œ์— ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.
  • ์ž๋ฐ”์—์„œ ์ง€์›ํ•˜๋Š” synchronized๋Š”,  ํ˜„์žฌ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ํ•ด๋‹น ์Šค๋ ˆ๋“œ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๋‚˜๋จธ์ง€ ์Šค๋ ˆ๋“œ๋“ค์€ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์„ ๋ง‰์•„ ์ˆœ์ฐจ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.

synchronized๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ @Transactional์„ ์ œ๊ฑฐํ•ด์ฃผ์–ด์•ผ๋งŒ ํ•˜๋Š” ์ด์œ ๋Š” @Transactional์„ ํ†ตํ•œ ์„ ์–ธ์  ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๋ฐฉ์‹์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ”„๋ก์‹œ ๋ฐฉ์‹์€ AOP๊ฐ€ ์ ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. 

TransactionStatus status = transactionManager.getTransaction(..);

try {
	target.logic(); // public synchronized void decrease ๋ฉ”์„œ๋“œ ์ˆ˜ํ–‰
    // logic ์ˆ˜ํ–‰ ์ดํ›„ ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ ์ „์— ๋‹ค๋ฅธ ์“ฐ๋ ˆ๋“œ๊ฐ€ decrease์— ์ ‘๊ทผ!
    transactionManager.commit(status);
} catch (Exception e) {
	transactionManager.rollback(status);
    throw new IllegalStateException(e); 
}

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

JAVA Sychronized์˜ ๋ฌธ์ œ์ 

synchronized์˜ ์‚ฌ์šฉ์€ ๊ฐ„๋‹จํ•˜์ง€๋งŒ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์ž๋ฐ”์˜ Sychronized๋Š” ํ•˜๋‚˜์˜ ํ”„๋กœ์„ธ์Šค ์•ˆ์—์„œ๋งŒ ๋ณด์žฅ์ด ๋ฉ๋‹ˆ๋‹ค.
  • ์ฆ‰, ์„œ๋ฒ„๊ฐ€ 1๋Œ€์ผ ๋•Œ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ ์„œ๋ฒ„๊ฐ€ ์—ฌ๋Ÿฌ ๋Œ€ ์ผ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ธ์Šคํ„ด์Šค๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์งˆ์ ์ธ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ์ดํ„ฐ์˜ ์ •ํ•ฉ์„ฑ์„ ๋ณด์žฅํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

 

4. DB์—์„œ ์ œ์–ดํ•˜๋Š” ๋ฐฉ๋ฒ•

์ด๋ฒˆ ๋ฐฉ๋ฒ•์€ DateBase Lock์„ ์ด์šฉํ•˜์—ฌ ์ˆœ์ฐจ์ ์ธ ์ ‘๊ทผ์œผ๋กœ ์ œ์–ดํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ํฌ๊ฒŒ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ๋ฒ•์ด ์žˆ์œผ๋ฉฐ, ์กฐ๊ธˆ ๋” ์ƒ์„ธํ•˜๊ณ  DB๋‹จ์—์„œ ์ง์ ‘ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋งํฌ: MySQL์—์„œ์˜ Lock

  1. Pessimistic Lock(๋น„๊ด€์  ๋ฝ)
  2. Optimistic Lock(๋‚™๊ด€์  ๋ฝ)
  3. Named Lock(๋„ค์ž„๋“œ ๋ฝ)

1. Pessimistic Lock

  • Pessimistic Lock์ด๋ž€ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ์— Lock์„ ๊ฑธ์–ด์„œ ์ •ํ•ฉ์„ฑ์„ ๋งž์ถ”๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
  • Exclusive Lock(๋ฐฐํƒ€์  ์ž ๊ธˆ)์„ ๊ฑธ๊ฒŒ ๋˜๋ฉด ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ๋Š” Lock ์ด ํ•ด์ œ๋˜๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์—†๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์ž์› ์š”์ฒญ์— ๋”ฐ๋ฅธ ๋™์‹œ์„ฑ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋ผ๊ณ  ์˜ˆ์ƒํ•˜๊ณ  ๋ฝ์„ ๊ฑธ์–ด๋ฒ„๋ฆฌ๋Š” ๋น„๊ด€์  ๋ฝ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ, Dead Lock(๊ต์ฐฉ์ƒํƒœ)์— ๋น ์งˆ ์œ„ํ—˜์„ฑ์ด ์žˆ์œผ๋ฏ€๋กœ ์œ ์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Dead Lock(๊ต์ฐฉ์ƒํƒœ)๋Š” ์ด์ „ OS ์ •๋ฆฌ ๋•Œ ํฌ์ŠคํŒ… ํ•œ ์  ์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

[OS] ๊ต์ฐฉ ์ƒํƒœ(Deadlocks)

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

dkswnkk.tistory.com

๋น„๊ด€์  ๋ฝ(pessimistic lock) ๋„์‹๋„ 1

์œ„์˜ ๋„์‹๋„์˜ ์ˆ˜ํ–‰ ๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Transaction_1์—์„œ table์˜ Id 2๋ฒˆ์„ ์ฝ์Œ ( name = Karol )
  2. Transaction_2์—์„œ table์˜ Id 2๋ฒˆ์„ ์ฝ์Œ ( name = Karol )
  3. Transaction_2์—์„œ table์˜ Id 2๋ฒˆ์˜ name์„ Karol2๋กœ ๋ณ€๊ฒฝ ์š”์ฒญ ( name = Karol )
  4. ํ•˜์ง€๋งŒ Transaction 1์—์„œ ์ด๋ฏธ shared Lock์„ ์žก๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Blocking
  5. Transaction_1์—์„œ ํŠธ๋žœ์žญ์…˜ ํ•ด์ œ (commit)
  6. Blocking ๋˜์–ด์žˆ์—ˆ๋˜ Transaction_2์˜ update ์š”์ฒญ ์ •์ƒ ์ฒ˜๋ฆฌ

๋น„๊ด€์  ๋ฝ(pessimistic lock) ๋„์‹๋„ 2

์ฝ”๋“œ

public interface StockRepository extends JpaRepository<Stock, Long> {

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    Optional<Stock> findById(Long id);
    
}
@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;
    
    @Transactional
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}
    @Test
    @DisplayName("Pessimistic Lock(๋น„๊ด€์  ๋ฝ) ์‚ฌ์šฉ")
    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();
        assertEquals(0L, stock.getQuantity());

    }

ํ…Œ์ŠคํŠธ ๋กœ์ง ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ

์œ„์™€ ๊ฐ™์ด Repository์—์„œ DB ์กฐํšŒ ์‹œ์— @Lock(value = LockModeType.PESSIMISTIC_WRITE)์„ ์ ์šฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์ด ์‹œ์ž‘ํ•  ๋•Œ Shared/Exclusive Lock์„ ์ ์šฉํ•˜๊ฒŒ ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ตญ Pesimistic Lock์ด๋ž€, ๋ฐ์ดํ„ฐ์—๋Š” Lock์„ ๊ฐ€์ง„ ์Šค๋ ˆ๋“œ๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ์ œ์–ดํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

์žฅ์ 

  • Pessimistic Lock์€ ๋™์‹œ์„ฑ ์ถฉ๋Œ์ด ์žฆ์„ ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋˜์–ด ๋™์‹œ์„ฑ์„ ๊ฐ•๋ ฅํ•˜๊ฒŒ ์ง€์ผœ์•ผ ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ์ถฉ๋Œ์ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚œ๋‹ค๋ฉด ๋กค๋ฐฑ์˜ ํšŸ์ˆ˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, Optimistic Lock๋ณด๋‹ค๋Š” ์„ฑ๋Šฅ์ด ์ข‹์„ ์ˆ˜ ์žˆ๊ณ , ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๋ณด์žฅ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

๋‹จ์ 

  • ๋ฐ์ดํ„ฐ ์ž์ฒด์— Lock์„ ๊ฑธ๊ธฐ ๋•Œ๋ฌธ์— ์†๋„๊ฐ€ ์ƒ๋Œ€์ ์œผ๋กœ ๋Š๋ฆฐ ํŽธ์ž…๋‹ˆ๋‹ค.
  • ์„œ๋กœ ์ž์›์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ, ๋ฝ์ด ๊ฑธ๋ ค์žˆ์œผ๋ฏ€๋กœ Dead Lock(๊ต์ฐฉ์ƒํƒœ)์— ๋น ์งˆ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

2. Optimistic Lock

  • Optimistic Lock์€ ์‹ค์ œ Lock์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ๋ฐ์ดํ„ฐ์˜ Version์„ ์ด์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ์˜ ์ •ํ•ฉ์„ฑ์„ ์ค€์ˆ˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
  • ๋จผ์ € ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ ํ›„์— update๋ฅผ ์ˆ˜ํ–‰ํ•  ๋•Œ ํ˜„์žฌ ๋‚ด๊ฐ€ ์กฐํšŒํ•œ ๋ฒ„์ „์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜๋ฉฐ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
  • ์ž์›์— Lock์„ ๊ฑธ์–ด์„œ ์„ ์ ํ•˜์ง€ ์•Š๊ณ , ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ทธ๋•Œ ๊ฐ€์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋‚™๊ด€์  ๋ฝ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.
  • ๋‚ด๊ฐ€ ์กฐํšŒํ•œ ๋ฒ„์ „์—์„œ ์ˆ˜์ •์‚ฌํ•ญ์ด ์ƒ๊ฒผ์„ ๊ฒฝ์šฐ์—๋Š” application์—์„œ ๋‹ค์‹œ ์กฐํšŒ ํ›„์— ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋กค๋ฐฑ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‚™๊ด€์  ๋ฝ(Optimistic lock) ๋„์‹๋„ 1

์œ„์˜ ๋„์‹๋„์˜ ์ˆ˜ํ–‰ ๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. A๊ฐ€ table์˜ Id 2๋ฒˆ์„ ์ฝ์Œ ( name = Karol, version = 1 )
  2. B๊ฐ€ table์˜ Id 2๋ฒˆ์„ ์ฝ์Œ ( name = Karol, version = 1 )
  3. B๊ฐ€ table์˜ Id 2๋ฒˆ, version 1์ธ row์˜ ๊ฐ’ ๊ฐฑ์‹  ( name = Karol2, version = 2 ) ์„ฑ๊ณต
  4. A๊ฐ€ table์˜ Id 2๋ฒˆ, version 1์ธ row์˜ ๊ฐ’ ๊ฐฑ์‹  ( name = Karol1, version = 2 ) ์‹คํŒจ
  5. Id 2๋ฒˆ์€ ์ด๋ฏธ version์ด 2๋กœ ์—…๋ฐ์ดํŠธ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— A๋Š” ํ•ด๋‹น row๋ฅผ ๊ฐฑ์‹ ํ•˜์ง€ ๋ชปํ•จ
  6. ์ฟผ๋ฆฌ๊ฐ€ ์‹คํŒจํ•˜๋ฉด ๋‹ค์‹œ ์กฐํšŒํ•˜์—ฌ ๋ฒ„์ „์„ ๋งž์ถ˜ ํ›„ ์—…๋ฐ์ดํŠธ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฌ๋Š” ๊ณผ์ •์„ ๋ฐ˜๋ณต

๋‚™๊ด€์  ๋ฝ(Optimistic lock) ๋„์‹๋„ 2

์œ„ flow๋ฅผ ํ†ตํ•ด์„œ ๊ฐ™์€ row์— ๋Œ€ํ•ด์„œ ๊ฐ๊ธฐ ๋‹ค๋ฅธ 2๊ฐœ์˜ ์ˆ˜์ • ์š”์ฒญ์ด ์žˆ์—ˆ์ง€๋งŒ 1๊ฐœ๊ฐ€ ์—…๋ฐ์ดํŠธ๋จ์— ๋”ฐ๋ผ version์ด ๋ณ€๊ฒฝ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋’ค์˜ ์ˆ˜์ • ์š”์ฒญ์€ ๋ฐ˜์˜๋˜์ง€ ์•Š๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋‚™๊ด€์ ๋ฝ์€ version๊ณผ ๊ฐ™์€ ๋ณ„๋„์˜ ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ถฉ๋Œ์ ์ธ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ง‰์Šต๋‹ˆ๋‹ค. version ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ hashcode ๋˜๋Š” timestamp๋ฅผ ์ด์šฉํ•˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ

Pessimistic Lock๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ DB ์กฐํšŒ ์‹œ์— @Lock(value = LockModeType.OPTIMISTIC)์„ ๋ช…์‹œํ•˜์—ฌ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

public interface StockRepository extends JpaRepository<Stock, Long> {

    @Lock(value = LockModeType.OPTIMISTIC)
    Optional<Stock> findById(Long id);
    
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private Long productId;
    private Long quantity;
	
    // ๋ฒ„์ „ ์ปฌ๋Ÿผ ์ถ”๊ฐ€!!
    @Version
    private Long version;
    
    public Stock(final Long id, final Long quantity) {
        this.id = id;
        this.quantity = quantity;
    }

    public void decrease(final Long quantity) {
        if (this.quantity - quantity < 0) {
            throw new RuntimeException("์žฌ๊ณ  ๋ถ€์กฑ");
        }
        this.quantity -= quantity;
    }
    
}

Entity์—๋Š” @Version์„ ํ†ตํ•ด version ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;
    
    @Transactional
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}

Service๋กœ์ง์˜ ๊ฒฝ์šฐ์—๋Š” ๊ธฐ์กด์˜ ๋กœ์ง๊ณผ ์ฐจ์ด๊ฐ€ ์—†๊ณ , ์ง€์†์ ์œผ๋กœ DB ๋ณ€๊ฒฝ์„ ์žฌ์‹œ๋„ํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ Helper ์œ ํ‹ธ๋ฆฌํ‹ฐ์™€ ๊ฐ™์€ Facade๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. 

@Component
@RequiredArgsConstructor
public class OptimisticLockStockFacade {

    private final StockService stockService;
	
 	/**
 	* Lock์„ ์žก์ง€ ์•Š์œผ๋ฏ€๋กœ, Pessimistic Lock(๋น„๊ด€์  ๋ฝ)๋ณด๋‹ค ์„ฑ๋Šฅ์ƒ ์ด์ ์ด ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.
 	* ํ•˜์ง€๋งŒ, ์—…๋ฐ์ดํŠธ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ ์žฌ์‹œ๋„ ๋กœ์ง์ด ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ž‘์„ฑ์„ ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
 	* ๋˜ํ•œ ์ถฉ๋Œ์ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚œ๋‹ค๋ฉด, Pessimistic Lock์ด ์„ฑ๋Šฅ์ƒ ์ด์ ์ด ๋” ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.
 	*/
    public void decrease(Long id, Long quantity) throws InterruptedException {
        while (true) {
            try {
                stockService.decrease(id, quantity);
                break;
            } catch (Exception e) {
                Thread.sleep(1);
            }
        }
    }
}

Optimistic Lock์—์„œ Version์„ ํ™•์ธํ•˜๋Š”๋ฐ, ๋งŒ์•ฝ DB ๋ณ€๊ฒฝ ํŠธ๋žœ์žญ์…˜์„ ๋งŒ๋“ค๊ณ ์ž ํ•˜๋Š” ํ˜„์žฌ ์Šค๋ ˆ๋“œ๊ฐ€ ์ด์ „ Version์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด ํŠธ๋žœ์žญ์…˜์„ ๋ณด๋‚ด์ง€ ๋ชปํ•˜๊ณ  1ms๋™์•ˆ ๊ธฐ๋‹ค๋ฆฌ๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

    @Test
    @DisplayName("Optimistic Lock(๋‚™๊ด€์  ๋ฝ) ์‚ฌ์šฉ")
    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) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0L, stock.getQuantity());
    }

ํ…Œ์ŠคํŠธ ๋กœ์ง ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ

์žฅ์ 

  • ์ถฉ๋Œ์ด ์ž์ฃผ ์ผ์–ด๋‚˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๊ฐ€์ • ํ•˜์—, ๋ณ„๋„์˜ Lock์„ ๊ฑธ์ง€ ์•Š์œผ๋ฏ€๋กœ Pessimistic Lock๋ณด๋‹ค๋Š” ์„ฑ๋Šฅ์  ์ด์ ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ 

  • ์—…๋ฐ์ดํŠธ๊ฐ€ ์‹คํŒจํ–ˆ์„ ์‹œ, ์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ž‘์„ฑํ•ด ์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ถฉ๋Œ์ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚œ๋‹ค๋ฉด, Roll Back ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, Pessimistic Lock์ด ๋” ์„ฑ๋Šฅ์ด ์ข‹์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

3. Named Lock

  • Named Lock์€ ์ด๋ฆ„์„ ๊ฐ€์ง„ Metadata Lock์ž…๋‹ˆ๋‹ค.
  • ์ด๋ฆ„์„ ๊ฐ€์ง„ Lock์„ ํš๋“ํ•œ ํ›„, ํ•ด์ง€๋  ๋•Œ๊นŒ์ง€ ๋‹ค๋ฅธ ์„ธ์…˜์€ ์ด Lock์„ ํš๋“ํ•  ์ˆ˜ ์—†๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์ฃผ์˜ํ•  ์ ์€, ํŠธ๋žœ์žญ์…˜์ด ์ข…๋ฃŒ๋  ๋•Œ Lock์ด ์ž๋™์œผ๋กœ ํ•ด์ง€๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ๋ณ„๋„๋กœ ํ•ด์ง€ํ•ด์ฃผ๊ฑฐ๋‚˜ ์„ ์ ์‹œ๊ฐ„์ด ๋๋‚˜์•ผ ํ•ด์ง€๋ฉ๋‹ˆ๋‹ค.
  • Mysql์—์„œ๋Š” getLock( )์„ ํ†ตํ•ด ํš๋“ค / releaseLock()์œผ๋กœ ํ•ด์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Named Lock ๋„์‹๋„

์œ„์˜ ๋„์‹๋„์˜ ์ˆ˜ํ–‰ ๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Named Lock์€ Stock์— ๋ฝ์„ ๊ฑธ์ง€ ์•Š๊ณ , ๋ณ„๋„์˜ ๊ณต๊ฐ„์— Lock์„ ๊ฑด๋‹ค.
  2. session-1 ์ด 1์ด๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ Lock์„ ๊ฑด๋‹ค๋ฉด, session 1 ์ด 1์„ ํ•ด์ง€ํ•œ ํ›„์— Lock์„ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

์ฃผ์˜ํ•  ์ ์€ Named Lock์„ ํ™œ์šฉํ•  ๋•Œ ๋ฐ์ดํ„ฐ์†Œ์Šค๋ฅผ ๋ถ„๋ฆฌํ•˜์ง€ ์•Š๊ณ , ํ•˜๋‚˜๋กœ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด connection pool์ด ๋ถ€์กฑํ•ด์ง€๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•ด Lock์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋‹ค๋ฅธ ์„œ๋น„์Šค๊นŒ์ง€ ์˜ํ–ฅ์„ ๋ผ์น  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

Named Lock์„ ํ™œ์šฉํ•˜๋ฉด ๋ถ„์‚ฐ ๋ฝ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ณ , Pessmistic  Lock์˜ ๊ฒฝ์šฐ ํƒ€์ž„์•„์›ƒ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ๊นŒ๋‹ค๋กญ์ง€๋งŒ, Named Lock์€ ์†์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ ์‹œ์— 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);
    
}
@Component
@RequiredArgsConstructor
public class NamedLockFacade {

    private final LockRepository lockRepository;
    private final StockService stockService;
	
    /**
    * Named Lock์€ ์ฃผ๋กœ ๋ถ„์‚ฐ ๋ฝ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
    * Pessimistic Lock์€ ํƒ€์ž„์•„์›ƒ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ๊ต‰์žฅํžˆ ๊นŒ๋‹ค๋กญ์ง€๋งŒ, Named Lock์€ ์†์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
    * ์ด์™ธ์—๋„ ๋ฐ์ดํ„ฐ ์‚ฝ์ž…์‹œ์— ์ •ํ•ฉ์„ฑ์„ ๋งž์ถฐ์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    * ํ•˜์ง€๋งŒ ์ด ๋ฐฉ๋ฒ•์€ ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ์‹œ์— ๋ฝ ํ•ด์ œ์™€ ์„ธ์…˜ ๊ด€๋ฆฌ๋ฅผ ์ง์ ‘ ์ž˜ ํ•ด์ค˜์•ผํ•˜๋ฏ€๋กœ ์ฃผ์˜ํ•ด์„œ ์‚ฌ์šฉํ•ด์•ผํ•˜๊ณ , ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๊ตฌํ˜„๋ฐฉ๋ฒ•์ด ๋ณต์žกํ•  ์ˆ˜ ์žˆ๋‹ค.
    */
    @Transactional
    public void decrease(Long id, Long quantity) {
        try {
            lockRepository.getLock(id.toString());
            stockService.decrease(id, quantity);
        }finally {
            // Lock ํ•ด์ œ
            lockRepository.releaseLock(id.toString());
        }
    }
}
@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;
    
    // ๋ถ€๋ชจ์˜ ํŠธ๋žœ์žญ์…˜๊ณผ ๋ณ„๋„๋กœ ์‹คํ–‰๋˜์–ด์•ผ ํ•จ
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void decrease(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);
        stockRepository.saveAndFlush(stock);
    }
    
}

์—ฌ๊ธฐ์„œ ์ค‘์š” ๋กœ์ง์˜ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • StockService๋Š” ๋ถ€๋ชจ์˜ ํŠธ๋žœ์žญ์…˜๊ณผ ๋ณ„๋„๋กœ ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— propergation์„ ๋ณ„๋„๋กœ ์ƒ์„ฑํ•ด ์ค๋‹ˆ๋‹ค
  • ๋ถ€๋ชจ์˜ ํŠธ๋žœ์žญ์…˜๊ณผ ๋™์ผํ•œ ๋ฒ”์œ„๋กœ ๋ฌถ์ธ๋‹ค๋ฉด Synchronized์™€ ๊ฐ™์€ ๋ฌธ์ œ์ธ DataBase์— Commit ๋˜๊ธฐ ์ „์— Lock์ด ํ’€๋ฆฌ๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
  • ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ถ„๋ฆฌํ•ด์„œ DataBase์— ์ •์ƒ์ ์œผ๋กœ Commit์ด ๋œ ํ›„์— ๋ฝ์„ ํ•ด์ œํ•ด ์ฃผ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•ต์‹ฌ์€ Lock์„ ํ•ด์ œํ•˜๊ธฐ ์ „์— DataBase์— Commit์ด ๋˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  Connection Pool์˜ ์ˆ˜๋ฅผ ๋Š˜๋ ค์ค๋‹ˆ๋‹ค.

spring:
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
  datasource:
    driver-class-name: org.h2.Driver
    url:  jdbc:h2:tcp://localhost/~/test
    username: sa
    password:
    hikari:
      maximum-pool-size: 40 	// here
    @Test
    @DisplayName("Named Lock ์‚ฌ์šฉ")
    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 {
                    namedLockStockFacade.decrease(1L, 1L);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0L, stock.getQuantity());
    }

ํ…Œ์ŠคํŠธ ๋กœ์ง ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ

์žฅ์ 

  • Named Lock ์€ ์ฃผ๋กœ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • Pessimistic ๋ฝ์€ timeout์„ ๊ตฌํ˜„ํ•˜๊ธฐ ๊ต‰์žฅํžˆ ํž˜๋“ค์ง€๋งŒ, Named Lock์€ ๋น„๊ต์  ์†์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ 

  • Named Lock ์€ ํŠธ๋žœ์žญ์…˜ ์ข…๋ฃŒ ์‹œ์—, Lock ํ•ด์ œ์™€ ์„ธ์…˜๊ด€๋ฆฌ๋ฅผ ์ž˜ํ•ด์ฃผ์–ด์•ผ ํ•˜๋ฏ€๋กœ ์ฃผ์˜ํ•ด์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์‹ค์ œ ์—…๋ฌด ํ™˜๊ฒฝ์—์„œ๋Š” ๊ตฌํ˜„๋ฐฉ๋ฒ•์ด ๋ณต์žกํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

5. Redis๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

Redis๋ž€ key-value ๊ตฌ์กฐ์˜ ๋น„์ •ํ˜• ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์˜คํ”ˆ ์†Œ์Šค ๊ธฐ๋ฐ˜์˜ ๋น„ ๊ด€๊ณ„ํ˜• ์ธ๋ฉ”๋ชจ๋ฆฌ DBMS์ž…๋‹ˆ๋‹ค. Redis์˜ ๋‹ค์–‘ํ•œ ํŠน์ง• ์ค‘์—์„œ๋„ Single Threaded ํ•œ ํŠน์ง• ์ฆ‰, ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์˜ ๋ช…๋ น๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํŠน์ง• ๋•Œ๋ฌธ์— ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š”๋ฐ ๋งŽ์ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

Java์˜ Redis Client๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ์ค‘์—์„œ Jedis๋Š” ์„ฑ๋Šฅ์ด ๋งŽ์ด ์ข‹์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— Lettuce์™€ Redisson์„ ๋น„๊ตํ•ด์„œ ์ •๋ฆฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. Jedis์™€์˜ ์„ฑ๋Šฅ ๋น„๊ต๋Š” ์กฐ์กธ๋‘๋‹˜์˜ ๋ธ”๋กœ๊ทธ์— ์ž์„ธํžˆ ์„ค๋ช…๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋งํฌ: https://jojoldu.tistory.com/418

1. Lettuce

  • Setnx ๋ช…๋ น์–ด๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ (Set if not Exist - key:value๋ฅผ Set ํ•  ๋–„. ๊ธฐ์กด์˜ ๊ฐ’์ด ์—†์„ ๋•Œ๋งŒ Set ํ•˜๋Š” ๋ช…๋ น์–ด)
  • Setnx๋Š” Spin Lock๋ฐฉ์‹์ด๋ฏ€๋กœ retry ๋กœ์ง์„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž‘์„ฑํ•ด ์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • Spin Lock ์ด๋ž€, Lock์„ ํš๋“ํ•˜๋ ค๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ Lock์„ ํš๋“ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋ฉด์„œ ๋ฐ˜๋ณต์ ์œผ๋กœ ์‹œ๋„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

๋ฝ์„ ํš๋“ํ•œ๋‹ค๋Š” ๊ฒƒ์€ “๋ฝ์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค”, “์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ๋ฝ์„ ํš๋“ํ•œ๋‹ค” ๋‘ ์—ฐ์‚ฐ์ด atomic ํ•˜๊ฒŒ ์ด๋ฃจ์–ด์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. Redis์— ๊ธฐ๋ฐ˜ํ•œ Lettuce ๋ฐฉ์‹์€ “๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ์„ธํŒ…ํ•œ๋‹ค”๋ผ๋Š” setnx(set when not exists) ๋ช…๋ น์–ด๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ด setnx๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ ˆ๋””์Šค์— ๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ์„ธํŒ…ํ•˜๊ฒŒ ํ•˜๊ณ , ๊ฐ’์ด ์„ธํŒ…๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ๋ฆฌํ„ด ๊ฐ’์œผ๋กœ ๋ฐ›์•„ ๋ฝ์„ ํš๋“ํ•˜๋Š” ๋ฐ์— ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

Spin Lock ๊ณผ์ •

Named Lock๊ณผ ๋‹ฌ๋ฆฌ Redis๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŠธ๋žœ์žญ์…˜์— ๋”ฐ๋ผ ๋Œ€์‘๋˜๋Š” ํ˜„์žฌ ํŠธ๋žœ์žญ์…˜ ํ’€ ์„ธ์…˜ ๊ด€๋ฆฌ๋ฅผ ํ•˜์ง€ ์•Š์•„๋„ ๋˜๋ฏ€๋กœ ๊ตฌํ˜„์ด ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์•ž์„œ ๋งํ–ˆ๋“ฏ Spin Lock ๋ฐฉ์‹์ด๋ฏ€๋กœ Sleep Time์ด ์ ์„์ˆ˜๋ก Redis์— ๋ถ€ํ•˜๋ฅผ ์ค„ ์ˆ˜ ์žˆ์–ด์„œ thread busy waiting์˜ ์š”์ฒญ ๊ฐ„์˜ ์‹œ๊ฐ„์„ ์ ์ ˆํžˆ ์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ

dependencies {
    //redis ์˜์กด์„ฑ ์ถ”๊ฐ€
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
@Component
@RequiredArgsConstructor
public class RedisLockRepository {

    private final RedisTemplate<String, String> redisTemplate;

    public Boolean lock(final Long key) {
        return redisTemplate
            .opsForValue()
            .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
    }

    public Boolean unlock(final Long key) {
        return redisTemplate.delete(generateKey(key));
    }

    private String generateKey(final Long key) {
        return key.toString();
    }
    
}
@Component
@RequiredArgsConstructor
public class LettuceLockStockFacade  {

    private final RedisLockRepository redisLockRepository;
    private final StockService stockService;
	
   /**
   * ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๋‹ค.
   * Spring Data Redis๋ฅผ ์ด์šฉํ•˜๋ฉด Lettuce๊ฐ€ ๊ธฐ๋ณธ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.
   * Spin Lock ๋ฐฉ์‹์ด๊ธฐ ๋•Œ๋ฌธ์— ๋™์‹œ์— ๋งŽ์€ ์Šค๋ ˆ๋“œ๊ฐ€ Lock ํš๋“ ๋Œ€๊ธฐ ์ƒํƒœ๋ผ๋ฉด Redis์— ๋ถ€ํ•˜๊ฐ€ ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.
   * ์‹ค๋ฌด์—์„œ๋Š” ์žฌ์‹œ๋„๊ฐ€ ํ•„์š”ํ•œ Lock์˜ ๊ฒฝ์šฐ์—๋Š” Redission์„ ํ™œ์šฉํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์„ ๊ฒฝ์šฐ์—๋Š” Lettuce์„ ํ™œ์šฉํ•œ๋‹ค.
   * ์žฌ์‹œ๋„๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ?: ์„ ์ฐฉ์ˆœ 100๋ช… ๊นŒ์ง€ ๋ฌผํ’ˆ์„ ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ์„ ๊ฒฝ์šฐ
   * ์žฌ์‹œ๋„๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ?: ์„ ์ฐฉ์ˆœ ํ•œ๋ช…๋งŒ ๊ฐ€๋Šฅ, Lock ํš๋“ ์žฌ์‹œ๋„ ํ•  ํ•„์š”๊ฐ€ ์—†์Œ
   */
    public void decrease(final Long key, final Long quantity) throws InterruptedException {
        while (!redisLockRepository.lock(key)) {
            Thread.sleep(50);
        }

        //lock ํš๋“ ์„ฑ๊ณต์‹œ
        try{
            stockService.decrease(key,quantity);
        }finally {
         //๋ฝ ํ•ด์ œ
            redisLockRepository.unlock(key);
        }
    }
    
}

Spin Lock๋ฐฉ์‹์œผ๋กœ Lock ์–ป๊ธฐ๋ฅผ ์‹œ๋„ํ•˜๊ณ , Lock์„ ์–ป์€ ํ›„, ์žฌ๊ณ  ๊ฐ์†Œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ํ›„ Lock์„ ํ•ด์ œํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

    @Test
    @DisplayName("Lettuce Lock ์‚ฌ์šฉ")
    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 {
                    lettuceLockStockFacade.decrease(1L, 1L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0L, stock.getQuantity());

    }

ํ…Œ์ŠคํŠธ ๋กœ์ง ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ

Spin Lock ๋ฐฉ์‹์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฌธ์ œ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.(https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html)

1. Lock์˜ ํƒ€์ž„์•„์›ƒ์ด ์ง€์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์œ„์˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด ์Šคํ•€ ๋ฝ์„ ๊ตฌํ˜„ํ–ˆ์„์‹œ์— ๋ฝ์„ ํš๋“ํ•˜์ง€ ๋ชปํ•˜๋ฉด ๋ฌดํ•œ ๋ฃจํ”„๋ฅผ ๋Œ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ํŠน์ •ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ tryLock์„ ์„ฑ๊ณตํ–ˆ๋Š”๋ฐ ๋ถˆ์šดํ•˜๊ฒŒ๋„ ์–ด๋–ค ์˜ค๋ฅ˜ ๋•Œ๋ฌธ์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ข…๋ฃŒ๋˜์–ด๋ฒ„๋ฆฌ๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”? ๋‹ค๋ฅธ ๋ชจ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊นŒ์ง€ ์˜์›ํžˆ ๋ฝ์„ ํš๋“ํ•˜์ง€ ๋ชปํ•œ ์ฑ„ ๋ฝ์ด ํ•ด์ œ๋˜๊ธฐ๋งŒ์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋ฌดํ•œ์ • ๋Œ€๊ธฐ์ƒํƒœ๊ฐ€ ๋˜์–ด ์ „์ฒด ์„œ๋น„์Šค์˜ ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ผ๋ฐ˜์ ์ธ ๋กœ์ปฌ ์Šคํ•€ ๋ฝ๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๋ฝ์ด ๋งŒ๋ฃŒ๋˜๋„๋ก ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ ค๋ฉด expire time์„ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์œ„์˜ ์ฝ”๋“œ์—์„œ๋Š” “๋ฝ์„ ์‚ฌ์šฉ ์ค‘์ธ์ง€ ํ™•์ธ”, “๋ฝ์„ ํš๋“” ์—ฐ์‚ฐ์„ ํ•˜๋‚˜๋กœ ๋ฌถ๊ธฐ ์œ„ํ•ด setnx ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ช…๋ น์–ด๋Š” expire time์„ ์ง€์ •ํ•  ์ˆ˜ ์—†๊ธฐ์— ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ๊ฐ€ ํž˜๋“ญ๋‹ˆ๋‹ค.

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

2. Redis์— ๋งŽ์€ ๋ถ€ํ•˜๋ฅผ ๊ฐ€ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์œ„์˜ ์ฝ”๋“œ๋Š” ์Šคํ•€ ๋ฝ์„ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ ์‚ฌ์‹ค ์Šคํ•€ ๋ฝ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ ˆ๋””์Šค์— ์—„์ฒญ๋‚œ ๋ถ€๋‹ด์„ ์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์Šคํ•€ ๋ฝ์€ ์ง€์†์ ์œผ๋กœ ๋ฝ์˜ ํš๋“์„ ์‹œ๋„ํ•˜๋Š” ์ž‘์—…์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ ˆ๋””์Šค์— ๊ณ„์† ์š”์ฒญ์„ ๋ณด๋‚ด๊ฒŒ ๋˜๊ณ  ๋ ˆ๋””์Šค๋Š” ์ด๋Ÿฐ ํŠธ๋ž˜ํ”ฝ์„ ์ฒ˜๋ฆฌํ•˜๋Š๋ผ ๋ถ€๋‹ด์„ ๋ฐ›๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์Šคํ•€ ๋ฝ์„ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋ ˆ๋””์Šค์— ๋ถ€๋‹ด์„ ๋œ ์ฃผ๊ธฐ ์œ„ํ•ด 50ms๋งŒํผ sleep ํ•˜๋ฉด์„œ tryLock์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ํ–ˆ์ง€๋งŒ, ์ด ๋˜ํ•œ 50ms๋งˆ๋‹ค ๊ณ„์† ๋ ˆ๋””์Šค์— ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ์ž‘์—…์ด ์˜ค๋ž˜ ๊ฑธ๋ฆด์ˆ˜๋ก, ์š”์ฒญ ์ˆ˜๊ฐ€ ๋งŽ์„์ˆ˜๋ก ๋” ํฐ ๋ถ€ํ•˜๋ฅผ ๊ฐ€ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ 300ms๊ฐ€ ๊ฑธ๋ฆฌ๋Š” ๋™๊ธฐํ™”๋œ ์ž‘์—…์— ๋™์‹œ์— 100๊ฐœ์˜ ์š”์ฒญ์ด ์™”๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. (๋ถ„์‚ฐ ๋ฝ์ด๋ฏ€๋กœ ์„œ๋ฒ„์˜ ๋Œ€์ˆ˜๋Š” ๋ฌด๊ด€ํ•ฉ๋‹ˆ๋‹ค.)

์ฒ˜์Œ์œผ๋กœ ๋ฝ์„ ํš๋“ํ•˜๋Š” ๋ฐ ์„ฑ๊ณตํ•œ 1๊ฐœ์˜ ์š”์ฒญ์„ ์ œ์™ธํ•˜๊ณ , ๋‚˜๋จธ์ง€ 99๊ฐœ์˜ ์š”์ฒญ์€ ์ž‘์—…์ด ์™„๋ฃŒ๋˜๋Š” 300ms ๋™์•ˆ ๋ฌด๋ ค ๋ ˆ๋””์Šค์— 594ํšŒ์˜ ๋ฝ ํš๋“ ์š”์ฒญ์„ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰ 1์ดˆ ๋™์•ˆ ์•ฝ 2000ํšŒ๋ผ๋Š” ๋งŽ์€ ์š”์ฒญ์„ ๋ ˆ๋””์Šค์— ๋ณด๋‚ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ์ผํšŒ์„ฑ์ด ์•„๋‹ˆ๋ผ ๋ชจ๋“  ์ž‘์—…์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ์ง€์†์ ์œผ๋กœ ๋ ˆ๋””์Šค์— ๋ถ€ํ•˜๋ฅผ ๊ฐ€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์š”์ฒญ์ด ์ง€์†์ ์œผ๋กœ ๋“ค์–ด์˜ค๋Š” ํ™˜๊ฒฝ์ด๋ผ๋ฉด ์ด๋Ÿฌํ•œ ๋น„ํšจ์œจ์„ฑ์€ ๋”์šฑ ์ปค์ง‘๋‹ˆ๋‹ค.

๋งŒ์•ฝ ๋ ˆ๋””์Šค์— ๋ถ€๋‹ด์„ ๋œ ์ฃผ๊ธฐ ์œ„ํ•ด sleep ์‹œ๊ฐ„์„ 300ms๋กœ ๋Š˜๋ฆฐ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? 50ms๊ฐ€ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์— ์ด ๋™๊ธฐํ™”๋ฅผ ์ ์šฉํ•˜๋ฉด ๋ฝ์„ ํš๋“ํ•˜์ง€ ๋ชปํ•  ๊ฒฝ์šฐ 50ms ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์„ ํ•˜๊ธฐ ์œ„ํ•ด 300ms๋ฅผ ๋Œ€๊ธฐํ•ด์•ผ ํ•˜๋Š” ๋‹ค๋ฅธ ๋น„ํšจ์œจ์ ์ธ ์ƒํ™ฉ์ด ์ƒ๊ธฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

2. Redisson

  • Pub-sub ๊ธฐ๋ฐ˜์œผ๋กœ Lock ๊ตฌํ˜„ ์ œ๊ณต
  • Pub-Sub ๋ฐฉ์‹์ด๋ž€, ์ฑ„๋„์„ ํ•˜๋‚˜ ๋งŒ๋“ค๊ณ , ๋ฝ์„ ์ ์œ  ์ค‘์ธ ์Šค๋ ˆ๋“œ๊ฐ€, Lock์„ ํ•ด์ œํ–ˆ์Œ์„, ๋Œ€๊ธฐ ์ค‘์ธ ์Šค๋ ˆ๋“œ์—๊ฒŒ ์•Œ๋ ค์ฃผ๋ฉด ๋Œ€๊ธฐ ์ค‘์ธ ์Šค๋ ˆ๋“œ๊ฐ€ Lock ์ ์œ ๋ฅผ ์‹œ๋„ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.
  • ์ด ๋ฐฉ์‹์€, Lettuce์™€ ๋‹ค๋ฅด๊ฒŒ ๋Œ€๋ถ€๋ถ„ ๋ณ„๋„์˜ Retry ๋ฐฉ์‹์„ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

Redisson ์ ์œ  ๊ณผ์ •

์ฝ”๋“œ

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    // redisson ์˜์กด์„ฑ ์ถ”๊ฐ€
    implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.19.0'
}
@Component
@RequiredArgsConstructor
public class RedissonLockStockFacade {

    private final RedissonClient redissonClient;
    private final StockService stockService;
	
   /**
   * Lock ํš๋“ ์žฌ์‹œ๋„๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•œ๋‹ค.
   * pub-sub ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„์ด ๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— Lettuce์™€ ๋น„๊ตํ–ˆ์„ ๋•Œ Redis์— ๋ถ€ํ•˜๊ฐ€ ๋œ ๊ฐ„๋‹ค.
   * ๋ณ„๋„์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
   * Lock์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฐจ์›์—์„œ ์ œ๊ณตํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉ๋ฒ•์„ ๊ณต๋ถ€ํ•ด์•ผ ํ•œ๋‹ค.
   * ์‹ค๋ฌด์—์„œ๋Š” ์žฌ์‹œ๋„๊ฐ€ ํ•„์š”ํ•œ Lock์˜ ๊ฒฝ์šฐ์—๋Š” Redission์„ ํ™œ์šฉํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์„ ๊ฒฝ์šฐ์—๋Š” Lettuce์„ ํ™œ์šฉํ•œ๋‹ค.
   * ์žฌ์‹œ๋„๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ?: ์„ ์ฐฉ์ˆœ 100๋ช… ๊นŒ์ง€ ๋ฌผํ’ˆ์„ ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ์„ ๊ฒฝ์šฐ
   * ์žฌ์‹œ๋„๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ?: ์„ ์ฐฉ์ˆœ ํ•œ๋ช…๋งŒ ๊ฐ€๋Šฅ, Lock ํš๋“ ์žฌ์‹œ๋„ ํ•  ํ•„์š”๊ฐ€ ์—†์Œ
   */
    public void decrease(final Long key, final Long quantity) {
    
        RLock lock = redissonClient.getLock(key.toString());

        try {
            // ํš๋“์‹œ๋„ ์‹œ๊ฐ„, ๋ฝ ์ ์œ  ์‹œ๊ฐ„
            boolean available = lock.tryLock(5, 1, TimeUnit.SECONDS);

            if (!available) {
                System.out.println("lock ํš๋“ ์‹คํŒจ");
                return;
            }

            stockService.decrease(key, quantity);
            
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            lock.unlock();
        }
    }
    
}

Redisson์€ Lettuce์™€ ๋‹ฌ๋ฆฌ ๋ณ„๋„์˜ ์ธํ„ฐํŽ˜์ด์Šค์ด๊ธฐ ๋•Œ๋ฌธ์— gradle ์˜์กด ํŒจํ‚ค์ง€ ์„ค์น˜ ๋ฐ ๋ณ„๋„์˜ Facade ์ž‘์„ฑ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

    @Test
    @DisplayName("Redisson Lock ์‚ฌ์šฉ")
    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 {
                    redissonLockStockFacade.decrease(1L, 1L);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0L, stock.getQuantity());

    }

ํ…Œ์ŠคํŠธ ๋กœ์ง ์ˆ˜ํ–‰ ๊ฒฐ๊ณผ

์ด์ œ๋Š” ์˜คํ”ˆ์†Œ์Šค ๋ ˆ๋””์Šค ํด๋ผ์ด์–ธํŠธ์ธ Redisson์ด ๋ถ„์‚ฐ ๋ฝ์„ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ–ˆ๋Š”์ง€ ์†Œ๊ฐœํ•˜๋ฉฐ ์–ด๋–ป๊ฒŒ ์ด ๋ฌธ์ œ์ ๋“ค์„ ํ•ด๊ฒฐํ–ˆ๊ณ , ๋ณด๋‹ค ๋น ๋ฅธ ์„ฑ๋Šฅ์„ ๋‚ด๊ฒŒ ๋˜์—ˆ๋Š”์ง€ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Redisson์€ Lettuce์™€ ๋น„์Šทํ•˜๊ฒŒ Netty๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ non-blocking I/O๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Redisson์˜ ํŠน์ดํ•œ ์ ์€ ์ง์ ‘ ๋ ˆ๋””์Šค์˜ ๋ช…๋ น์–ด๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š๊ณ , Bucket์ด๋‚˜ Map๊ฐ™์€ ์ž๋ฃŒ๊ตฌ์กฐ๋‚˜ Lock ๊ฐ™์€ ํŠน์ •ํ•œ ๊ตฌํ˜„์ฒด์˜ ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

1. Lock์— ํƒ€์ž„์•„์›ƒ์ด ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Redisson์€ tryLock ๋ฉ”์†Œ๋“œ์— ํƒ€์ž„์•„์›ƒ์„ ๋ช…์‹œํ•˜๋„๋ก ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ฝ ํš๋“์„ ๋Œ€๊ธฐํ•  ํƒ€์ž„์•„์›ƒ์ด๊ณ , ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ฝ์ด ๋งŒ๋ฃŒ๋˜๋Š” ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค.

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

์ด๋กœ ์ธํ•ด ๋ฝ์ด ํ•ด์ œ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๋กœ ๋ฌดํ•œ ๋ฃจํ”„์— ๋น ์งˆ ์œ„ํ—˜์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

// RedissonLock์˜ tryLock ๋ฉ”์†Œ๋“œ ์‹œ๊ทธ๋‹ˆ์ณ
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException

2. ์Šคํ•€ ๋ฝ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Redisson์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์Šคํ•€ ๋ฝ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ ˆ๋””์Šค์— ๋ถ€๋‹ด์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์–ด๋–ป๊ฒŒ ๋ฝ์˜ ํš๋“ ๊ฐ€๋Šฅ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ• ๊นŒ์š”?

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

๋˜ํ•œ Redisson์€ ์ตœ๋Œ€ํ•œ ๋ ˆ๋””์Šค์™€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋ถ€ํ•˜๋ฅผ ์ฃผ์ง€ ์•Š๋„๋ก ์‹ ๊ฒฝ ์“ด ๋ชจ์Šต์ด ๋ณด์ž…๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” Redisson์˜ Lock ํš๋“ ํ”„๋กœ์„ธ์Šค์ž…๋‹ˆ๋‹ค.

  1. ๋Œ€๊ธฐ ์—†๋Š”tryLock ์˜คํผ๋ ˆ์ด์…˜์„ ํ•˜์—ฌ ๋ฝ ํš๋“์— ์„ฑ๊ณตํ•˜๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ฒฝํ•ฉ์ด ์—†์„ ๋•Œ ์•„๋ฌด๋Ÿฐ ์˜ค๋ฒ„ํ—ค๋“œ ์—†์ด ๋ฝ์„ ํš๋“ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.
  2. pubsub์„ ์ด์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€๊ฐ€ ์˜ฌ ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๋‹ค๊ฐ€ ๋ฝ์ด ํ•ด์ œ๋˜์—ˆ๋‹ค๋Š” ๋ฉ”์„ธ์ง€๊ฐ€ ์˜ค๋ฉด ๋Œ€๊ธฐ๋ฅผ ํ’€๊ณ  ๋‹ค์‹œ ๋ฝ ํš๋“์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ๋ฝ ํš๋“์— ์‹คํŒจํ•˜๋ฉด ๋‹ค์‹œ ๋ฝ ํ•ด์ œ ๋ฉ”์‹œ์ง€๋ฅผ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ด ํ”„๋กœ์„ธ์Šค๋ฅผ ํƒ€์ž„์•„์›ƒ ์‹œ๊นŒ์ง€ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.
  3. ํƒ€์ž„์•„์›ƒ์ด ์ง€๋‚˜๋ฉด ์ตœ์ข…์ ์œผ๋กœ false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ๋ฝ ํš๋“์— ์‹คํŒจํ–ˆ์Œ์„ ์•Œ๋ฆฝ๋‹ˆ๋‹ค. ๋Œ€๊ธฐ๊ฐ€ ํ’€๋ฆด ๋•Œ ํƒ€์ž„์•„์›ƒ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•˜๋ฏ€๋กœ ํƒ€์ž„์•„์›ƒ์ด ๋ฐœ์ƒํ•˜๋Š” ์ˆœ๊ฐ„์€ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ธด ํƒ€์ž„์•„์›ƒ์‹œ๊ฐ„๊ณผ ์•ฝ๊ฐ„์˜ ์ฐจ์ด๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ •๋ฆฌ

Lettuce

  • ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๋‹ค
  • Spring data redis๋ฅผ ์ด์šฉํ•˜๋ฉด lettuce๊ฐ€ ๊ธฐ๋ณธ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.
  • Spin Lock ๋ฐฉ์‹์ด๊ธฐ ๋•Œ๋ฌธ์— ๋™์‹œ์— ๋งŽ์€ ์Šค๋ ˆ๋“œ๊ฐ€ lock ํš๋“ ๋Œ€๊ธฐ ์ƒํƒœ๋ผ๋ฉด redis์— ๋ถ€ํ•˜๊ฐ€ ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.

Redisson

  • ๋ฝ ํš๋“ ์žฌ์‹œ๋„๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•œ๋‹ค.
  • pub-sub ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„์ด ๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— lettuce์™€ ๋น„๊ตํ–ˆ์„ ๋•Œ redis์— ๋ถ€ํ•˜๊ฐ€ ๋œ ๊ฐ„๋‹ค.
  • ๋ณ„๋„์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
  • lock์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฐจ์›์—์„œ ์ œ๊ณตํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉ๋ฒ•์„ ๊ณต๋ถ€ํ•ด์•ผ ํ•œ๋‹ค.

 

์ฐธ๊ณ 

  1. https://www.inflearn.com/course/๋™์‹œ์„ฑ์ด์Šˆ-์žฌ๊ณ ์‹œ์Šคํ…œ
  2. https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
  3. https://jypthemiracle.medium.com/weekly-java-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%ED%95%99%EC%8A%B5%ED%95%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-9daa85155f66
  4. https://velog.io/@minnseong/Spring-synchronized%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C
  5. https://thalals.tistory.com/370

๋Œ“๊ธ€