diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/annotation/RedisCacheable.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/annotation/RedisCacheable.kt new file mode 100644 index 0000000..23430f3 --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/annotation/RedisCacheable.kt @@ -0,0 +1,9 @@ +package com.tiketeer.TiketeerWaiting.annotation + + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class RedisCacheable( + val key: String = "", + val value: String +) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/aspect/RedisCacheAspect.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/aspect/RedisCacheAspect.kt new file mode 100644 index 0000000..c14a336 --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/aspect/RedisCacheAspect.kt @@ -0,0 +1,76 @@ +package com.tiketeer.TiketeerWaiting.aspect + +import com.fasterxml.jackson.databind.ObjectMapper +import com.tiketeer.TiketeerWaiting.annotation.RedisCacheable +import io.github.oshai.kotlinlogging.KotlinLogging +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.reflect.MethodSignature +import org.springframework.data.redis.core.ReactiveRedisTemplate +import org.springframework.stereotype.Component +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.util.stream.Collectors +import com.tiketeer.TiketeerWaiting.util.AspectUtils + +private val logger = KotlinLogging.logger {} + +@Aspect +@Component +class RedisCacheAspect( + private val redisTemplate: ReactiveRedisTemplate, + private val objectMapper: ObjectMapper, + private val aspectUtils: AspectUtils) { + @Around("execution(public * *(..)) && @annotation(com.tiketeer.TiketeerWaiting.annotation.RedisCacheable)") + fun redisReactiveCacheable(joinPoint: ProceedingJoinPoint): Any { + val methodSignature = joinPoint.signature as MethodSignature + val method = methodSignature.method + + val returnType = method.returnType + val annotation = method.getAnnotation(RedisCacheable::class.java) + + val key = aspectUtils.resolveKey(joinPoint, annotation.key, annotation.value) + + val typeReference = aspectUtils.getTypeReference(method) + + logger.info { "Evaluated Redis cacheKey: $key" } + + val cachedValue = redisTemplate.opsForValue().get(key) + + if (returnType.isAssignableFrom(Mono::class.java)) { + return cachedValue + .map { v -> + objectMapper.readValue(v, typeReference) + } + .switchIfEmpty(Mono.defer { + (joinPoint.proceed(joinPoint.args) as Mono<*>) + .map { t -> + redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(t)).subscribe() + t + } + }) + } else if (returnType.isAssignableFrom(Flux::class.java)) { + return cachedValue + .flatMapMany { v -> + Flux.fromIterable( + (v as List).stream() + .map { e -> objectMapper.readValue(e, typeReference) } + .collect(Collectors.toList()) as List<*> + ) + + } + .switchIfEmpty(Flux.defer { + (joinPoint.proceed(joinPoint.args) as Flux<*>) + .collectList() + .map { t -> + redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(t)).subscribe() + t + } + .flatMapMany { Flux.fromIterable(it) } + }) + } + + throw RuntimeException("RedisReactiveCacheGet: Annotated method has unsupported return type, expected Mono or Flux") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/RedisConfig.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/RedisConfig.kt index 26f9e4c..af42cc0 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/RedisConfig.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/RedisConfig.kt @@ -1,16 +1,16 @@ package com.tiketeer.TiketeerWaiting.configuration +import com.tiketeer.TiketeerWaiting.util.AspectUtils import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile -import org.springframework.data.redis.cache.RedisCacheManager import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory -import org.springframework.data.redis.connection.RedisConnectionFactory import org.springframework.data.redis.connection.RedisStandaloneConfiguration import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory import org.springframework.data.redis.core.ReactiveRedisTemplate import org.springframework.data.redis.serializer.RedisSerializationContext +import org.springframework.expression.spel.standard.SpelExpressionParser @Configuration @Profile("!test") @@ -35,9 +35,4 @@ class RedisConfig { fun redisTemplate(connectionFactory: ReactiveRedisConnectionFactory) : ReactiveRedisTemplate { return ReactiveRedisTemplate(connectionFactory, RedisSerializationContext.string()) } - - @Bean - fun cacheManager(connectionFactory: RedisConnectionFactory?): RedisCacheManager { - return RedisCacheManager.create(connectionFactory!!) - } } \ No newline at end of file diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SpelConfig.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SpelConfig.kt new file mode 100644 index 0000000..0b79d77 --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SpelConfig.kt @@ -0,0 +1,13 @@ +package com.tiketeer.TiketeerWaiting.configuration + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.expression.spel.standard.SpelExpressionParser + +@Configuration +class SpelConfig { + @Bean + fun spelExpressionParser(): SpelExpressionParser { + return SpelExpressionParser() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/ticketing/repository/TicketingRepository.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/ticketing/repository/TicketingRepository.kt index 8e43c9c..b5bfd4d 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/ticketing/repository/TicketingRepository.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/ticketing/repository/TicketingRepository.kt @@ -1,8 +1,13 @@ package com.tiketeer.TiketeerWaiting.domain.ticketing.repository +import com.tiketeer.TiketeerWaiting.annotation.RedisCacheable import com.tiketeer.TiketeerWaiting.domain.ticketing.Ticketings +import org.springframework.cache.annotation.Cacheable import org.springframework.data.repository.reactive.ReactiveCrudRepository +import reactor.core.publisher.Mono import java.util.UUID interface TicketingRepository : ReactiveCrudRepository { + @RedisCacheable(key = "#id", value = "ticketingId") + override fun findById(id: UUID) : Mono } \ No newline at end of file diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/usecase/GetRankAndTokenUseCase.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/usecase/GetRankAndTokenUseCase.kt index 775d4ee..f23028a 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/usecase/GetRankAndTokenUseCase.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/usecase/GetRankAndTokenUseCase.kt @@ -21,13 +21,14 @@ class GetRankAndTokenUseCase @Autowired constructor(private val redisTemplate: R override fun getRankAndToken(dto: GetRankAndTokenCommandDto): Mono { val currentTime = dto.entryTime val token = generateToken(dto.email, dto.ticketingId) + val key = "queue::${dto.ticketingId}" val validateResult = validateSalePeriod(dto.ticketingId, currentTime) val mono = validateResult.flatMap { _ -> - redisTemplate.opsForZSet().rank(dto.ticketingId.toString(), token) + redisTemplate.opsForZSet().rank(key, token) .switchIfEmpty( - redisTemplate.opsForZSet().add(dto.ticketingId.toString(), token, currentTime.toDouble()) - .then(redisTemplate.opsForZSet().rank(dto.ticketingId.toString(), token)) + redisTemplate.opsForZSet().add(key, token, currentTime.toDouble()) + .then(redisTemplate.opsForZSet().rank(key, token)) ) } diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/util/AspectUtils.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/util/AspectUtils.kt new file mode 100644 index 0000000..a4fd293 --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/util/AspectUtils.kt @@ -0,0 +1,49 @@ +package com.tiketeer.TiketeerWaiting.util + +import com.fasterxml.jackson.core.type.TypeReference +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.reflect.CodeSignature +import org.springframework.expression.spel.standard.SpelExpressionParser +import org.springframework.expression.spel.support.StandardEvaluationContext +import org.springframework.stereotype.Component +import org.springframework.util.StringUtils +import java.lang.reflect.Method +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +@Component +class AspectUtils(private val expressionParser: SpelExpressionParser) { + fun resolveKey(joinPoint: JoinPoint, key: String, value: String): String { + if (StringUtils.hasText(key) && StringUtils.hasText(value)) { + if (key.contains("#") || key.contains("'")) { + val parameterNames: Array = getParamNames(joinPoint) + val args = joinPoint.args + val context = StandardEvaluationContext() + for (i in parameterNames.indices) { + context.setVariable(parameterNames[i], args[i]) + } + val v = expressionParser.parseExpression(key).getValue(context) + return "$value::$v" + } + return "$value::$key" + } + throw RuntimeException("RedisReactiveCache annotation missing key or missing value") + } + + private fun getParamNames(joinPoint: JoinPoint): Array { + val codeSignature = joinPoint.signature as CodeSignature + return codeSignature.parameterNames + } + + fun getTypeReference(method: Method): TypeReference { + return object : TypeReference() { + override fun getType(): Type { + return getMethodActualReturnType(method) + } + } + } + + private fun getMethodActualReturnType(method: Method): Type { + return (method.genericReturnType as ParameterizedType).actualTypeArguments[0] + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingControllerTest.kt b/src/test/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingControllerTest.kt index 7530916..4e27377 100644 --- a/src/test/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingControllerTest.kt +++ b/src/test/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingControllerTest.kt @@ -3,8 +3,6 @@ package com.tiketeer.TiketeerWaiting.domain.waiting.controller import com.tiketeer.TiketeerWaiting.auth.constant.JwtMetadata import com.tiketeer.TiketeerWaiting.configuration.EmbeddedRedisConfig import com.tiketeer.TiketeerWaiting.configuration.R2dbcConfiguration -import com.tiketeer.TiketeerWaiting.domain.ticketing.Ticketings -import com.tiketeer.TiketeerWaiting.domain.ticketing.repository.TicketingRepository import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.GetRankAndTokenUseCase import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenCommandDto import com.tiketeer.TiketeerWaiting.testHelper.TestHelper @@ -20,7 +18,6 @@ import org.springframework.context.annotation.Import import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory import org.springframework.r2dbc.core.DatabaseClient import org.springframework.test.web.reactive.server.WebTestClient -import java.nio.ByteBuffer import java.time.LocalDateTime import java.util.Date import java.util.UUID