Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -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
Expand All @@ -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) {
Expand All @@ -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<Blooming> = bloomingFinder.readRecentlyBloomingBySpotId(spotId)

suspend fun recentlyBloomingByEventId(eventId: Long): List<Blooming> = bloomingFinder.readRecentlyBloomingByEventId(eventId)

fun recentlyBloomingBySpotIds(spotIds: List<Long>): List<Blooming> = bloomingFinder.recentlyBloomingBySpotIds(spotIds)
fun recentlyBloomingBySpotIds(spotIds: List<Long>): List<Blooming> =
spotIds.flatMap { spotId -> cachedRecentlyBloomingBySpotId(spotId) }

fun recentlyBloomingByEventIds(eventIds: List<Long>): List<Blooming> =
eventIds.flatMap { eventId -> cachedRecentlyBloomingByEventId(eventId) }

fun recentlyBloomingByEventIds(eventIds: List<Long>): List<Blooming> = bloomingFinder.recentlyBloomingByEventIds(eventIds)
private fun cachedRecentlyBloomingBySpotId(spotId: Long): List<Blooming> =
Cache.cacheBlocking(
ttl = BLOOMING_TTL,
key = "$BLOOMING_SPOT_KEY:$spotId",
typeReference = object : TypeReference<List<Blooming>>() {},
) {
bloomingFinder.recentlyBloomingBySpotIds(listOf(spotId))
}

private fun cachedRecentlyBloomingByEventId(eventId: Long): List<Blooming> =
Cache.cacheBlocking(
ttl = BLOOMING_TTL,
key = "$BLOOMING_EVENT_KEY:$eventId",
typeReference = object : TypeReference<List<Blooming>>() {},
) {
bloomingFinder.recentlyBloomingByEventIds(listOf(eventId))
}

fun verifyTodayBlooming(
userId: Long,
Expand All @@ -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}")
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) }
Expand Down Expand Up @@ -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 }
Expand All @@ -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<S3ImageInfo?>() {},
) {
imageS3Caller.getPreviewImage(
prefix = ImagePrefix.FLOWERSPOT.value,
prefixId = spotId,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -36,12 +37,26 @@ class FlowerSpotFinder(
is FindFlowerSpotPolicyCondition.ByRegionAndLocation -> readAllByLocationAndRegion(condition.region, condition.location)
}

suspend fun readAllByLocation(location: FlowerSpotLocation): List<FlowerSpot> = flowerSpotRepository.findAllByLocation(location)
suspend fun readAllByLocation(location: FlowerSpotLocation): List<FlowerSpot> = readAll().filterByBounds(location)

suspend fun readAllByLocationAndRegion(
region: Region,
location: FlowerSpotLocation,
): List<FlowerSpot> = flowerSpotRepository.findAllByLocationAndRegion(region, location)
): List<FlowerSpot> = readAll().filter { it.region == region }.filterByBounds(location)

private fun List<FlowerSpot>.filterByBounds(location: FlowerSpotLocation): List<FlowerSpot> {
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<FlowerSpot> =
cacheAdvice.invoke(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Cache(
ttl: Long,
key: String,
typeReference: TypeReference<T>,
function: () -> T,
function: suspend () -> T,
): T = cacheAdvice.invoke(ttl, key, typeReference, function)

fun <T> cacheBlocking(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,7 +17,8 @@ class BloomingServiceTest {
val bloomingAppender = mockk<BloomingAppender>()
val bloomingValidator = mockk<BloomingValidator>()
val bloomingFinder = mockk<BloomingFinder>()
val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder)
val cacheRepository = mockk<CacheRepository>(relaxed = true)
val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder, cacheRepository)
val blooming =
Blooming(
id = 1L,
Expand Down Expand Up @@ -46,7 +48,8 @@ class BloomingServiceTest {
val bloomingAppender = mockk<BloomingAppender>()
val bloomingValidator = mockk<BloomingValidator>()
val bloomingFinder = mockk<BloomingFinder>()
val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder)
val cacheRepository = mockk<CacheRepository>(relaxed = true)
val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder, cacheRepository)

val exception =
assertThrows<ErrorException> {
Expand All @@ -63,7 +66,8 @@ class BloomingServiceTest {
val bloomingAppender = mockk<BloomingAppender>()
val bloomingValidator = mockk<BloomingValidator>()
val bloomingFinder = mockk<BloomingFinder>()
val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder)
val cacheRepository = mockk<CacheRepository>(relaxed = true)
val service = BloomingService(bloomingAppender, bloomingValidator, bloomingFinder, cacheRepository)

val exception =
assertThrows<ErrorException> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,6 +32,7 @@ class FlowerSpotFacadeTest {
val flowerSpotService = mockk<FlowerSpotService>()
val bloomingService = mockk<BloomingService>()
val imageS3Caller = mockk<ImageS3Caller>()
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 = "첫 번째 거리")
Expand Down Expand Up @@ -87,6 +94,7 @@ class FlowerSpotFacadeTest {
val flowerSpotService = mockk<FlowerSpotService>()
val bloomingService = mockk<BloomingService>()
val imageS3Caller = mockk<ImageS3Caller>()
Cache(CacheAdvice(inMemoryCacheRepository(), cacheObjectMapper()))
val facade = FlowerSpotFacade(flowerSpotService, bloomingService, imageS3Caller)
val location = FlowerSpotLocation(swLat = null, swLng = null, neLat = null, neLng = null)

Expand Down Expand Up @@ -116,4 +124,28 @@ class FlowerSpotFacadeTest {
type = FlowerSpotType.WALKING_TRAIL,
deletedAt = null,
)

private fun inMemoryCacheRepository(): CacheRepository =
object : CacheRepository {
private val storage = mutableMapOf<String, String>()

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),
)
}
Loading