우리가 개발하는 프로덕트에 동시성 이슈는 어떤 것이 있을까?
사실은 그다지 많지 않을 수 있을 것 같다. 특히, 트래픽은 적은 회사에서는 더욱 그러지 않을까 싶다. 그래서 언제든 맞닥뜨리더라도 당황하지 않았으면 좋겠다.
먼저 동시성 이슈가 무엇인지부터 살펴보자.
하나의 Table Row 을 Client 1과 Client 2가 서로 업데이트를 하려고 하다보니, 동시성 이슈가 발생한다.
좀 더 구체적으로 어떤 상황에서 동시성 이슈가 발생하는가?
참고 할 만한 예시 테스트 코드는 다음과 같다.
@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());
}
그럼 어떻게 동시성 문제를 해결할 수 있을까?
1. 근본적인 비지니스 로직을 변경하기
위 그림에 있었던 PartitionQueue 을 해결하기 위한 방법으로 다음과 같은 2가지 방법을 제시했었다.
위 두 가지 방식이 담고 있는 기본 기조는 방향성을 2개의 Client 가 하나의 Table Row 을 업데이트 하는 것이 아니라, 1개의 Client 만 하나의 Table Row을 업데이트 할 수 있도록 의존성을 방향을 변경하는 방법이다. 실제로 이 문제을 해결할 때 위와같은 방식을 사용했었다.
2. 비관적 잠금과 낙관적 잠금
- 비관적 잠금과 낙관점 잠금이란 무엇인가?
- 비관적 잠금이란?
- 비관적 잠금은 Pessimistic Lock 이라고도 하며, 선점 잠금이라고도 부른다. 선점잠금이라 불리는 이유는 먼저 자원을 선점하면 다른 Client는 해당 자원을 수정하지 못하게 막는 방식이 때문이다. 흔히 ' ~ for update' 라는 Query 로 비관적 잠금을 한다.
- 낙관적 잠금이란?
- 비관적 잠금의 문제는 A스레드가 자원의 Lock 이 얻는 동안 B는 대기하다가 A가 업데이트 한 자원을 갖고 다음 작업을 취하기 때문에 Dirty Read 가 발생할 여지가 있다. (물론 Read 에도 비관적 잠금을 할 수 있지만, 그렇게 되면 성능상의 문제가 크게 발생한다) 이런 문제을 해결하는 방법으로 낙관적 잠금을 사용할 수 있고 이것은 비선점잠금이라 부른다. 비선점 잠금은 동시에 접근하는 것을 막지 않는 대신 변경한 데이터를 실제 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 을 획득하거나, 포기하거나.
[참고자료]
- https://github.com/LenKIM/Book/blob/master/DDD%20start/8-%EC%96%B4%EA%B7%B8%EB%A6%AC%EA%B1%B0%ED%8A%B8%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98.md
- https://github.com/take-small-steps/understanding-concurrency-Issue
- https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C
'개발 관련됨 > 개발 이슈를 해결함' 카테고리의 다른 글
Java 에 Enum Circular Dependency 이라는 말을 들어봤나요? (0) | 2023.12.16 |
---|---|
Java Regex 정규표현식 사용시 java.lang.StackOverflowError 가 발생하는걸까 (0) | 2023.02.25 |
ConnectionAcquireTimeoutError [SequelizeConnectionAcquireTimeoutError] 문제 해결하기 (0) | 2022.09.11 |
Message Relay 를 PollingPublisher 방식으로 구현하기 (2) | 2022.06.19 |
정규표현식에서 알지 못했던 capture group 과 non- capture group (0) | 2022.06.06 |
댓글