feat(recommend): 추천 스냅샷 lock을 적용한다

This commit is contained in:
2026-06-08 19:12:20 +09:00
parent 08cd856d25
commit 7fee004e7f
2 changed files with 63 additions and 3 deletions

View File

@@ -1,15 +1,29 @@
package kr.co.vividnext.sodalive.v2.recommend.adapter.out.scheduler package kr.co.vividnext.sodalive.v2.recommend.adapter.out.scheduler
import kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshService import kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshService
import org.redisson.api.RedissonClient
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit
@Component @Component
class RecommendationSnapshotScheduler( class RecommendationSnapshotScheduler(
private val refreshService: RecommendationSnapshotRefreshService private val refreshService: RecommendationSnapshotRefreshService,
private val redissonClient: RedissonClient
) { ) {
@Scheduled(cron = "0 0 6 * * *", zone = "Asia/Seoul") @Scheduled(cron = "0 0 6 * * *", zone = "Asia/Seoul")
fun refreshDailySnapshots() { fun refreshDailySnapshots() {
refreshService.refreshDailySnapshots() val lockName = "lock:recommendation-snapshot-refresh"
val lock = redissonClient.getLock(lockName)
try {
if (lock.tryLock(0, -1, TimeUnit.SECONDS)) {
refreshService.refreshDailySnapshots()
}
} finally {
if (lock.isHeldByCurrentThread) {
lock.unlock()
}
}
} }
} }

View File

@@ -10,11 +10,14 @@ import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mockito import org.mockito.Mockito
import org.redisson.api.RLock
import org.redisson.api.RedissonClient
import org.springframework.boot.test.system.CapturedOutput import org.springframework.boot.test.system.CapturedOutput
import org.springframework.boot.test.system.OutputCaptureExtension import org.springframework.boot.test.system.OutputCaptureExtension
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.transaction.support.TransactionSynchronizationManager import org.springframework.transaction.support.TransactionSynchronizationManager
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.concurrent.TimeUnit
@ExtendWith(OutputCaptureExtension::class) @ExtendWith(OutputCaptureExtension::class)
class RecommendationSnapshotRefreshServiceTest { class RecommendationSnapshotRefreshServiceTest {
@@ -170,7 +173,12 @@ class RecommendationSnapshotRefreshServiceTest {
.getDeclaredMethod("refreshDailySnapshots") .getDeclaredMethod("refreshDailySnapshots")
.getAnnotation(Scheduled::class.java) .getAnnotation(Scheduled::class.java)
val service = Mockito.mock(RecommendationSnapshotRefreshService::class.java) val service = Mockito.mock(RecommendationSnapshotRefreshService::class.java)
val scheduler = RecommendationSnapshotScheduler(service) val redissonClient = Mockito.mock(RedissonClient::class.java)
val lock = Mockito.mock(RLock::class.java)
Mockito.`when`(redissonClient.getLock("lock:recommendation-snapshot-refresh")).thenReturn(lock)
Mockito.`when`(lock.tryLock(0, -1, TimeUnit.SECONDS)).thenReturn(true)
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true)
val scheduler = RecommendationSnapshotScheduler(service, redissonClient)
scheduler.refreshDailySnapshots() scheduler.refreshDailySnapshots()
@@ -179,6 +187,44 @@ class RecommendationSnapshotRefreshServiceTest {
Mockito.verify(service).refreshDailySnapshots() Mockito.verify(service).refreshDailySnapshots()
} }
@Test
@DisplayName("추천 스냅샷 스케줄러는 Redisson lock을 획득한 인스턴스만 갱신을 실행한다")
fun shouldRefreshDailySnapshotsOnlyWhenRedissonLockAcquired() {
val service = Mockito.mock(RecommendationSnapshotRefreshService::class.java)
val redissonClient = Mockito.mock(RedissonClient::class.java)
val lock = Mockito.mock(RLock::class.java)
Mockito.`when`(redissonClient.getLock("lock:recommendation-snapshot-refresh")).thenReturn(lock)
Mockito.`when`(lock.tryLock(0, -1, TimeUnit.SECONDS)).thenReturn(true)
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true)
val scheduler = RecommendationSnapshotScheduler(service, redissonClient)
scheduler.refreshDailySnapshots()
Mockito.verify(redissonClient).getLock("lock:recommendation-snapshot-refresh")
Mockito.verify(lock).tryLock(0, -1, TimeUnit.SECONDS)
Mockito.verify(service).refreshDailySnapshots()
Mockito.verify(lock).unlock()
}
@Test
@DisplayName("추천 스냅샷 스케줄러는 Redisson lock 획득 실패 시 갱신을 건너뛴다")
fun shouldSkipDailySnapshotRefreshWhenRedissonLockNotAcquired() {
val service = Mockito.mock(RecommendationSnapshotRefreshService::class.java)
val redissonClient = Mockito.mock(RedissonClient::class.java)
val lock = Mockito.mock(RLock::class.java)
Mockito.`when`(redissonClient.getLock("lock:recommendation-snapshot-refresh")).thenReturn(lock)
Mockito.`when`(lock.tryLock(0, -1, TimeUnit.SECONDS)).thenReturn(false)
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(false)
val scheduler = RecommendationSnapshotScheduler(service, redissonClient)
scheduler.refreshDailySnapshots()
Mockito.verify(redissonClient).getLock("lock:recommendation-snapshot-refresh")
Mockito.verify(lock).tryLock(0, -1, TimeUnit.SECONDS)
Mockito.verify(service, Mockito.never()).refreshDailySnapshots()
Mockito.verify(lock, Mockito.never()).unlock()
}
private fun service( private fun service(
snapshotPort: RecommendationSnapshotPort = FakeRecommendationSnapshotPort(), snapshotPort: RecommendationSnapshotPort = FakeRecommendationSnapshotPort(),
queryPort: HomeRecommendationQueryPort = Mockito.mock(HomeRecommendationQueryPort::class.java) queryPort: HomeRecommendationQueryPort = Mockito.mock(HomeRecommendationQueryPort::class.java)