Spring ๐ŸŒฑ

๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ : synchronized, MySQL Lock, Redis Lock

z.zzz 2024. 2. 11. 13:53

๋™์‹œ์„ฑ ๋ฌธ์ œ


ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ์— ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์ ‘๊ทผํ•˜๋ฉด์„œ ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ๋ฅผ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ผ๊ณ  ํ•œ๋‹ค.

ํ•˜๋‚˜์˜ ์„ธ์…˜์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ • ์ค‘์ผ๋•Œ, ๋‹ค๋ฅธ ์„ธ์…˜์—์„œ ์ˆ˜์ • ์ „์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•จ์œผ๋กœ์จ ๋ฐ์ดํ„ฐ์˜ ์ •ํ•ฉ์„ฑ์ด ๊นจ์ง€๊ฒŒ ๋œ๋‹ค.

 

๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•


๋™์‹œ์„ฑ ๋ฌธ์ œ๋Š” ํ•˜๋‚˜์˜ ์„ธ์…˜์ด ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋™์•ˆ, ๋‹ค๋ฅธ ์„ธ์…˜์€ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ํ•จ์œผ๋กœ์„œ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. ์„ธ์…˜์˜ ๋…๋ฆฝ์ ์ธ ์‹คํ–‰์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๋‹ค์Œ ์„ธ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.

1. Application ๋ ˆ๋ฒจ์—์„œ synchronized ์‚ฌ์šฉ

2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ Lock ์‚ฌ์šฉ

3. Redis์˜ Lock ์‚ฌ์šฉ

 

์žฌ๊ณ  ๊ฐ์†Œ ๋กœ์ง์„ ํ†ตํ•ด ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ์‚ดํŽด๋ณด๊ณ  ํ•ด๊ฒฐํ•ด๋ณด์ž.

 

๋ชฉ์ฐจ
0. ์žฌ๊ณ  ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ๋กœ์ง
1. ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ด์œ 
2. ๊ฒฝ์Ÿ ์ƒํƒœ(Race Condition) ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
   1. java synchronized
   2. MySQL Lock
      1. Pessimistic Lock
      2. Optimistic Lock
      3. Named Lock
   3. Redis Lock
      1. Lettuce Lock
      2. Redisson Lock
3. ์ •๋ฆฌ

 

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

  • Stock.java : ์—”ํ‹ฐํ‹ฐ
  • StockRepository.java : ๋ ˆํฌ์ง€ํ† ๋ฆฌ
  • StockService.java : ์žฌ๊ณ  ๊ฐ์†Œ ๋กœ์ง

Stock.java

@Entity
@Getter
@NoArgsConstructor
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(Long productId, Long quantity) {
        this.productId = productId;
        this.quantity = quantity;
    }

    public void decrease(Long quantity) {
        if (this.quantity - quantity < 0) {
            throw new RuntimeException("์žฌ๊ณ ๋Š” 0๊ฐœ ๋ฏธ๋งŒ์ด ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
        }
        this.quantity -= quantity;
    }
}

 

StockService.java

@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;

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

 

 

์œ„ ์ฝ”๋“œ๋Š” ์‰ฝ๊ฒŒ ๋– ์˜ฌ๋ฆด ์ˆ˜ ์žˆ๋Š” ์žฌ๊ณ  ๊ฐ์†Œ ๋กœ์ง์œผ๋กœ ์‹คํ–‰ํ•˜๋ฉด ์•„๋ฌด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ์ƒํ™ฉ์—์„  ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜ํƒ€๋‚œ๋‹ค.

 

StockServiceTest.java

์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๋กœ ๋™์‹œ์— ์žฌ๊ณ ๋ฅผ ๊ฐ์†Œ์‹œํ‚ค๋Š” ํ…Œ์ŠคํŠธ

  • ExecutorService : ๋ณ‘๋ ฌ ์ž‘์—… ์‹œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ž‘์—…์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์ œ๊ณต๋˜๋Š” JAVA ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • CountDownLatch : ์–ด๋–ค ์“ฐ๋ ˆ๋“œ๊ฐ€ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ์ž‘์—…์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆด ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ํด๋ž˜์Šค
@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 {
                stockService.decrease(1L, 1L);
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await();

    Stock stock = stockRepository.findById(1L).orElseThrow();
    // 100 - (1 * 100) = 0
    assertEquals(0, stock.getQuantity());
}

 

