스프링 스케줄러를 이용하여 콘텐츠 예약 오픈 설정
This commit is contained in:
		| @@ -26,6 +26,7 @@ repositories { | |||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | 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-jpa") | ||||||
|     implementation("org.springframework.boot:spring-boot-starter-data-redis") |     implementation("org.springframework.boot:spring-boot-starter-data-redis") | ||||||
|     implementation("org.springframework.boot:spring-boot-starter-security") |     implementation("org.springframework.boot:spring-boot-starter-security") | ||||||
| @@ -33,6 +34,7 @@ dependencies { | |||||||
|     implementation("com.fasterxml.jackson.module:jackson-module-kotlin") |     implementation("com.fasterxml.jackson.module:jackson-module-kotlin") | ||||||
|     implementation("org.springframework.retry:spring-retry") |     implementation("org.springframework.retry:spring-retry") | ||||||
|     implementation("org.jetbrains.kotlin:kotlin-reflect") |     implementation("org.jetbrains.kotlin:kotlin-reflect") | ||||||
|  |     implementation("org.redisson:redisson-spring-boot-starter:3.17.7") | ||||||
|  |  | ||||||
|     // jwt |     // jwt | ||||||
|     implementation("io.jsonwebtoken:jjwt-api:0.11.5") |     implementation("io.jsonwebtoken:jjwt-api:0.11.5") | ||||||
|   | |||||||
| @@ -4,8 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication | |||||||
| import org.springframework.boot.runApplication | import org.springframework.boot.runApplication | ||||||
| import org.springframework.retry.annotation.EnableRetry | import org.springframework.retry.annotation.EnableRetry | ||||||
| import org.springframework.scheduling.annotation.EnableAsync | import org.springframework.scheduling.annotation.EnableAsync | ||||||
|  | import org.springframework.scheduling.annotation.EnableScheduling | ||||||
|  |  | ||||||
| @SpringBootApplication | @SpringBootApplication | ||||||
|  | @EnableScheduling | ||||||
| @EnableAsync | @EnableAsync | ||||||
| @EnableRetry | @EnableRetry | ||||||
| class SodaLiveApplication | class SodaLiveApplication | ||||||
|   | |||||||
| @@ -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") | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,5 +1,8 @@ | |||||||
| package kr.co.vividnext.sodalive.configs | 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.beans.factory.annotation.Value | ||||||
| import org.springframework.cache.annotation.EnableCaching | import org.springframework.cache.annotation.EnableCaching | ||||||
| import org.springframework.context.annotation.Bean | import org.springframework.context.annotation.Bean | ||||||
| @@ -26,6 +29,17 @@ class RedisConfig( | |||||||
|     @Value("\${spring.redis.port}") |     @Value("\${spring.redis.port}") | ||||||
|     private val port: Int |     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 |     @Bean | ||||||
|     fun redisConnectionFactory(): RedisConnectionFactory { |     fun redisConnectionFactory(): RedisConnectionFactory { | ||||||
|         val clientConfiguration = LettuceClientConfiguration.builder() |         val clientConfiguration = LettuceClientConfiguration.builder() | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper | |||||||
| import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront | import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront | ||||||
| import kr.co.vividnext.sodalive.aws.s3.S3Uploader | import kr.co.vividnext.sodalive.aws.s3.S3Uploader | ||||||
| import kr.co.vividnext.sodalive.common.SodaException | 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.comment.AudioContentCommentRepository | ||||||
| import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag | import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag | ||||||
| import kr.co.vividnext.sodalive.content.hashtag.HashTag | import kr.co.vividnext.sodalive.content.hashtag.HashTag | ||||||
| @@ -402,6 +403,7 @@ class AudioContentService( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @SchedulerOnly | ||||||
|     @Transactional |     @Transactional | ||||||
|     fun releaseContent() { |     fun releaseContent() { | ||||||
|         val contentIdList = repository.getNotReleaseContentId() |         val contentIdList = repository.getNotReleaseContentId() | ||||||
|   | |||||||
| @@ -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("락을 획득하지 못해서 배포를 건너뜁니다") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user