From c19a8bc217b0cc2740d5ff5bd498af6b91193def Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 16:06:37 +0900 Subject: [PATCH 01/10] refactor: Implement S3AsyncClient for Non-blocking preview image lookup --- pida-clients/aws-client/build.gradle.kts | 1 + .../com/pida/client/aws/config/AwsConfig.kt | 15 ++++++++ .../pida/client/aws/image/ImageS3Processor.kt | 8 +++- .../pida/client/aws/s3/AwsS3AsyncClient.kt | 37 +++++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3AsyncClient.kt diff --git a/pida-clients/aws-client/build.gradle.kts b/pida-clients/aws-client/build.gradle.kts index 18abbf7a..af4ffe50 100644 --- a/pida-clients/aws-client/build.gradle.kts +++ b/pida-clients/aws-client/build.gradle.kts @@ -2,6 +2,7 @@ dependencies { compileOnly(libs.spring.boot.starter.web) implementation(libs.bundles.aws.client) + implementation(libs.kotlinx.coroutine.core) implementation(project(":pida-core:core-domain")) testImplementation(libs.spring.boot.starter.web) diff --git a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsConfig.kt b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsConfig.kt index fbb908d8..061b0ccc 100644 --- a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsConfig.kt +++ b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsConfig.kt @@ -8,6 +8,7 @@ import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3AsyncClient import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.presigner.S3Presigner import java.net.URI @@ -46,6 +47,20 @@ class AwsConfig( return client.build() } + @Bean(destroyMethod = "close") + fun s3AsyncClient(): S3AsyncClient { + val client = + S3AsyncClient + .builder() + .credentialsProvider(credentialProvider()) + .region(Region.of(awsProperties.region)) + awsProperties.endpoint?.let { + client.endpointOverride(URI.create(awsProperties.endpoint)) + } + + return client.build() + } + @Bean(destroyMethod = "close") fun s3Presigner(): S3Presigner { val client = diff --git a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt index b83753b0..bfff957f 100644 --- a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt +++ b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt @@ -1,6 +1,7 @@ package com.pida.client.aws.image import com.pida.client.aws.config.AwsProperties +import com.pida.client.aws.s3.AwsS3AsyncClient import com.pida.client.aws.s3.AwsS3Client import com.pida.support.aws.ImageS3Caller import com.pida.support.aws.PresignedUrlRateLimiter @@ -16,6 +17,7 @@ import java.time.ZoneId @Component class ImageS3Processor( private val awsS3Client: AwsS3Client, + private val awsS3AsyncClient: AwsS3AsyncClient, private val awsProperties: AwsProperties, private val imageFileConstructor: ImageFileConstructor, private val rateLimiter: PresignedUrlRateLimiter, @@ -91,7 +93,11 @@ class ImageS3Processor( ): S3ImageInfo? { val imageFilePath = imageFileConstructor.imageFilePath(prefix, prefixId) - return listImageObjects(imageFilePath) + return awsS3AsyncClient + .listObjects( + bucketName = awsProperties.s3.bucket, + filePath = imageFilePath, + ).filterNot { it.key().endsWith("/") } .maxByOrNull(S3Object::lastModified) ?.toImageInfo(imageFilePath, Duration.ofSeconds(30)) } diff --git a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3AsyncClient.kt b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3AsyncClient.kt new file mode 100644 index 00000000..c327e0db --- /dev/null +++ b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3AsyncClient.kt @@ -0,0 +1,37 @@ +package com.pida.client.aws.s3 + +import kotlinx.coroutines.future.await +import org.springframework.stereotype.Component +import software.amazon.awssdk.services.s3.S3AsyncClient +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request +import software.amazon.awssdk.services.s3.model.S3Object + +@Component +class AwsS3AsyncClient( + private val s3AsyncClient: S3AsyncClient, +) { + suspend fun listObjects( + bucketName: String, + filePath: String, + ): List { + val allObjects = mutableListOf() + var continuationToken: String? = null + + do { + val request = + ListObjectsV2Request + .builder() + .bucket(bucketName) + .prefix(filePath) + .maxKeys(1000) + .continuationToken(continuationToken) + .build() + + val response = s3AsyncClient.listObjectsV2(request).await() + allObjects.addAll(response.contents()) + continuationToken = response.nextContinuationToken() + } while (response.isTruncated) + + return allObjects + } +} From a841c8270f7d57a8dda37799646587aeabf3a37e Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 16:28:29 +0900 Subject: [PATCH 02/10] feat: add log temporally --- .../com/pida/flowerspot/FlowerSpotFacade.kt | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) 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 72de6b0b..e565f975 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,11 +1,9 @@ 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.extension.logger import com.pida.support.geo.Region import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -22,6 +20,8 @@ class FlowerSpotFacade( const val PREVIEW_IMAGE_TTL = 10L } + private val logger by logger() + suspend fun readFlowerSpotDetails(spotId: Long): FlowerSpotDetails = coroutineScope { val flowerSpotDeferred = async { flowerSpotService.readOneFlowerSpot(spotId) } @@ -51,10 +51,19 @@ class FlowerSpotFacade( if (flowerSpots.isEmpty()) return@coroutineScope emptyList() val bloomingDeferred = async { bloomingService.recentlyBloomingBySpotIds(flowerSpots.map { it.id }) } + + val startTime = System.currentTimeMillis() val previewDeferred = flowerSpots.map { spot -> - async { spot.id to cachedPreviewImage(spot.id) } + async { + spot.id to + imageS3Caller.getPreviewImage( + prefix = ImagePrefix.FLOWERSPOT.value, + prefixId = spot.id, + ) + } } + logger.info("Preview image fetch initiated for ${flowerSpots.size} spots in ${System.currentTimeMillis() - startTime}ms") val bloomingBySpotId = bloomingDeferred.await().groupBy { it.flowerSpotId } val previewBySpotId = previewDeferred.associate { it.await() } @@ -68,15 +77,15 @@ 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, - ) - } +// 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, +// ) +// } } From 8365c8239f07a98db21764fb64e94aff35259c9b Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 16:39:10 +0900 Subject: [PATCH 03/10] feat: temporally set as blocking s3client --- .../pida/client/aws/image/ImageS3Processor.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt index bfff957f..bc50bd71 100644 --- a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt +++ b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt @@ -93,13 +93,24 @@ class ImageS3Processor( ): S3ImageInfo? { val imageFilePath = imageFileConstructor.imageFilePath(prefix, prefixId) - return awsS3AsyncClient - .listObjects( + return awsS3Client + .getBucketListObjects( bucketName = awsProperties.s3.bucket, filePath = imageFilePath, - ).filterNot { it.key().endsWith("/") } + ).contents() + .orEmpty() + .asSequence() + .filterNot { it.key().endsWith("/") } .maxByOrNull(S3Object::lastModified) ?.toImageInfo(imageFilePath, Duration.ofSeconds(30)) + +// return awsS3AsyncClient +// .listObjects( +// bucketName = awsProperties.s3.bucket, +// filePath = imageFilePath, +// ).filterNot { it.key().endsWith("/") } +// .maxByOrNull(S3Object::lastModified) +// ?.toImageInfo(imageFilePath, Duration.ofSeconds(30)) } private fun generateGetUrl( From a71587dd3cefcb59dfa88ac8c0994c85d4fd030c Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 16:49:20 +0900 Subject: [PATCH 04/10] feat: add logs to check querying time --- .../kotlin/com/pida/flowerspot/FlowerSpotFacade.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 e565f975..b9b71fc3 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 @@ -47,12 +47,16 @@ class FlowerSpotFacade( location: FlowerSpotLocation, ): List = coroutineScope { + val point1 = System.currentTimeMillis() val flowerSpots = flowerSpotService.readAllFlowerSpot(region, location) + logger.info("flowerSpots querying time ${System.currentTimeMillis() - point1}ms") if (flowerSpots.isEmpty()) return@coroutineScope emptyList() + val point2 = System.currentTimeMillis() val bloomingDeferred = async { bloomingService.recentlyBloomingBySpotIds(flowerSpots.map { it.id }) } + logger.info("bloomingDeferred querying time ${System.currentTimeMillis() - point2}ms") - val startTime = System.currentTimeMillis() + val point3 = System.currentTimeMillis() val previewDeferred = flowerSpots.map { spot -> async { @@ -63,10 +67,12 @@ class FlowerSpotFacade( ) } } - logger.info("Preview image fetch initiated for ${flowerSpots.size} spots in ${System.currentTimeMillis() - startTime}ms") + logger.info("previewDeferred querying time ${System.currentTimeMillis() - point3}ms") + val point4 = System.currentTimeMillis() val bloomingBySpotId = bloomingDeferred.await().groupBy { it.flowerSpotId } val previewBySpotId = previewDeferred.associate { it.await() } + logger.info("grouping time ${System.currentTimeMillis() - point4}ms") flowerSpots.map { flowerSpot -> FlowerSpotDetails.of( From 6f7be333a561ee9b7b765bd780ae3a1dcca0ea97 Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 16:50:26 +0900 Subject: [PATCH 05/10] feat: restore to use async client --- .../pida/client/aws/image/ImageS3Processor.kt | 17 ++-------- .../com/pida/flowerspot/FlowerSpotFacade.kt | 31 +++++++++---------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt index bc50bd71..bfff957f 100644 --- a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt +++ b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt @@ -93,24 +93,13 @@ class ImageS3Processor( ): S3ImageInfo? { val imageFilePath = imageFileConstructor.imageFilePath(prefix, prefixId) - return awsS3Client - .getBucketListObjects( + return awsS3AsyncClient + .listObjects( bucketName = awsProperties.s3.bucket, filePath = imageFilePath, - ).contents() - .orEmpty() - .asSequence() - .filterNot { it.key().endsWith("/") } + ).filterNot { it.key().endsWith("/") } .maxByOrNull(S3Object::lastModified) ?.toImageInfo(imageFilePath, Duration.ofSeconds(30)) - -// return awsS3AsyncClient -// .listObjects( -// bucketName = awsProperties.s3.bucket, -// filePath = imageFilePath, -// ).filterNot { it.key().endsWith("/") } -// .maxByOrNull(S3Object::lastModified) -// ?.toImageInfo(imageFilePath, Duration.ofSeconds(30)) } private fun generateGetUrl( 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 b9b71fc3..d14b20e1 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.extension.logger import com.pida.support.geo.Region import kotlinx.coroutines.async @@ -60,11 +63,7 @@ class FlowerSpotFacade( val previewDeferred = flowerSpots.map { spot -> async { - spot.id to - imageS3Caller.getPreviewImage( - prefix = ImagePrefix.FLOWERSPOT.value, - prefixId = spot.id, - ) + spot.id to cachedPreviewImage(spot.id) } } logger.info("previewDeferred querying time ${System.currentTimeMillis() - point3}ms") @@ -83,15 +82,15 @@ 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, -// ) -// } + 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, + ) + } } From 4ba9d52361b1bc9b25f40d7aaf73e2a13d84796b Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 17:05:59 +0900 Subject: [PATCH 06/10] feat: add log fetching time non-blocking s3 client --- .../kotlin/com/pida/flowerspot/FlowerSpotFacade.kt | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) 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 d14b20e1..7d8c0463 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 @@ -50,28 +50,22 @@ class FlowerSpotFacade( location: FlowerSpotLocation, ): List = coroutineScope { - val point1 = System.currentTimeMillis() val flowerSpots = flowerSpotService.readAllFlowerSpot(region, location) - logger.info("flowerSpots querying time ${System.currentTimeMillis() - point1}ms") if (flowerSpots.isEmpty()) return@coroutineScope emptyList() - val point2 = System.currentTimeMillis() val bloomingDeferred = async { bloomingService.recentlyBloomingBySpotIds(flowerSpots.map { it.id }) } - logger.info("bloomingDeferred querying time ${System.currentTimeMillis() - point2}ms") - val point3 = System.currentTimeMillis() + val s3Start = System.currentTimeMillis() val previewDeferred = flowerSpots.map { spot -> async { spot.id to cachedPreviewImage(spot.id) } } - logger.info("previewDeferred querying time ${System.currentTimeMillis() - point3}ms") - val point4 = System.currentTimeMillis() - val bloomingBySpotId = bloomingDeferred.await().groupBy { it.flowerSpotId } val previewBySpotId = previewDeferred.associate { it.await() } - logger.info("grouping time ${System.currentTimeMillis() - point4}ms") + logger.info("s3 preview fetch time ${System.currentTimeMillis() - s3Start}ms (${flowerSpots.size} spots)") + val bloomingBySpotId = bloomingDeferred.await().groupBy { it.flowerSpotId } flowerSpots.map { flowerSpot -> FlowerSpotDetails.of( From cec9b6cc00d800a881191a0374f1ab60f383d1f7 Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 17:08:03 +0900 Subject: [PATCH 07/10] feat: disable cache --- .../src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 7d8c0463..41cdad5e 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 @@ -59,7 +59,11 @@ class FlowerSpotFacade( val previewDeferred = flowerSpots.map { spot -> async { - spot.id to cachedPreviewImage(spot.id) + spot.id to + imageS3Caller.getPreviewImage( + prefix = ImagePrefix.FLOWERSPOT.value, + prefixId = spot.id, + ) } } From b4c8379fd4eb548dfa5f3b57048a1c0bd1fa6ebb Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 18:25:26 +0900 Subject: [PATCH 08/10] refactor: replace S3 ListObjectsV2 per spot with stored preview image key --- .../pida/client/aws/image/ImageS3Processor.kt | 8 +++ .../com/pida/blooming/BloomingFacade.kt | 18 +++++-- .../kotlin/com/pida/flowerspot/FlowerSpot.kt | 2 + .../com/pida/flowerspot/FlowerSpotFacade.kt | 52 +++++-------------- .../pida/flowerspot/FlowerSpotRepository.kt | 7 +++ .../com/pida/flowerspot/FlowerSpotService.kt | 6 +++ .../com/pida/flowerspot/FlowerSpotUpdater.kt | 16 ++++++ .../com/pida/support/aws/ImageS3Caller.kt | 7 +++ .../kotlin/com/pida/support/aws/S3ImageUrl.kt | 1 + .../com/pida/blooming/BloomingFacadeTest.kt | 10 ++++ .../pida/flowerspot/FlowerSpotFacadeTest.kt | 21 ++++---- .../flowerspot/FlowerSpotCoreRepository.kt | 10 ++++ .../db/core/flowerspot/FlowerSpotEntity.kt | 14 +++++ 13 files changed, 118 insertions(+), 54 deletions(-) create mode 100644 pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotUpdater.kt diff --git a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt index bfff957f..70057b5f 100644 --- a/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt +++ b/pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt @@ -44,9 +44,11 @@ class ImageS3Processor( Duration.ofSeconds(30), // 만료 시간 최소화 ) + val s3Key = "$imageFilePath/$imageFileName" return S3ImageUrl( presignedUrl, generateGetUrl(imageFilePath, imageFileName), + s3Key, ) } @@ -102,6 +104,12 @@ class ImageS3Processor( ?.toImageInfo(imageFilePath, Duration.ofSeconds(30)) } + override fun generatePresignedUrl(s3Key: String): String { + val filePath = s3Key.substringBeforeLast("/") + val fileName = s3Key.substringAfterLast("/") + return generateGetUrl(filePath, fileName) + } + private fun generateGetUrl( filePath: String, fileName: String, diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingFacade.kt b/pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingFacade.kt index fe5b8b13..b3d6c13d 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingFacade.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingFacade.kt @@ -1,5 +1,6 @@ package com.pida.blooming +import com.pida.flowerspot.FlowerSpotService import com.pida.reporter.RecentReporterService import com.pida.support.aws.ImagePrefix import com.pida.support.aws.ImageS3Caller @@ -20,6 +21,7 @@ class BloomingFacade( private val userService: UserService, private val imageS3Caller: ImageS3Caller, private val eventPublisher: ApplicationEventPublisher, + private val flowerSpotService: FlowerSpotService, ) { suspend fun readBloomingDetailsBySpotId(flowerSpotId: Long): BloomingDetails = coroutineScope { @@ -107,9 +109,19 @@ class BloomingFacade( is NewBlooming.FlowerEvent -> ImagePrefix.FLOWEREVENT.value to newBlooming.flowerEventId } - return BloomingImageUploadUrl.from( - imageS3Caller.createUploadUrl(newBlooming.userId, prefix, prefixId), - ) + val imageUploadUrl = imageS3Caller.createUploadUrl(newBlooming.userId, prefix, prefixId) + + when (newBlooming) { + is NewBlooming.FlowerSpot -> { + flowerSpotService.updatePreviewImageKey( + newBlooming.flowerSpotId, + imageUploadUrl.s3Key, + ) + } + is NewBlooming.FlowerEvent -> {} + } + + return BloomingImageUploadUrl.from(imageUploadUrl) } private fun buildBloomingDetails( diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpot.kt b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpot.kt index 15af5502..3a587a3e 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpot.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpot.kt @@ -16,4 +16,6 @@ data class FlowerSpot( val kind: FlowerKind, val type: FlowerSpotType, val deletedAt: LocalDateTime?, + val previewImageKey: String? = null, + val previewImageUploadedAt: LocalDateTime? = null, ) 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 41cdad5e..43b302c2 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,12 +1,9 @@ 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.extension.logger import com.pida.support.geo.Region import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -18,13 +15,6 @@ 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 - } - - private val logger by logger() - suspend fun readFlowerSpotDetails(spotId: Long): FlowerSpotDetails = coroutineScope { val flowerSpotDeferred = async { flowerSpotService.readOneFlowerSpot(spotId) } @@ -53,42 +43,24 @@ class FlowerSpotFacade( val flowerSpots = flowerSpotService.readAllFlowerSpot(region, location) if (flowerSpots.isEmpty()) return@coroutineScope emptyList() - val bloomingDeferred = async { bloomingService.recentlyBloomingBySpotIds(flowerSpots.map { it.id }) } - - val s3Start = System.currentTimeMillis() - val previewDeferred = - flowerSpots.map { spot -> - async { - spot.id to - imageS3Caller.getPreviewImage( - prefix = ImagePrefix.FLOWERSPOT.value, - prefixId = spot.id, - ) - } - } - - val previewBySpotId = previewDeferred.associate { it.await() } - logger.info("s3 preview fetch time ${System.currentTimeMillis() - s3Start}ms (${flowerSpots.size} spots)") - val bloomingBySpotId = bloomingDeferred.await().groupBy { it.flowerSpotId } + val bloomingBySpotId = + bloomingService + .recentlyBloomingBySpotIds(flowerSpots.map { it.id }) + .groupBy { it.flowerSpotId } flowerSpots.map { flowerSpot -> + val previewImage = + flowerSpot.previewImageKey?.let { key -> + S3ImageInfo( + url = imageS3Caller.generatePresignedUrl(key), + uploadedAt = flowerSpot.previewImageUploadedAt!!, + ) + } FlowerSpotDetails.of( flowerSpot = flowerSpot, bloomings = bloomingBySpotId[flowerSpot.id] ?: emptyList(), - images = listOfNotNull(previewBySpotId[flowerSpot.id]), + images = listOfNotNull(previewImage), ) } } - - 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/FlowerSpotRepository.kt b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotRepository.kt index 73ae3531..67a5700c 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotRepository.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotRepository.kt @@ -1,6 +1,7 @@ package com.pida.flowerspot import com.pida.support.geo.Region +import java.time.LocalDateTime interface FlowerSpotRepository { suspend fun findBy(spotId: Long): FlowerSpot @@ -23,4 +24,10 @@ interface FlowerSpotRepository { ): List fun findByStreetNameContaining(streetName: String): List + + suspend fun updatePreviewImageKey( + spotId: Long, + key: String, + uploadedAt: LocalDateTime, + ) } diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotService.kt b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotService.kt index 59c51a08..b62033c6 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotService.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotService.kt @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service @Service class FlowerSpotService( private val flowerSpotFinder: FlowerSpotFinder, + private val flowerSpotUpdater: FlowerSpotUpdater, ) { suspend fun readAllFlowerSpot( region: Region?, @@ -29,4 +30,9 @@ class FlowerSpotService( return flowerSpotFinder.searchByStreetName(trimmed) } + + suspend fun updatePreviewImageKey( + spotId: Long, + key: String, + ) = flowerSpotUpdater.updatePreviewImageKey(spotId, key) } diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotUpdater.kt b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotUpdater.kt new file mode 100644 index 00000000..faeea875 --- /dev/null +++ b/pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotUpdater.kt @@ -0,0 +1,16 @@ +package com.pida.flowerspot + +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class FlowerSpotUpdater( + private val flowerSpotRepository: FlowerSpotRepository, +) { + suspend fun updatePreviewImageKey( + spotId: Long, + key: String, + ) { + flowerSpotRepository.updatePreviewImageKey(spotId, key, LocalDateTime.now()) + } +} diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt b/pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt index aca86182..36bfa5e6 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt @@ -33,6 +33,13 @@ interface ImageS3Caller { prefixId: Long, ): S3ImageInfo? + /** + * S3 key로부터 presigned GET URL 생성 (로컬 서명, 네트워크 호출 없음) + * + * @param s3Key [String] S3 객체 전체 키 (e.g. "prod/flowerspot/42/abcdef.jpeg") + */ + fun generatePresignedUrl(s3Key: String): String + /** * 서버 사이드 이미지 업로드 * diff --git a/pida-core/core-domain/src/main/kotlin/com/pida/support/aws/S3ImageUrl.kt b/pida-core/core-domain/src/main/kotlin/com/pida/support/aws/S3ImageUrl.kt index 4f811513..19ca151f 100644 --- a/pida-core/core-domain/src/main/kotlin/com/pida/support/aws/S3ImageUrl.kt +++ b/pida-core/core-domain/src/main/kotlin/com/pida/support/aws/S3ImageUrl.kt @@ -3,4 +3,5 @@ package com.pida.support.aws data class S3ImageUrl( val presignedUrl: String, val presignedGetUrl: String, + val s3Key: String, ) diff --git a/pida-core/core-domain/src/test/kotlin/com/pida/blooming/BloomingFacadeTest.kt b/pida-core/core-domain/src/test/kotlin/com/pida/blooming/BloomingFacadeTest.kt index 63a2e616..c62d9b3e 100644 --- a/pida-core/core-domain/src/test/kotlin/com/pida/blooming/BloomingFacadeTest.kt +++ b/pida-core/core-domain/src/test/kotlin/com/pida/blooming/BloomingFacadeTest.kt @@ -1,5 +1,6 @@ package com.pida.blooming +import com.pida.flowerspot.FlowerSpotService import com.pida.reporter.RecentReporterService import com.pida.support.aws.ImagePrefix import com.pida.support.aws.ImageS3Caller @@ -25,6 +26,7 @@ class BloomingFacadeTest { val userService = mockk() val imageS3Caller = mockk() val eventPublisher = mockk() + val flowerSpotService = mockk() val facade = BloomingFacade( bloomingService = bloomingService, @@ -32,6 +34,7 @@ class BloomingFacadeTest { userService = userService, imageS3Caller = imageS3Caller, eventPublisher = eventPublisher, + flowerSpotService = flowerSpotService, ) coEvery { bloomingService.recentlyBloomingByEventId(7L) } returns @@ -90,6 +93,7 @@ class BloomingFacadeTest { val userService = mockk() val imageS3Caller = mockk() val eventPublisher = mockk() + val flowerSpotService = mockk() val facade = BloomingFacade( bloomingService = bloomingService, @@ -97,6 +101,7 @@ class BloomingFacadeTest { userService = userService, imageS3Caller = imageS3Caller, eventPublisher = eventPublisher, + flowerSpotService = flowerSpotService, ) val newBlooming = NewBlooming.FlowerSpot( @@ -121,7 +126,9 @@ class BloomingFacadeTest { S3ImageUrl( presignedUrl = "spot-upload", presignedGetUrl = "spot-preview", + s3Key = "prod/flowerspot/3/abc.jpeg", ) + coEvery { flowerSpotService.updatePreviewImageKey(3L, "prod/flowerspot/3/abc.jpeg") } returns Unit val result = facade.uploadBloomingStatus(newBlooming) @@ -140,6 +147,7 @@ class BloomingFacadeTest { val userService = mockk() val imageS3Caller = mockk() val eventPublisher = mockk() + val flowerSpotService = mockk() val facade = BloomingFacade( bloomingService = bloomingService, @@ -147,6 +155,7 @@ class BloomingFacadeTest { userService = userService, imageS3Caller = imageS3Caller, eventPublisher = eventPublisher, + flowerSpotService = flowerSpotService, ) val newBlooming = NewBlooming.FlowerEvent( @@ -171,6 +180,7 @@ class BloomingFacadeTest { S3ImageUrl( presignedUrl = "event-upload", presignedGetUrl = "event-preview", + s3Key = "prod/flowerevent/7/abc.jpeg", ) val result = facade.uploadBloomingStatus(newBlooming) 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 5184b2db..06482cf8 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 @@ -6,9 +6,7 @@ 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 @@ -35,7 +33,9 @@ class FlowerSpotFacadeTest { 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 = "첫 번째 거리") + val previewKey = "prod/flowerspot/10/abc.jpeg" + val previewUploadedAt = LocalDateTime.of(2026, 3, 20, 12, 0) + val firstSpot = flowerSpot(id = 10L, streetName = "첫 번째 거리", previewImageKey = previewKey, previewImageUploadedAt = previewUploadedAt) val secondSpot = flowerSpot(id = 20L, streetName = "두 번째 거리") coEvery { flowerSpotService.readAllFlowerSpot(region = null, location = location) } returns listOf(firstSpot, secondSpot) @@ -66,12 +66,7 @@ class FlowerSpotFacadeTest { createdAt = LocalDateTime.of(2026, 3, 20, 9, 0), ), ) - coEvery { imageS3Caller.getPreviewImage(ImagePrefix.FLOWERSPOT.value, 10L) } returns - S3ImageInfo( - url = "https://cdn.example.com/flower-spot-10-preview.jpg", - uploadedAt = LocalDateTime.of(2026, 3, 20, 12, 0), - ) - coEvery { imageS3Caller.getPreviewImage(ImagePrefix.FLOWERSPOT.value, 20L) } returns null + every { imageS3Caller.generatePresignedUrl(previewKey) } returns "https://cdn.example.com/flower-spot-10-preview.jpg" val result = facade.findAllFlowerSpot(region = null, location = location) @@ -83,8 +78,8 @@ class FlowerSpotFacadeTest { result.last().bloomingStatus shouldBe BloomingStatus.LITTLE result.last().images shouldBe emptyList() - coVerify(exactly = 1) { imageS3Caller.getPreviewImage(ImagePrefix.FLOWERSPOT.value, 10L) } - coVerify(exactly = 1) { imageS3Caller.getPreviewImage(ImagePrefix.FLOWERSPOT.value, 20L) } + verify(exactly = 1) { imageS3Caller.generatePresignedUrl(previewKey) } + coVerify(exactly = 0) { imageS3Caller.getPreviewImage(any(), any()) } coVerify(exactly = 0) { imageS3Caller.getImageUrl(any(), any(), any()) } } @@ -111,6 +106,8 @@ class FlowerSpotFacadeTest { private fun flowerSpot( id: Long, streetName: String, + previewImageKey: String? = null, + previewImageUploadedAt: LocalDateTime? = null, ) = FlowerSpot( id = id, address = "서울특별시 강남구", @@ -123,6 +120,8 @@ class FlowerSpotFacadeTest { kind = FlowerKind.BLOSSOM, type = FlowerSpotType.WALKING_TRAIL, deletedAt = null, + previewImageKey = previewImageKey, + previewImageUploadedAt = previewImageUploadedAt, ) private fun inMemoryCacheRepository(): CacheRepository = diff --git a/pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotCoreRepository.kt b/pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotCoreRepository.kt index b0f1c24c..dbfb54d7 100644 --- a/pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotCoreRepository.kt +++ b/pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotCoreRepository.kt @@ -8,6 +8,7 @@ import com.pida.support.geo.Region import com.pida.support.tx.TransactionTemplates import com.pida.support.tx.coExecute import org.springframework.stereotype.Repository +import java.time.LocalDateTime @Repository class FlowerSpotCoreRepository( @@ -76,4 +77,13 @@ class FlowerSpotCoreRepository( flowerSpotJpaRepository .findByStreetNameContainingAndDeletedAtIsNull(streetName) .map { it.toFlowerSpot() } + + override suspend fun updatePreviewImageKey( + spotId: Long, + key: String, + uploadedAt: LocalDateTime, + ) = tx.writer.coExecute { + val entity = flowerSpotJpaRepository.findByIdAndDeletedAtIsNullOrElseThrow(spotId) + entity.updatePreviewImage(key, uploadedAt) + } } diff --git a/pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotEntity.kt b/pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotEntity.kt index 07766e4a..f69b2895 100644 --- a/pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotEntity.kt +++ b/pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotEntity.kt @@ -14,6 +14,7 @@ import jakarta.persistence.Index import jakarta.persistence.Table import org.locationtech.jts.geom.LineString import org.locationtech.jts.geom.Point +import java.time.LocalDateTime @Entity @Table( @@ -41,6 +42,9 @@ class FlowerSpotEntity( @Column(columnDefinition = "varchar(30)") val type: FlowerSpotType, ) : BaseEntity() { + var previewImageKey: String? = null + var previewImageUploadedAt: LocalDateTime? = null + fun toFlowerSpot(): FlowerSpot = FlowerSpot( id = id!!, @@ -54,5 +58,15 @@ class FlowerSpotEntity( kind = kind, type = type, deletedAt = deletedAt, + previewImageKey = previewImageKey, + previewImageUploadedAt = previewImageUploadedAt, ) + + fun updatePreviewImage( + key: String, + uploadedAt: LocalDateTime, + ) { + this.previewImageKey = key + this.previewImageUploadedAt = uploadedAt + } } From 86c44a43ad8ff621ab0d98dc640f8a0f41e36089 Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 18:28:21 +0900 Subject: [PATCH 09/10] feat: add logs to get querying time each stage --- .../com/pida/flowerspot/FlowerSpotFacade.kt | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) 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 43b302c2..841ea4e6 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 @@ -4,6 +4,7 @@ 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.extension.logger import com.pida.support.geo.Region import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -15,6 +16,8 @@ class FlowerSpotFacade( private val bloomingService: BloomingService, private val imageS3Caller: ImageS3Caller, ) { + private val logger by logger() + suspend fun readFlowerSpotDetails(spotId: Long): FlowerSpotDetails = coroutineScope { val flowerSpotDeferred = async { flowerSpotService.readOneFlowerSpot(spotId) } @@ -40,27 +43,37 @@ class FlowerSpotFacade( location: FlowerSpotLocation, ): List = coroutineScope { + val t0 = System.currentTimeMillis() val flowerSpots = flowerSpotService.readAllFlowerSpot(region, location) + logger.info("flowerSpot query: ${System.currentTimeMillis() - t0}ms (${flowerSpots.size} spots)") if (flowerSpots.isEmpty()) return@coroutineScope emptyList() + val t1 = System.currentTimeMillis() val bloomingBySpotId = bloomingService .recentlyBloomingBySpotIds(flowerSpots.map { it.id }) .groupBy { it.flowerSpotId } + logger.info("blooming query: ${System.currentTimeMillis() - t1}ms") + + val t2 = System.currentTimeMillis() + val result = + flowerSpots.map { flowerSpot -> + val previewImage = + flowerSpot.previewImageKey?.let { key -> + S3ImageInfo( + url = imageS3Caller.generatePresignedUrl(key), + uploadedAt = flowerSpot.previewImageUploadedAt!!, + ) + } + FlowerSpotDetails.of( + flowerSpot = flowerSpot, + bloomings = bloomingBySpotId[flowerSpot.id] ?: emptyList(), + images = listOfNotNull(previewImage), + ) + } + logger.info("presigned url generation: ${System.currentTimeMillis() - t2}ms") + logger.info("total: ${System.currentTimeMillis() - t0}ms") - flowerSpots.map { flowerSpot -> - val previewImage = - flowerSpot.previewImageKey?.let { key -> - S3ImageInfo( - url = imageS3Caller.generatePresignedUrl(key), - uploadedAt = flowerSpot.previewImageUploadedAt!!, - ) - } - FlowerSpotDetails.of( - flowerSpot = flowerSpot, - bloomings = bloomingBySpotId[flowerSpot.id] ?: emptyList(), - images = listOfNotNull(previewImage), - ) - } + result } } From e40837132e86391e8305b7890f29717b3d1f5883 Mon Sep 17 00:00:00 2001 From: leehaneum Date: Fri, 27 Mar 2026 18:58:11 +0900 Subject: [PATCH 10/10] feat: remove unnecessary logs and improve readability --- .../com/pida/flowerspot/FlowerSpotFacade.kt | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) 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 841ea4e6..ffc5a7f6 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 @@ -4,7 +4,6 @@ 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.extension.logger import com.pida.support.geo.Region import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -16,8 +15,6 @@ class FlowerSpotFacade( private val bloomingService: BloomingService, private val imageS3Caller: ImageS3Caller, ) { - private val logger by logger() - suspend fun readFlowerSpotDetails(spotId: Long): FlowerSpotDetails = coroutineScope { val flowerSpotDeferred = async { flowerSpotService.readOneFlowerSpot(spotId) } @@ -43,37 +40,28 @@ class FlowerSpotFacade( location: FlowerSpotLocation, ): List = coroutineScope { - val t0 = System.currentTimeMillis() val flowerSpots = flowerSpotService.readAllFlowerSpot(region, location) - logger.info("flowerSpot query: ${System.currentTimeMillis() - t0}ms (${flowerSpots.size} spots)") if (flowerSpots.isEmpty()) return@coroutineScope emptyList() - val t1 = System.currentTimeMillis() val bloomingBySpotId = bloomingService .recentlyBloomingBySpotIds(flowerSpots.map { it.id }) .groupBy { it.flowerSpotId } - logger.info("blooming query: ${System.currentTimeMillis() - t1}ms") - val t2 = System.currentTimeMillis() - val result = - flowerSpots.map { flowerSpot -> - val previewImage = - flowerSpot.previewImageKey?.let { key -> - S3ImageInfo( - url = imageS3Caller.generatePresignedUrl(key), - uploadedAt = flowerSpot.previewImageUploadedAt!!, - ) - } - FlowerSpotDetails.of( - flowerSpot = flowerSpot, - bloomings = bloomingBySpotId[flowerSpot.id] ?: emptyList(), - images = listOfNotNull(previewImage), - ) - } - logger.info("presigned url generation: ${System.currentTimeMillis() - t2}ms") - logger.info("total: ${System.currentTimeMillis() - t0}ms") + flowerSpots.map { flowerSpot -> + FlowerSpotDetails.of( + flowerSpot = flowerSpot, + bloomings = bloomingBySpotId[flowerSpot.id] ?: emptyList(), + images = listOfNotNull(previewImagePresignedUrl(flowerSpot)), + ) + } + } - result + private fun previewImagePresignedUrl(flowerSpot: FlowerSpot): S3ImageInfo? = + flowerSpot.previewImageKey?.let { key -> + S3ImageInfo( + url = imageS3Caller.generatePresignedUrl(key), + uploadedAt = flowerSpot.previewImageUploadedAt!!, + ) } }