diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingService.kt b/pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingService.kt index 6490f6c..ff23f81 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingService.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingService.kt @@ -1,5 +1,8 @@ package com.pida.blooming +import com.fasterxml.jackson.core.type.TypeReference +import com.pida.support.cache.Cache +import com.pida.support.cache.CacheRepository import com.pida.support.error.ErrorException import com.pida.support.error.ErrorType import org.springframework.stereotype.Service @@ -9,7 +12,14 @@ class BloomingService( private val bloomingAppender: BloomingAppender, private val bloomingValidator: BloomingValidator, private val bloomingFinder: BloomingFinder, + private val cacheRepository: CacheRepository, ) { + companion object { + const val BLOOMING_SPOT_KEY = "blooming:spot" + const val BLOOMING_EVENT_KEY = "blooming:event" + const val BLOOMING_TTL = 30L + } + suspend fun add(newBlooming: NewBlooming): Blooming { val blooming = when (newBlooming) { @@ -22,16 +32,38 @@ class BloomingService( } bloomingValidator.addValidate(blooming) - return bloomingAppender.add(newBlooming) + val result = bloomingAppender.add(newBlooming) + evictBloomingCache(newBlooming) + return result } suspend fun recentlyBloomingBySpotId(spotId: Long): List = bloomingFinder.readRecentlyBloomingBySpotId(spotId) suspend fun recentlyBloomingByEventId(eventId: Long): List = bloomingFinder.readRecentlyBloomingByEventId(eventId) - fun recentlyBloomingBySpotIds(spotIds: List): List = bloomingFinder.recentlyBloomingBySpotIds(spotIds) + fun recentlyBloomingBySpotIds(spotIds: List): List = + spotIds.flatMap { spotId -> cachedRecentlyBloomingBySpotId(spotId) } + + fun recentlyBloomingByEventIds(eventIds: List): List = + eventIds.flatMap { eventId -> cachedRecentlyBloomingByEventId(eventId) } - fun recentlyBloomingByEventIds(eventIds: List): List = bloomingFinder.recentlyBloomingByEventIds(eventIds) + private fun cachedRecentlyBloomingBySpotId(spotId: Long): List = + Cache.cacheBlocking( + ttl = BLOOMING_TTL, + key = "$BLOOMING_SPOT_KEY:$spotId", + typeReference = object : TypeReference>() {}, + ) { + bloomingFinder.recentlyBloomingBySpotIds(listOf(spotId)) + } + + private fun cachedRecentlyBloomingByEventId(eventId: Long): List = + Cache.cacheBlocking( + ttl = BLOOMING_TTL, + key = "$BLOOMING_EVENT_KEY:$eventId", + typeReference = object : TypeReference>() {}, + ) { + bloomingFinder.recentlyBloomingByEventIds(listOf(eventId)) + } fun verifyTodayBlooming( userId: Long, @@ -52,4 +84,11 @@ class BloomingService( return bloomingValidator.todayBloomingValidate(blooming) } + + private fun evictBloomingCache(newBlooming: NewBlooming) { + when (newBlooming) { + is NewBlooming.FlowerSpot -> cacheRepository.delete("$BLOOMING_SPOT_KEY:${newBlooming.flowerSpotId}") + is NewBlooming.FlowerEvent -> cacheRepository.delete("$BLOOMING_EVENT_KEY:${newBlooming.flowerEventId}") + } + } } diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt index 15ebca1..72de6b0 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt @@ -1,8 +1,11 @@ package com.pida.flowerspot +import com.fasterxml.jackson.core.type.TypeReference import com.pida.blooming.BloomingService import com.pida.support.aws.ImagePrefix import com.pida.support.aws.ImageS3Caller +import com.pida.support.aws.S3ImageInfo +import com.pida.support.cache.Cache import com.pida.support.geo.Region import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -14,6 +17,11 @@ class FlowerSpotFacade( private val bloomingService: BloomingService, private val imageS3Caller: ImageS3Caller, ) { + companion object { + const val PREVIEW_IMAGE_KEY = "spot:preview" + const val PREVIEW_IMAGE_TTL = 10L + } + suspend fun readFlowerSpotDetails(spotId: Long): FlowerSpotDetails = coroutineScope { val flowerSpotDeferred = async { flowerSpotService.readOneFlowerSpot(spotId) } @@ -45,13 +53,7 @@ class FlowerSpotFacade( val bloomingDeferred = async { bloomingService.recentlyBloomingBySpotIds(flowerSpots.map { it.id }) } val previewDeferred = flowerSpots.map { spot -> - async { - spot.id to - imageS3Caller.getPreviewImage( - prefix = ImagePrefix.FLOWERSPOT.value, - prefixId = spot.id, - ) - } + async { spot.id to cachedPreviewImage(spot.id) } } val bloomingBySpotId = bloomingDeferred.await().groupBy { it.flowerSpotId } @@ -65,4 +67,16 @@ class FlowerSpotFacade( ) } } + + private suspend fun cachedPreviewImage(spotId: Long): S3ImageInfo? = + Cache.cache( + ttl = PREVIEW_IMAGE_TTL, + key = "$PREVIEW_IMAGE_KEY:$spotId", + typeReference = object : TypeReference() {}, + ) { + imageS3Caller.getPreviewImage( + prefix = ImagePrefix.FLOWERSPOT.value, + prefixId = spotId, + ) + } } diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFinder.kt b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFinder.kt index c93aa0c..8f40fcf 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFinder.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFinder.kt @@ -2,6 +2,7 @@ package com.pida.flowerspot import com.fasterxml.jackson.core.type.TypeReference import com.pida.support.cache.CacheAdvice +import com.pida.support.geo.GeoJson import com.pida.support.geo.Region import org.springframework.stereotype.Component @@ -36,12 +37,26 @@ class FlowerSpotFinder( is FindFlowerSpotPolicyCondition.ByRegionAndLocation -> readAllByLocationAndRegion(condition.region, condition.location) } - suspend fun readAllByLocation(location: FlowerSpotLocation): List = flowerSpotRepository.findAllByLocation(location) + suspend fun readAllByLocation(location: FlowerSpotLocation): List = readAll().filterByBounds(location) suspend fun readAllByLocationAndRegion( region: Region, location: FlowerSpotLocation, - ): List = flowerSpotRepository.findAllByLocationAndRegion(region, location) + ): List = readAll().filter { it.region == region }.filterByBounds(location) + + private fun List.filterByBounds(location: FlowerSpotLocation): List { + val swLat = location.swLat ?: return this + val swLng = location.swLng ?: return this + val neLat = location.neLat ?: return this + val neLng = location.neLng ?: return this + + return filter { spot -> + val point = spot.pinPoint as? GeoJson.Point ?: return@filter false + val lng = point.coordinates[0] + val lat = point.coordinates[1] + lat in swLat..neLat && lng in swLng..neLng + } + } suspend fun searchByStreetName(streetName: String): List = cacheAdvice.invoke( diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/support/cache/Cache.kt b/pida-core/core-domain/src/main/kotlin/com/pida/support/cache/Cache.kt index 97659ba..1da555a 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/support/cache/Cache.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/support/cache/Cache.kt @@ -19,7 +19,7 @@ class Cache( ttl: Long, key: String, typeReference: TypeReference, - function: () -> T, + function: suspend () -> T, ): T = cacheAdvice.invoke(ttl, key, typeReference, function) fun cacheBlocking( diff --git a/pida-core/core-domain/src/test/kotlin/com/pida/blooming/BloomingServiceTest.kt b/pida-core/core-domain/src/test/kotlin/com/pida/blooming/BloomingServiceTest.kt index 4a7e5fe..e68a681 100644 --- a/pida-core/core-domain/src/test/kotlin/com/pida/blooming/BloomingServiceTest.kt +++ b/pida-core/core-domain/src/test/kotlin/com/pida/blooming/BloomingServiceTest.kt @@ -1,5 +1,6 @@ package com.pida.blooming +import com.pida.support.cache.CacheRepository import com.pida.support.error.ErrorException import com.pida.support.error.ErrorType import io.kotest.matchers.shouldBe @@ -16,7 +17,8 @@ class BloomingServiceTest { val bloomingAppender = mockk() val bloomingValidator = mockk() val bloomingFinder = mockk() - val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder) + val cacheRepository = mockk(relaxed = true) + val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder, cacheRepository) val blooming = Blooming( id = 1L, @@ -46,7 +48,8 @@ class BloomingServiceTest { val bloomingAppender = mockk() val bloomingValidator = mockk() val bloomingFinder = mockk() - val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder) + val cacheRepository = mockk(relaxed = true) + val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder, cacheRepository) val exception = assertThrows { @@ -63,7 +66,8 @@ class BloomingServiceTest { val bloomingAppender = mockk() val bloomingValidator = mockk() val bloomingFinder = mockk() - val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder) + val cacheRepository = mockk(relaxed = true) + val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder, cacheRepository) val exception = assertThrows { diff --git a/pida-core/core-domain/src/test/kotlin/com/pida/flowerspot/FlowerSpotFacadeTest.kt b/pida-core/core-domain/src/test/kotlin/com/pida/flowerspot/FlowerSpotFacadeTest.kt index b0cab33..5184b2d 100644 --- a/pida-core/core-domain/src/test/kotlin/com/pida/flowerspot/FlowerSpotFacadeTest.kt +++ b/pida-core/core-domain/src/test/kotlin/com/pida/flowerspot/FlowerSpotFacadeTest.kt @@ -1,11 +1,17 @@ package com.pida.flowerspot +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.pida.blooming.Blooming import com.pida.blooming.BloomingService import com.pida.blooming.BloomingStatus import com.pida.support.aws.ImagePrefix import com.pida.support.aws.ImageS3Caller import com.pida.support.aws.S3ImageInfo +import com.pida.support.cache.Cache +import com.pida.support.cache.CacheAdvice +import com.pida.support.cache.CacheRepository import com.pida.support.geo.GeoJson import com.pida.support.geo.Region import io.kotest.matchers.collections.shouldContainExactly @@ -26,6 +32,7 @@ class FlowerSpotFacadeTest { val flowerSpotService = mockk() val bloomingService = mockk() val imageS3Caller = mockk() + Cache(CacheAdvice(inMemoryCacheRepository(), cacheObjectMapper())) val facade = FlowerSpotFacade(flowerSpotService, bloomingService, imageS3Caller) val location = FlowerSpotLocation(swLat = null, swLng = null, neLat = null, neLng = null) val firstSpot = flowerSpot(id = 10L, streetName = "첫 번째 거리") @@ -87,6 +94,7 @@ class FlowerSpotFacadeTest { val flowerSpotService = mockk() val bloomingService = mockk() val imageS3Caller = mockk() + Cache(CacheAdvice(inMemoryCacheRepository(), cacheObjectMapper())) val facade = FlowerSpotFacade(flowerSpotService, bloomingService, imageS3Caller) val location = FlowerSpotLocation(swLat = null, swLng = null, neLat = null, neLng = null) @@ -116,4 +124,28 @@ class FlowerSpotFacadeTest { type = FlowerSpotType.WALKING_TRAIL, deletedAt = null, ) + + private fun inMemoryCacheRepository(): CacheRepository = + object : CacheRepository { + private val storage = mutableMapOf() + + override fun get(key: String): String? = storage[key] + + override fun put( + key: String, + value: String, + ttl: Long, + ) { + storage[key] = value + } + + override fun delete(key: String) { + storage.remove(key) + } + } + + private fun cacheObjectMapper() = + jacksonObjectMapper().registerModule( + SimpleModule().addSerializer(LocalDateTime::class.java, ToStringSerializer.instance), + ) }