test #243
@@ -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