100๊ฐœ์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์žฌ๊ณ ๋ฅผ ํ•˜๋‚˜ ๊ฐ์†Œ์‹œํ‚ค๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋‹ค. ์šฐ๋ฆฌ๋Š” ์Šค๋ ˆ๋“œ ํ•˜๋‚˜๋‹น ํ•˜๋‚˜์˜ ์žฌ๊ณ ๋ฅผ ๊ฐ์†Œ์‹œํ‚ค๋ฉฐ ๋งˆ์ง€๋ง‰์— ๋‚จ์€ ์žฌ๊ณ  ์ˆ˜๋Š” 0์ด๊ธธ ๊ธฐ๋Œ€ํ•œ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰์‹œ์ผœ๋ณด๋ฉด ๋‚จ์€ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰์€ 0๋ณด๋‹ค ๋งŽ์•„ ํ…Œ์ŠคํŠธ๋Š” ์‹คํŒจํ•œ๋‹ค.

 

์ด๋Š” ๋ ˆ์ด์Šค ์ปจ๋””์…˜(Race Condition)์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ ˆ์ด์Šค ์ปจ๋””์…˜์ด๋ž€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ”„๋กœ์„ธ์Šค๊ฐ€ ๊ณต์œ  ์ž์›์— ๋™์‹œ ์ ‘๊ทผํ•  ๋•Œ ์‹คํ–‰ ์ˆœ์„œ์— ๋”ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ’์ด ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ๋Š” ํ˜„์ƒ์ด๋‹ค.

 

1. ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ๋ ˆ์ด์Šค ์ปจ๋””์…˜์ด ๋ฐœ์ƒํ•˜๋Š” ์ด์œ 


์˜ˆ์ƒ ์ž‘์—… ์ˆœ์„œ

๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ๋กœ ์ž‘์—…์„ ํ•  ๋•Œ, ์•„๋ž˜ ๊ทธ๋ฆผ์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ์— ์ˆœ์ฐจ์ ์œผ๋กœ ์ ‘๊ทผํ•˜์—ฌ ์žฌ๊ณ ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธธ ๊ธฐ๋Œ€ํ•œ๋‹ค.

Thread-1 Stock Thread-2
select *
from stock
where id = 1
{id: 1, quantity: 5}  
update set quantity = 4
from stock
where id = 1
{id: 1, quantity: 4}  
  {id: 1, quantity: 4} select *
from stock
where id = 1
  {id: 1, quantity: 3} update set quantity = 3
from stock
where id = 1

 

์‹ค์ œ ์ž‘์—… ์ˆœ์„œ

๊ทธ๋Ÿฌ๋‚˜ ์‹ค์ œ๋กœ๋Š” ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋™์‹œ์— ๋ณ€๊ฒฝํ•˜๋ ค ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์žฌ๊ณ  ๊ฐ์†Œ ์ž‘์—…์ด ๋ˆ„๋ฝ๋  ์ˆ˜ ์žˆ๋‹ค.

Thread-1 Stock Thread-2
select *
from stock
where id = 1
{id: 1, quantity: 5}  
  {id: 1, quantity: 5} select *
from stock
where id = 1
update set quantity = 4
from stock
where id = 1
{id: 1, quantity: 4}  
  {id: 1, quantity: 4} update set quantity = 4
from stock
where id = 1

 

2. ๋ ˆ์ด์Šค ์ปจ๋””์…˜ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ ˆ์ด์Šค ์ปจ๋””์…˜์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„  ๊ณต์œ  ์ž์›์— ๋Œ€ํ•ด ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ๋” ์ œํ•œํ•˜๋ฉด ๋œ๋‹ค.

 

(1) synchronized ์ด์šฉํ•˜๊ธฐ


synchronized๋Š” java์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์Šค๋ ˆ๋“œ ๋™๊ธฐํ™”์˜ ๋Œ€ํ‘œ์ ์ธ ๋ฐฉ๋ฒ•

synchronized ํ‚ค์›Œ๋“œ๋ฅผ ์ด์šฉํ•˜๋ฉด ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋ฅผ ํ•œ๋ฒˆ์— ํ•œ ์Šค๋ ˆ๋“œ์”ฉ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋ณด์žฅํ•œ๋‹ค.

synchronized ์‚ฌ์šฉ ์‹œ ์ฃผ์˜ํ•  ์  : @Transactional์„ ๋ถ™์ด์ง€ ๋ง ๊ฒƒ(๐Ÿ”—), stock์„ ๊ฐฑ์‹ ํ•  ๋• save๊ฐ€ ์•„๋‹Œ saveAndFlush๋ฅผ ์“ธ ๊ฒƒ(๐Ÿ”—)

public synchronized void decrease(Long id, Long quantity) {
    Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);
    stockRepository.saveAndFlush(stock);
}

 

