서비스를 구현하다 보면, 여러 쓰레드에서 동시다발적으로 일어나는 작업에 대해 동기화 혹은 일관적인 처리가 필요할 때가 존재한다.
그럴 때는 여러 방법을 이용하여 동시성 문제를 해결 할 수 있는데, 오늘은 많이 사용하는 key-value store인 Redis를 이용하여 Lock을 구현하는 방법에 대해 알아보자.
사실 이 방법은 분산 락 구현 관점에서는 옳지 않다. 서비스가 단일 프로세스로만 구동되는 상황이라면 적합할 수 있지만, 한 서비스가 분산환경에서 동작하고 있다면 이 방법은 적용해봤자 분산환경에서는 원하는대로 동작하지 않는다.
그렇다면, 어떻게 구현해야 할까?
Redis를 이용하면 손쉽게 구현할 수 있는데, 그 방식을 알아보자.
추가 : Java 혹은 Kotlin에서 프로세스 단위 동시성 제어에 대해 궁금하다면,
ReentarantLock
혹은synchronized
에 대해 공부해보자.
Redis를 이용하여 Lock을 구현하는 방식중 가장 쉽게 떠올릴 수 있는 생각은 무엇일까?
가장 간단하게 생각하면, Reids에 특정 key가 존재하는지 확인하고 없으면 key를 세팅하는 방식을 생각할 수 있다.
그 구현 예시를 알아보자.
@Component
class Lock(private val redisTemplate: StringRedisTemplate) {
fun lock(key: String, ttl: Long): Boolean {
redisTemplate.opsForValue().get(key)?.let {
return false
}
redisTemplate.opsForValue().set(key, "locked", ttl)
return true
}
fun unlock(key: String) {
redisTemplate.delete(key)
}
}
Spring을 사용하는 경우에는 spring-data-redis
를 통해 Redis의 명령어를 손쉽게 사용할 수 있다.
구현에서 알 수 있듯이, key가 존재하는지 확인하고 없으면 그 key를 세팅하는 과정을 거치면 된다.
하지만 이 방식에도 문제점이 있다. 그 문제점을 알아보자.
이 방식의 문제점은 key가 존재하는지 체크하는 과정과 없으면, set하는 과정이 원자적으로 일어나지 않는 것이다. 보통 이러한 상황을 check-act가 원자적으로 동작하지 않는다고 이야기한다.
이게 왜 문제일까? 모든 쓰레드가 갇혀있다가 나갔을 때, race condition이 발생할 수 있는 문제점이 존재하기 때문이다.
이러한 문제에 대해 간단하게 알아보고 싶다면, 이 포스트를 읽어보는 것을 추천한다.
그럼 이것을 어떻게 해결할까? 다음 파트인 "Redis를 이용한 Atomic한 구현"에서 알아보자.
위에서 알아보았듯이, check-act를 원자적으로 구현하지 않으면 동시성 문제를 제대로 해결하지 못한다. 어떻게 해결할 수 있을까?
그 방법은, Redis에서 제공하는 SETNX 연산을 사용하는 것이다.
SETNX
는 SET if Not eXist 의 줄임말으로, 키가 존재하지 않을 경우 값을 지정하는 방식으로 동작한다.
"Redis를 이용한 구현"에서 보여주었던 것 처럼, 존재하는지 확인하고 없으면 SET하는 방식이 아닌 check-act를 원자적으로 동작하도록 보장한 방법이다.
@Component
class Lock(private val redisTemplate: StringRedisTemplate) {
fun lock(key: String, ttl: Long): Boolean {
return redisTemplate.opsForValue().setIfAbsent(key, "locked", ttl) ?: false
}
fun unlock(key: String) {
redisTemplate.delete(key)
}
}
spring-data-redis
의 RedisTemplate
은 위와 같이 setIfAbsent
라는 함수를 통해 SETNX
명령어를 사용할 수 있도록 해준다.
사실 이렇게 직접 구현할 필요없이, Redisson이라는 라이브러리에서 SETNX
를 이용한 lock을 제공하고있다.
구현하지 않고, 이러한 라이브러리를 잘 사용하는 것도 좋은 방법이다.