스터디 방은 매달 1일에 다시 참여할 수 있다. 모든 스터디방의 참여 인원은 10명
으로 제한한다.
동시성 제어가 없는 상태로 멀티스레드를 만들어 테스트한 결과 원하는 결과보다 훨씬 더 많은 스터디방 참여자가 생겼다.
ExecutorService
와 CountDownLatch
를 사용했다.@Test
fun studyRoomJoinConcurrencyTest() {
//given
val threadCount = 50
val category = categoryRepository.save(fixture<Category>())
val studyRoom = studyRoomRepository.save(fixture<StudyRoom>() {
property(StudyRoom::category) { category }
property(StudyRoom::state) { StudyRoomState.ACTIVATED }
})
val studyRoomId = studyRoom.id
val users = (1..threadCount).map { userRepository.save(fixture<User> { property(User::state) { UserState.ACTIVATED } }) }
val executorService: ExecutorService = Executors.newFixedThreadPool(32)
val latch = CountDownLatch(threadCount)
//when
for (user in users) {
executorService.execute {
val response = studyRoomService.joinStudyRoom(studyRoomId, user.id)
latch.countDown()
}
}
latch.await()
//then
val userCount = userStudyRoomRepository.countByStudyRoom(studyRoomId)
Assertions.assertThat(userCount).isEqualTo(10)
}
해결방안으로 많은 방법이 있다. pessmistic lock, optimistic lock, named lock 등등이 있지만 나는 분산환경에서 빠른 성능을 기대해 볼 수 있는 redis를 활용해 동시성 제어를 해보겠다. 물론 최대 10명의 동시성을 처리하는데는 너무나도 오버스펙이지만 학습의 의의를 둬야한다.
레디스 클라이언트 라이브러리도 lettuce, redisson 두 개가 존재한다. 다음과 같은 이유로 redisson을 사용한다.
Redisson의 경우 따로 의존성 추가가 필요하다.
implementation("org.redisson:redisson-spring-boot-starter:3.21.1")
config파일을 작성한다.
@Configuration
class RedisRedissonConfig (
@Value("\${spring.data.redis.host}")
private val host: String,
@Value("\${spring.data.redis.port}")
private val port: Int
) {
@Bean(destroyMethod = "shutdown")
fun redissonClient(): RedissonClient {
val config: Config = Config()
config.useSingleServer()
.setAddress("redis://$host:$port")
.dnsMonitoringInterval = -1
return Redisson.create(config)
}
}
LettuceConnectionFactory
를 사용 중이다. Redisson의 경우 LettuceConnectionFactory
로 레디스를 사용할 수 있어 따로 팩토리 빈을 만들어주지 않았다.분산락을 실행하는 코드를 추상화 하였다.
@Component
class RedisLockExecutor (
private val redissonClient: RedissonClient
) {
fun <R> execute(target:Long, task:() -> R): R {
val lock = redissonClient.getLock(target.toString())
try {
val available = lock.tryLock(10, 1, TimeUnit.SECONDS)
if (!available) {
throw RedisException(RedisExceptionType.LOCK_TIME_OUT)
}
return task()
} catch (e: InterruptedException) {
throw RedisException(RedisExceptionType.LOCK_TIME_OUT, e)
} finally {
lock.unlock()
}
}
}
@Component
class StudyRoomServiceLockFacade (
private val studyRoomService: StudyRoomService,
private val redisLockExecutor: RedisLockExecutor
) {
fun joinStudyRoom(studyRoomId: Long, userId: Long) {
redisLockExecutor.execute(studyRoomId) {studyRoomService.joinStudyRoom(studyRoomId, userId)}
}
}
StudyRoomServiceLockFacade
클래스를 통해 락을 획득한 유저만이 스터디 방에 가입할 수 있다.