동시성 문제를 해결해보자
Last updated
Was this helpful?
Last updated
Was this helpful?
째깍 프로젝트를 진행하면서 발생했던 문제점에 대해 의논하고 해결한 경험을 작성합니다.
째깍 프로젝트에서 다음과 같은 문제점이 존재했다.
모임을 생성할 때 미리 모임의 인원 수 만큼 일정을 만든다.
사용자(회원/비회원)가 일정을 할당할 때 미리 만들어진 일정을 업데이트 하는 방식을 사용한다.
ScheduleService.java
위의 코드에서 발생할 수 있는 문제는 무엇이 있을까?
일정(Schedule)에 여러 스레드가 동시에 접근하여 조회를 하게 될 경우를 생각해보자.
이러한 상황에서 동시성 문제가 발생하여, 각각 다른 스레드가 하나의 일정을 가져오고, 서로의 정보로 갱신하여 Lost Update 현상이 발생하게 된다.
위와 같이 Race Condition이 발생하여 예상했던 흐름대로 진행이 되지 않는다.
정말 문제가 발생하는지 한 번 테스트를 해보자.
동시성 테스트용 모임을 1개 생성한다. 해당 모임에서는 5명의 인원만 일정을 할당할 수 있다.
동시에 100명의 사용자가 일정을 한다고 가정을 해보고 JMeter를 활용하여 테스트를 진행해봤다.
정상적으로 원했던 방식과는 다른 오류가 발생했다.
5명의 인원까지만 할당되고 나머지 인원은 할당이 되지 않아야 함.
또한, 요청을 보낸 순서대로 처리가 되지 않음.
테스트 결과 5명의 인원만 할당 되어야 하지만, 10개의 요청에 응답을 해버림.
위에서 예상했던대로 동시성 문제로 인해서 문제가 발생하는 것을 확인했다. 그러면 어떻게 해결할 수 있을까?
여러 방안들이 있었지만, 두 가지의 해결법을 찾아냈었다. MySQL의 Named Lock 기법과 Redis를 활용하여 분산 락을 사용하는 방법이었다.
네임드 락은 말 그대로 이름을 통해서 락을 거는 기법을 말한다.
SELECT GET_LOCK('lock_name', timeout) 을 통해 락 획득
SELECT RELEASE_LOCK('lock_name')을 통해 락 해제
네임드 락을 실무에서 사용할 경우에는, DataSource를 분리하는 것을 추천한다고 한다.비즈니스 로직과 동일한 DataSource를 사용하게 될 경우 커넥션 풀이 부족하여 서비스에 영향이 미칠 수 있다니 조심하도록 하자.
간단하게 네임드 락을 사용할 때의 흐름을 생각해보면,
실제 비즈니스 로직(Service)을 수행하기 전, 락을 획득한다.
정상적으로 락을 획득했을 경우, 비즈니스 로직을 수행한다.
마지막으로 락을 해제한다.
그렇다면, 우리 서비스에서는 어떻게 적용을 해야할까? 간단하게 예시로 살펴보자.
NamedLockRepository.java
getLock: Lock 획득을 위한 메서드
releaseLock: Lock 해제를 위한 메서드
ScheduleFacade.java
위와 같은 방식으로 간단하게 구현할 수 있으며, 분산 락(Distributed Lock)을 구현할 때 사용한다. 하지만, 다음과 같은 유의점이 있다.
자동으로 락이 해제되지 않아 철저한 관리가 필요
커넥션 풀을 사용하기 때문에 DB 부하가 늘어날 수 있음.
락과 비즈니스 로직의 트랜잭션 분리가 필요
이러한 문제점을 파악하고, 현재 프로젝트에 레디스가 도입이 되어있는 상황이었기 때문에 레디스를 사용하여 분산 락을 구현하면 어떨까? 라는 생각이 들었다.
물론, 레디스를 사용하지 않는 상황이었더라면 인프라 구축을 해야한다는 점을 고려하여 진행하지 않았을 것 같다
위에서 진행했던 방식과 레디스 락도 크게 다른 점은 없다.
기존: Controller -> Service
변경: Controller -> Facade (Redis Lock) -> Service
레디스의 명령어 중 SETNX 를 통해 다음과 같은 시나리오를 생각할 수 있다.
키가 존재하지 않는 경우에만 락을 획득
이미 다른 스레드가 락을 획득한 경우에는 대기
작업이 끝난 스레드는 DEL 명령어를 통해 락 해제
Spring Data Redis의 RedisTemplate에서는 setIfAbsent() 메서드로 추상화하여 제공한다.
위의 내용을 바탕으로 RedisRepository를 정의해보자.
RedisRepository.java
ScheduleFacade.java
코드를 보면, while 문을 통해 락을 얻어올 떄 까지 요청을 보내는 Spin Lock 방식을 사용하였습니다.
테스트 환경은 위와 동일하게 설정하였습니다.
우리가 원했던 방식대로 동시성 문제를 해결하였습니다. 락을 획득한 스레드만 일정을 할당할 수 있게 진행되어 5명의 인원만 모임에 일정을 할당할 수 있는 결과를 얻었습니다.
하지만, Lettuce Lock은 위에서 언급했던 것 처럼 Spin Lock 방식을 사용한다. 락을 획득할 때까지 계속해서 레디스한테 '락을 획득하고 싶어' 라고 요청을 보내게 된다. 싱글 스레드인 레디스에 계속해서 이러한 요청은 부하를 줄 수 있다고 한다.
그렇기에 정말 많은 사용자가 동시에 요청한다면 이러한 방식은 피해야 할 것으로 보인다.
더 좋은 방법으로 Redisson 을 사용하여 pub/sub 방식을 통해 개선할 수 있다고 한다. 아래는 컬리에서 Redisson을 사용하여 문제를 해결한 방식이라고 한다. 시간이 될 때 읽어보고 별도의 프로젝트에서 진행해봐도 좋을 것 같다.