본문 바로가기
개발 관련됨/개발 이슈를 해결함

비관적 잠금, 낙관적 잠금 그런 동시성 이슈 해결하기

by simplify-len 2023. 1. 11.

우리가 개발하는 프로덕트에 동시성 이슈는 어떤 것이 있을까?

사실은 그다지 많지 않을 수 있을 것 같다. 특히, 트래픽은 적은 회사에서는 더욱 그러지 않을까 싶다. 그래서 언제든 맞닥뜨리더라도 당황하지 않았으면 좋겠다.

먼저 동시성 이슈가 무엇인지부터 살펴보자.

img

하나의 Table Row 을 Client 1과 Client 2가 서로 업데이트를 하려고 하다보니, 동시성 이슈가 발생한다.

좀 더 구체적으로 어떤 상황에서 동시성 이슈가 발생하는가?

참고 할 만한 예시 테스트 코드는 다음과 같다.

https://github.com/take-small-steps/understanding-concurrency-Issue/blob/9eea5414c1a35ec09484f2281e51e1b18380dd93/src/test/java/com/example/stock/service/StockServiceTest.java#L49

@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 - (100 * 1) = 0
  assertEquals(0, stock.getQuantity());
}

image-20230106160513339

그럼 어떻게 동시성 문제를 해결할 수 있을까?

1. 근본적인 비지니스 로직을 변경하기

위 그림에 있었던 PartitionQueue 을 해결하기 위한 방법으로 다음과 같은 2가지 방법을 제시했었다.

imgimg

위 두 가지 방식이 담고 있는 기본 기조는 방향성을 2개의 Client 가 하나의 Table Row 을 업데이트 하는 것이 아니라, 1개의 Client 만 하나의 Table Row을 업데이트 할 수 있도록 의존성을 방향을 변경하는 방법이다. 실제로 이 문제을 해결할 때 위와같은 방식을 사용했었다.

2. 비관적 잠금과 낙관적 잠금

  • 비관적 잠금과 낙관점 잠금이란 무엇인가?
    • 비관적 잠금이란?
      • 비관적 잠금은 Pessimistic Lock 이라고도 하며, 선점 잠금이라고도 부른다. 선점잠금이라 불리는 이유는 먼저 자원을 선점하면 다른 Client는 해당 자원을 수정하지 못하게 막는 방식이 때문이다. 흔히 ' ~ for update' 라는 Query 로 비관적 잠금을 한다.
    • 낙관적 잠금이란?
      • 비관적 잠금의 문제는 A스레드가 자원의 Lock 이 얻는 동안 B는 대기하다가 A가 업데이트 한 자원을 갖고 다음 작업을 취하기 때문에 Dirty Read 가 발생할 여지가 있다. (물론 Read 에도 비관적 잠금을 할 수 있지만, 그렇게 되면 성능상의 문제가 크게 발생한다) 이런 문제을 해결하는 방법으로 낙관적 잠금을 사용할 수 있고 이것은 비선점잠금이라 부른다. image-20230104133328007 비선점 잠금은 동시에 접근하는 것을 막지 않는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다. 이때 사용하는 방식은 'Version'을 넣는 것이다.
  • 비관적 잠금과 낙관적 잠금은 어떤 차이를 갖는가?
    • 용어에서 나타난 것과 같이 비관적 잠금은 많은 사람들이 하나의 자원에 접근을 많이 할 거라는 관점에서 착안한 부분이고, 낙관적 잠금은 많은 사람들이 하나의 자원에 접근하지 않을 거라는 관점에서 나온 용어이다.
    • 그러므로, 비관적 잠금(선점잠금)은 충돌이 많이 일어날 수 있는 환경에서 사용될 수 있으며 블로킹이 많이 일어날 수 있으므로 성능에 대한 기대가 높지 않다.
    • 반대로, 낙관적 잠금(비선점잠금)은 충돌이 많이 일어나는 환경에서는 사용할 수 없고, 만약 사용하더라도 OptimisticLockingFailureException, VerisonConflictException 이런 예외를 만나기 쉬울 것이다. 다만 성능적으로 선점잠금보다는 좀 더 우세하다고 말할 수 있을 것이다.

3. Redis 라이브러리를 활용한 해결 방법(라이브러리는 Spring F/W 에서 유효하다)

Redis 을 활용해서 문제를 해결하는데 2가지 방식이 있다.

  • Lettuce
    • setnx 명령어를 활용하여 분산락을 구현하는 방식. 이 방식은 마치 뮤텍스을 쓰는 것과 비슷하다.
    • 구현이 꽤 간단하고, Spring-Data-redis를 이용하면 Lettuce 가 기본이기 때문에 별도의 라이브러리를 사용하지 않아도 된다.
    • Spin Lock 방식이기 때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 Redis에 부하가 갈 수 있다.
  • Redisson
    • 락 획득 재시도를 기본으로 제공한다.
    • Pub-Sub 기반의 Lock 구현이기 때문에 Lettuce와 비교했을 때 Redis에 부하가 덜 간다. 그러나 별도의 라이브러리를 사용해야 한다.

2 개의 라이브러리을 그럼 언제언제 사용해야 하는가?

  • 재시도가 필요하지 않는 Lock의 경우에는 Lettuce 활용하기
  • 재시도가 필요한 경우에는 Redisson 을 활용한다.

생각을 마치며...

아마도 동시성 이슈을 해결할 수 있는 방법으로 더 많은 방법이 있을 것이다. 왜냐하면 기술은 계속 발전하고 더 나은 방법을 찾으려 하기 때문이다. 그러나 가장 큰 기조는 동시에 2개의 Client가 동시에 업데이트 되어서는 안된다는 것이다. Lock 을 획득하거나, 포기하거나.

[참고자료]

댓글