diff --git a/build.gradle.kts b/build.gradle.kts index 2d3a3e5..2e02f9e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,7 @@ repositories { } dependencies { + implementation("org.springframework.boot:spring-boot-starter-aop") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springframework.boot:spring-boot-starter-security") @@ -33,6 +34,7 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.retry:spring-retry") implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.redisson:redisson-spring-boot-starter:3.17.7") // jwt implementation("io.jsonwebtoken:jjwt-api:0.11.5") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt index 78fe613..d160bf6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt @@ -4,8 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.retry.annotation.EnableRetry import org.springframework.scheduling.annotation.EnableAsync +import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication +@EnableScheduling @EnableAsync @EnableRetry class SodaLiveApplication diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/annotation/SchedulerOnly.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/annotation/SchedulerOnly.kt new file mode 100644 index 0000000..2c29ee8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/annotation/SchedulerOnly.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.common.annotation + +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.springframework.stereotype.Component + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class SchedulerOnly + +@Aspect +@Component +class SchedulerOnlyAspect { + + @Before("@annotation(SchedulerOnly)") + fun checkSchedulerAccess() { + if (!isSchedulerThread()) { + throw IllegalStateException("잘못된 접근입니다.") + } + } + + private fun isSchedulerThread(): Boolean { + // 스케줄러 스레드 여부를 판단하는 간단한 로직 + return Thread.currentThread().name.contains("scheduler") + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt index 09989bd..0e98856 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt @@ -1,5 +1,8 @@ package kr.co.vividnext.sodalive.configs +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config import org.springframework.beans.factory.annotation.Value import org.springframework.cache.annotation.EnableCaching import org.springframework.context.annotation.Bean @@ -26,6 +29,17 @@ class RedisConfig( @Value("\${spring.redis.port}") private val port: Int ) { + @Bean + fun redissonClient(): RedissonClient { + val config = Config() + config.useSingleServer() + .setAddress("redis://$host:$port") + .setConnectionMinimumIdleSize(1) // 최소 유휴 연결: 1 + .setConnectionPoolSize(5) // 최대 연결 풀 크기: 5 + + return Redisson.create(config) + } + @Bean fun redisConnectionFactory(): RedisConnectionFactory { val clientConfiguration = LettuceClientConfiguration.builder() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index d971e56..888bd31 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.common.annotation.SchedulerOnly import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag import kr.co.vividnext.sodalive.content.hashtag.HashTag @@ -402,6 +403,7 @@ class AudioContentService( } } + @SchedulerOnly @Transactional fun releaseContent() { val contentIdList = repository.getNotReleaseContentId() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseSchedulerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseSchedulerService.kt new file mode 100644 index 0000000..fdd62bd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseSchedulerService.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.scheduler + +import kr.co.vividnext.sodalive.content.AudioContentService +import org.redisson.api.RedissonClient +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.util.concurrent.TimeUnit + +@Service +class AudioContentReleaseSchedulerService( + private val redissonClient: RedissonClient, + private val audioContentService: AudioContentService +) { + @Scheduled(fixedRate = 1000 * 60 * 5) + fun release() { + val lock = redissonClient.getLock("lock:audioContentRelease") + + if (lock.tryLock(10, TimeUnit.SECONDS)) { + try { + println("락을 획득하여 배포를 시작합니다.") + audioContentService.releaseContent() + } finally { + if (lock.isHeldByCurrentThread) { + lock.unlock() + println("락 해제") + } + } + } else { + println("락을 획득하지 못해서 배포를 건너뜁니다") + } + } +}