๐Ÿ’ฅsynchronized์˜ ๋ฌธ์ œ์ 

- synchronized๋Š” ํ•˜๋‚˜์˜ ํ”„๋กœ์„ธ์Šค ์•ˆ์—์„œ๋งŒ ๋ณด์žฅ๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์„œ๋ฒ„๊ฐ€ ์—ฌ๋Ÿฌ ๋Œ€์ธ ๊ฒฝ์šฐ, synchronized๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋˜ ๋‹ค์‹œ race condition์ด ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.(๋ฉ€ํ‹ฐ ํ”„๋กœ์„ธ์Šค)

- ์‹ค๋ฌด์—์„  ๋Œ€๋ถ€๋ถ„ ๋‘ ๋Œ€ ์ด์ƒ์˜ ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ synchronized๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

(2) ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค(MySQL)์˜ Lock ์ด์šฉํ•˜๊ธฐ


1. Pessimistic Lock

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

2. Optimistic Lock

  • ์‹ค์ œ๋กœ Lock์„ ์ด์šฉํ•˜์ง€ ์•Š๊ณ  ๋ฒ„์ „์„ ์ด์šฉํ•จ์œผ๋กœ์จ ์ •ํ•ฉ์„ฑ์„ ๋งž์ถ”๋Š” ๋ฐฉ๋ฒ•
  • ๋จผ์ € ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์€ ํ›„, update๋ฅผ ์ˆ˜ํ–‰ํ•  ๋•Œ ํ˜„์žฌ ๋‚ด๊ฐ€ ์ฝ์€ ๋ฒ„์ „๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€๋ฅผ ํ™•์ธํ•˜์—ฌ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.
  • ๋‚ด๊ฐ€ ์ฝ์€ ๋ฒ„์ „์—์„œ ์ˆ˜์ •์‚ฌํ•ญ์ด ์ƒ๊ธด ๊ฒฝ์šฐ, application์—์„œ ๋‹ค์‹œ ์ฝ์–ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•œ๋‹ค.
    โ‘  Server 1์ด version 1์„ ๋ช…์‹œํ•˜์—ฌ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฐ๋‹ค.
    โ‘ก version 1 ์ฟผ๋ฆฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค version์€ 2๊ฐ€ ๋œ๋‹ค.
    โ‘ข Server 2๊ฐ€ version 1์„ ๋ช…์‹œํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฐ๋‹ค. ํ•˜์ง€๋งŒ ๋ฒ„์ „์ด ์ผ์น˜ํ•˜์ง€ ์•Š์•„ ์—…๋ฐ์ดํŠธ์— ์‹คํŒจํ•œ๋‹ค.
    โ‘ฃ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํŒจํ–ˆ์œผ๋ฏ€๋กœ Server 2๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ฒ„์ „์„ ๋‹ค์‹œ ์กฐํšŒํ•œ ํ›„ version์„ ์ˆ˜์ •ํ•˜์—ฌ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฐ๋‹ค.

3. Named Lock

  • ์ด๋ฆ„์„ ๊ฐ€์ง„ metadata locking (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐœ์ฒด์— ๋Œ€ํ•œ ๋™์‹œ ์•ก์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ์ž ๊ธˆ)
  • ์ด๋ฆ„์„ ๊ฐ€์ง„ lock์„ ํš๋“ํ•œ ํ›„ ํ•ด์ œํ•  ๋•Œ๊นŒ์ง€ ๋‹ค๋ฅธ ์„ธ์…˜์€ ์ด lock์„ ํš๋“ํ•  ์ˆ˜ ์—†๋‹ค.
  • ํŠธ๋žœ์žญ์…˜์ด ์ข…๋ฃŒ๋  ๋•Œ lock์ด ์ž๋™์œผ๋กœ ํ•ด์ œ๋˜์ง€ ์•Š๋Š”๋‹ค. ๋”ฐ๋ผ์„œ ๋ณ„๋„์˜ ๋ช…๋ น์–ด๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜ ์„ ์ ์‹œ๊ฐ„์ด ๋๋‚˜์•ผ ํ•ด์ œ๋œ๋‹ค.

1. Pessimistic Lock ์‚ฌ์šฉํ•˜๊ธฐ

StockRepository.java

public interface StockRepository extends JpaRepository<Stock, Long> {

    //Lock์„ ๊ฑธ๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select s from Stock s where s.id = :id")
    Stock findByIdWithPessimisticLock(Long id);
}

 

- ๋‚ด์šฉ ์ถ”๊ฐ€ ์˜ˆ์ • -