Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEV-267] redis cache 구현 #8

Merged
merged 4 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.tiketeer.TiketeerWaiting.annotation


@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RedisCacheable(
val key: String = ""
)
Original file line number Diff line number Diff line change
@@ -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<String, String>,
private val objectMapper: ObjectMapper,
private val aspectUtils: AspectUtils) {
@Around("execution(public * *(..)) && @annotation(com.tiketeer.TiketeerWaiting.annotation.RedisCacheable)")
dla0510 marked this conversation as resolved.
Show resolved Hide resolved
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)

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<String>).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<?>")
}
}
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -37,7 +37,7 @@ class RedisConfig {
}

@Bean
fun cacheManager(connectionFactory: RedisConnectionFactory?): RedisCacheManager {
return RedisCacheManager.create(connectionFactory!!)
fun aspectUtils(expressionParser: SpelExpressionParser) : AspectUtils {
return AspectUtils(expressionParser)
}
}
dla0510 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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()
}
}
Comment on lines +7 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분을 따로 Bean으로 등록하는 이유가 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

싱글톤으로 사용하려고 빈으로 등록해둔 검다
spring에서 자동으로 생성해주지 않기 때문

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SpelExpressionParser를 확장한 다른 클래스로 갈아끼우는 게 아니면 이 부분이 활용될 여지가 없어보여서 여쭤봅니다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 답글을 이제 봤네요
SpelExpressionParser를 싱글톤으로 사용해야하는 이유가 있나요?

Copy link
Contributor Author

@punkryn punkryn May 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈으로 등록해서 싱글톤으로 사용하는 거 자체에 의미가 있다고 생각하는데 어떤 점에서 활용될 여지가 없는 건지

잘 이해가 되지 않슴다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

싱크가 안 맞았네요 ㅋㅋㅋ

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 AspectUtil 내부 메서드에서 호출될 때마다 인스턴스를 만들어줬는데

인스턴스를 한번만 만들도록 하고 싶어서 싱글톤으로 했슴다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굳이 빈으로 등록 안 하고 생성자에서 만들어줘도 충분할 거 같슴다

Original file line number Diff line number Diff line change
@@ -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<Ticketings, UUID> {
@RedisCacheable(key = "#id")
override fun findById(id: UUID) : Mono<Ticketings>
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ class GetRankAndTokenUseCase @Autowired constructor(private val redisTemplate: R
override fun getRankAndToken(dto: GetRankAndTokenCommandDto): Mono<GetRankAndTokenResultDto> {
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))
)
}

Expand Down
49 changes: 49 additions & 0 deletions src/main/kotlin/com/tiketeer/TiketeerWaiting/util/AspectUtils.kt
Original file line number Diff line number Diff line change
@@ -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): String {
if (StringUtils.hasText(key)) {
if (key.contains("#") || key.contains("'")) {
val parameterNames: Array<String> = getParamNames(joinPoint)
val args = joinPoint.args
val context = StandardEvaluationContext()
for (i in parameterNames.indices) {
context.setVariable(parameterNames[i], args[i])
}
val value = expressionParser.parseExpression(key).getValue(context)
return value.toString()
}
return key
}
throw RuntimeException("RedisReactiveCache annotation missing key")
}

private fun getParamNames(joinPoint: JoinPoint): Array<String> {
val codeSignature = joinPoint.signature as CodeSignature
return codeSignature.parameterNames
}

fun getTypeReference(method: Method): TypeReference<Any> {
return object : TypeReference<Any>() {
override fun getType(): Type {
return getMethodActualReturnType(method)
}
}
}

private fun getMethodActualReturnType(method: Method): Type {
return (method.genericReturnType as ParameterizedType).actualTypeArguments[0]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading