Skip to content

Conversation

@LeeHanEum
Copy link
Collaborator

@LeeHanEum LeeHanEum commented Dec 23, 2025

🌱 관련 이슈

📌 작업 내용 및 특이 사항

  • S3 연동
  • presigned url 생성
  • token bucket 기반 rate limit

📝 참고

  • redis 쓰고 있어서 처리율 제한기는 향후 복잡해지면 redis로 이관해도 될것 같아요~

📌 체크 리스트

  • 리뷰어를 추가하셨나요 ?
  • 변경사항에 대해 충분히 설명하고 있나요 ?

Summary by CodeRabbit

  • New Features

    • Image upload via presigned S3 upload URL (with origin image URL) and retrieval of image URLs for flower spots
    • Multi-environment AWS S3 configuration (local/dev/prod)
  • Bug Fixes

    • Controller now returns the upload URL payload directly for image uploads
  • Chores

    • AWS SDK upgraded; added token-bucket and in-memory cache libraries for rate limiting
    • Implemented per-user presigned-URL rate limiting (5 requests/min) and new 429 error type

✏️ Tip: You can customize this high-level summary in your review settings.

@LeeHanEum LeeHanEum self-assigned this Dec 23, 2025
@LeeHanEum LeeHanEum requested a review from char-yb as a code owner December 23, 2025 17:28
@LeeHanEum LeeHanEum linked an issue Dec 23, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

📝 Walkthrough

Walkthrough

Adds an S3 image-upload subsystem: Gradle version/library updates; new AWS client, configuration, and properties; S3 helpers producing presigned URLs; per-user rate limiting (Bucket4j + Caffeine); DTOs and error types; facade/controller wiring to return upload URLs; aws.yml enabled.

Changes

Cohort / File(s) Summary
Build config
gradle/libs.versions.toml, pida-core/core-domain/build.gradle.kts, pida-clients/aws-client/build.gradle.kts
Updated AWS SDK version; added bucket4j and caffeine versions & library entries; added Bucket4j/Caffeine deps to core-domain; removed explicit S3 dependency from aws-client (now managed via libs).
AWS client — config & properties
pida-clients/aws-client/src/main/kotlin/.../AwsConfig.kt, .../AwsProperties.kt, pida-clients/aws-client/src/main/resources/aws.yml
New AwsConfig Spring beans (credential provider, S3Client, S3Presigner) and AwsProperties config binding; added profile-aware aws.yml.
AWS client — image & s3 helpers
pida-clients/aws-client/src/main/kotlin/.../ImageFileConstructor.kt, .../ImageS3Processor.kt, .../s3/AwsS3Client.kt
New components to build file names/paths, generate presigned upload/get URLs, list objects, and implement ImageS3Caller; ImageS3Processor enforces rate limiting.
Core domain — S3 support & DTOs
pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt, .../S3ImageUrl.kt, .../ImagePrefix.kt, .../BloomingImageUploadUrl.kt
Added ImageS3Caller interface, S3ImageUrl DTO, ImagePrefix enum, and BloomingImageUploadUrl mapping.
Core domain — rate limiting
pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimitPolicy.kt, .../PresignedUrlRateLimiter.kt
Added Bucket4j bandwidth policy and a Caffeine-backed per-user rate limiter that throws EXCEED_RATE_LIMIT when limit exceeded.
Core domain — errors
pida-core/core-domain/src/main/kotlin/com/pida/support/error/ErrorKind.kt, .../ErrorType.kt
Added TOO_MANY_REQUESTS error kind and EXCEED_RATE_LIMIT error type (429).
Core domain — facade & wiring
pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingFacade.kt
BloomingFacade now depends on ImageS3Caller and exposes uploadBloomingStatus(...) that persists and returns a BloomingImageUploadUrl.
API layer — controller & config import
pida-core/core-api/src/main/kotlin/.../BloomingController.kt, pida-core/core-api/src/main/resources/application.yml
bloomingAdd now returns BloomingImageUploadUrl delegating to facade; application.yml imports aws.yml.
Presentation — flower spot details
pida-core/core-api/src/main/kotlin/.../FlowerSpotDetailsResponse.kt, pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotDetails.kt, pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt
Added imageUrls to domain DTO and API response; FlowerSpotFacade fetches image URLs via ImageS3Caller and passes them into details.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as BloomingController
    participant Facade as BloomingFacade
    participant Repo as BloomingRepository
    participant ImageCaller as ImageS3Caller (ImageS3Processor)
    participant RateLimiter as PresignedUrlRateLimiter
    participant AwsClient as AwsS3Client
    participant AWS as S3Presigner/S3Client

    Client->>Controller: POST /blooming (NewBlooming)
    Controller->>Facade: uploadBloomingStatus(newBlooming)
    Facade->>Repo: persist NewBlooming -> Blooming (spotId)
    Facade->>ImageCaller: createUploadUrl(userId, ImagePrefix.FLOWERSPOT, spotId)
    ImageCaller->>RateLimiter: consume(userId)
    alt token available
        RateLimiter-->>ImageCaller: ok
        ImageCaller->>AwsClient: generateUploadUrl(bucket, path, filename, ttl)
        AwsClient->>AWS: presign PutObjectRequest
        AWS-->>AwsClient: presignedUrl
        AwsClient-->>ImageCaller: S3ImageUrl(presignedUrl, imageUrl)
        ImageCaller-->>Facade: BloomingImageUploadUrl.from(S3ImageUrl)
        Facade-->>Controller: BloomingImageUploadUrl
        Controller-->>Client: 200 { uploadUrl, imageUrl }
    else rate limit exceeded
        RateLimiter-->>ImageCaller: throws EXCEED_RATE_LIMIT (429)
        ImageCaller-->>Facade: propagate error
        Facade-->>Controller: propagate error
        Controller-->>Client: 429 TOO_MANY_REQUESTS
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"I hopped through code and made a key,
A presigned door to store a pic for free.
Tokens count and Caffeine hums,
Bucket4j guards the upload drums.
— 🐇 (cheers for S3!)"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[Feature] 이미지 업로드를 위한 S3 연동' clearly summarizes the main change: S3 integration for image upload functionality.
Linked Issues check ✅ Passed All coding requirements from issue #67 (AWS S3 image upload module development) are implemented: S3 client integration, presigned URL generation, rate limiting, and image URL retrieval functionality.
Out of Scope Changes check ✅ Passed All changes directly support S3 image upload functionality. AWS SDK upgrades and new dependencies (Bucket4j, Caffeine) are necessary for core features, not out-of-scope.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/s3-image-upload

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 730da99 and a115c1c.

📒 Files selected for processing (8)
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageFileConstructor.kt
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt
  • pida-core/core-api/src/main/kotlin/com/pida/presentation/v1/flowerspot/response/FlowerSpotDetailsResponse.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotDetails.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt
🧰 Additional context used
🧬 Code graph analysis (1)
pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotDetails.kt (2)
pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpot.kt (1)
  • id (5-16)
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotEntity.kt (1)
  • name (16-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI (21, 2.1.0, corretto)
🔇 Additional comments (7)
pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotDetails.kt (1)

19-40: LGTM! Clean addition of imageUrls support.

The imageUrls property is properly added with sensible defaults and correctly propagated through the factory method.

pida-core/core-api/src/main/kotlin/com/pida/presentation/v1/flowerspot/response/FlowerSpotDetailsResponse.kt (1)

55-79: LGTM! Proper API response integration.

The imageUrls field is correctly added with appropriate schema documentation and properly mapped from the domain model.

pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt (1)

1-25: LGTM! Clean interface design.

The interface clearly defines S3 image operations with appropriate method signatures. The mix of synchronous (createUploadUrl) and suspending (getImageUrl) functions aligns with their respective use cases.

pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt (1)

14-34: LGTM! Proper concurrent image URL fetching.

The imageS3Caller integration correctly uses async to fetch image URLs concurrently with other data, and properly passes the results to the factory method.

pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt (1)

14-39: LGTM! Robust rate limiting implementation.

The Caffeine-based cache with TTL and size limits properly addresses the memory leak concerns from previous reviews. The Bucket4j integration is correct, and the implementation appropriately throws an exception when rate limits are exceeded.

pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt (2)

42-53: LGTM! Clean conditional logic.

The method correctly handles both single-file retrieval and directory listing cases with appropriate helper delegation.


67-82: LGTM! Proper S3 object listing.

The method correctly filters out directory markers and extracts filenames before generating presigned URLs.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (5)
pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsConfig.kt (1)

36-47: Inject the credentialProvider bean instead of calling the method directly.

Calling credentialProvider() directly creates a new instance each time instead of reusing the Spring-managed bean. This results in multiple credential provider instances being created unnecessarily.

🔎 Proposed fix
-    @Bean(destroyMethod = "close") // 스프링 종료 시 커넥션 풀 정리
-    fun s3Client(): S3Client {
+    @Bean(destroyMethod = "close")
+    fun s3Client(credentialsProvider: AwsCredentialsProvider): S3Client {
         val client =
             S3Client
                 .builder()
-                .credentialsProvider(credentialProvider())
+                .credentialsProvider(credentialsProvider)
                 .region(Region.of(awsProperties.region))
         awsProperties.endpoint?.let {
             client.endpointOverride(URI.create(awsProperties.endpoint))
         }
 
         return client.build()
     }

Apply the same pattern to s3Presigner():

     @Bean(destroyMethod = "close")
-    fun s3Presigner(): S3Presigner {
+    fun s3Presigner(credentialsProvider: AwsCredentialsProvider): S3Presigner {
         val client =
             S3Presigner
                 .builder()
-                .credentialsProvider(credentialProvider())
+                .credentialsProvider(credentialsProvider)
                 .region(Region.of(awsProperties.region))
pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimitPolicy.kt (1)

6-13: Consider making the rate limit configurable.

The hardcoded 5 requests per minute limit works for now, but making this configurable via properties would allow tuning without code changes.

🔎 Example using configuration properties
@ConfigurationProperties(prefix = "rate-limit.presigned-url")
data class PresignedUrlRateLimitProperties(
    val capacity: Long = 5,
    val refillPerMinute: Long = 5,
)

Then inject and use these properties in a component that builds the Bandwidth.

pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageFileConstructor.kt (1)

32-35: Increase filename entropy by including numbers.

The random filename generator uses only lowercase letters (26 possible characters), which provides less collision resistance than including numbers (36 possible characters). With only letters, the chance of collision is higher, especially at scale.

🔎 Proposed improvement
     companion object {
         private val RANDOM = SecureRandom() // 파일명 생성용 난수기
-        private const val LOWERCASE_A = 97
-        private const val LOWERCASE_Z = 122
+        private const val ALPHANUMERIC_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
         private const val FILENAME_LENGTH = 24
     }

     private fun randomFileName(): String =
-        (1..FILENAME_LENGTH)
-            .map { RANDOM.nextInt(LOWERCASE_A, LOWERCASE_Z + 1).toChar() }
-            .joinToString("")
+        (1..FILENAME_LENGTH)
+            .map { ALPHANUMERIC_CHARS[RANDOM.nextInt(ALPHANUMERIC_CHARS.length)] }
+            .joinToString("")
pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt (1)

27-28: Unused helper method with TODO.

The retryAfterSeconds method is currently unused. The TODO comment indicates it may be used for Retry-After headers in the future.

Would you like me to help implement the Retry-After header support now, or track this as a separate issue?

pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt (1)

17-19: Environment variable read at class-load time may not reflect runtime changes.

The profile is read from SPRING_PROFILES_ACTIVE when the class is loaded (companion object initialization). If the environment variable changes or Spring's active profile differs from the OS environment variable, this could cause path mismatches.

Consider injecting the active profile from Spring's Environment bean instead:

🔎 Proposed refactor
+import org.springframework.core.env.Environment
+
 @Component
 class AwsS3Client(
     private val s3Presigner: S3Presigner,
     private val s3Client: S3Client,
+    private val environment: Environment,
 ) {
-    companion object {
-        private val profile = System.getenv("SPRING_PROFILES_ACTIVE") ?: "dev"
-    }
+    private val profile: String
+        get() = environment.activeProfiles.firstOrNull() ?: "dev"

This ensures the profile reflects Spring's actual runtime configuration.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19cdc75 and e0766be.

📒 Files selected for processing (21)
  • gradle/libs.versions.toml
  • pida-clients/aws-client/build.gradle.kts
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/.gitkeep
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsConfig.kt
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsProperties.kt
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageFileConstructor.kt
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt
  • pida-clients/aws-client/src/main/resources/aws.yml
  • pida-core/core-api/src/main/kotlin/com/pida/presentation/v1/blooming/BloomingController.kt
  • pida-core/core-api/src/main/resources/application.yml
  • pida-core/core-domain/build.gradle.kts
  • pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingFacade.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingImageUploadUrl.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImagePrefix.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimitPolicy.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/S3ImageUrl.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/error/ErrorKind.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/error/ErrorType.kt
💤 Files with no reviewable changes (1)
  • pida-clients/aws-client/build.gradle.kts
🧰 Additional context used
🧬 Code graph analysis (2)
pida-core/core-domain/build.gradle.kts (1)
build.gradle.kts (2)
  • implementation (43-56)
  • libs (23-83)
pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsProperties.kt (2)
pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmConfig.kt (1)
  • fcmProperties (10-28)
pida-clients/notification/src/main/kotlin/com/pida/client/notification/FcmProperties.kt (1)
  • prefix (5-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI (21, 2.1.0, corretto)
🔇 Additional comments (11)
pida-core/core-api/src/main/resources/application.yml (1)

21-21: LGTM! The addition of aws.yml to the configuration imports is correct and all prerequisites are met.

The aws.yml file exists in the pida-clients/aws-client module (which is a declared dependency of core-api), making it accessible via the classpath. AWS credentials are properly configured using environment variables (${AWS_ACCESS_KEY}, ${AWS_SECRET_KEY}, etc.) with fallback defaults for the local profile, and the configuration is profile-aware for local, dev, and prod environments.

pida-core/core-domain/src/main/kotlin/com/pida/support/error/ErrorType.kt (1)

56-58: LGTM!

The rate limit error type is correctly implemented with HTTP 429 status code, appropriate TOO_MANY_REQUESTS kind, and WARN level. This aligns with standard HTTP semantics and the existing error handling patterns in the codebase.

pida-core/core-domain/src/main/kotlin/com/pida/support/aws/S3ImageUrl.kt (1)

1-6: LGTM!

Clean DTO design that appropriately separates the presigned upload URL from the final image URL. This separation is useful since clients need the presigned URL for upload but may want to store or display the permanent image URL.

pida-core/core-domain/src/main/kotlin/com/pida/support/error/ErrorKind.kt (1)

12-12: LGTM!

The new TOO_MANY_REQUESTS enum constant correctly represents HTTP 429 rate limiting scenarios and is properly referenced by EXCEED_RATE_LIMIT in ErrorType.

pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsProperties.kt (1)

13-16: Verify secrets are not committed to version control.

The credentials are correctly modeled for Spring Boot configuration binding. Ensure that actual accessKey and secretKey values are provided via environment variables, secrets manager, or profile-specific configuration that is excluded from version control (e.g., via .gitignore).

pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImagePrefix.kt (1)

1-7: LGTM!

Clean enum design that provides type safety for S3 path prefixes. The lowercase value is appropriate for S3 key naming conventions.

pida-core/core-domain/build.gradle.kts (1)

17-19: LGTM — Bucket4j is a solid choice for rate limiting.

Bucket4j provides a robust token bucket implementation. The PR description mentions potential future migration to Redis for distributed rate limiting, which Bucket4j supports natively via its Redis integrations (Lettuce, Jedis, or Redisson with ProxyManager).

pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt (1)

3-9: LGTM!

The interface is clean, focused, and provides a clear contract for S3 image upload URL generation.

pida-core/core-domain/src/main/kotlin/com/pida/blooming/BloomingImageUploadUrl.kt (1)

5-16: LGTM!

The data class and factory method are well-structured, providing clear mapping from S3-specific types to domain types.

gradle/libs.versions.toml (1)

43-43: Verify Netty transitive dependencies for AWS SDK 2.40.13; confirm Bucket4j 8.15.0 stability.

AWS SDK 2.40.13 (released Dec 20, 2025) may transitively depend on Netty versions vulnerable to CVE-2025-24970 (affects netty-handler 4.1.91–4.1.117; patched in 4.1.118+) and CVE-2023-44487 (affects Netty < 4.1.94). Verify the resolved io.netty version in your dependency tree (./gradlew dependencies --configuration runtimeClasspath | grep netty) and ensure it is >= 4.1.118.Final; if not, add an explicit Netty override or upgrade to a later AWS SDK release that includes the patch.

Bucket4j 8.15.0 is the latest stable version with no direct published CVEs. Run a software composition analysis (SCA) scan on your final artifact to check transitive dependencies.

pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt (1)

80-109: LGTM: Pagination logic correctly handles large object lists.

The pagination implementation properly accumulates all objects across continuation tokens and builds a complete response.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt (2)

59-83: Critical path inconsistency and hard-coded values already flagged in previous review.

This method has two issues that were identified in the previous review:

  1. Critical: Line 69 omits the profile prefix ("$filePath/$fileName"), while generateUrl and getObjectAsBytes include it ("$profile/$filePath/$fileName"). This will break the upload/download flow.
  2. Major: Lines 70-71 hard-code "image/jpeg" and PUBLIC_READ ACL, limiting flexibility for multi-format support.

Please address these issues as outlined in the previous review comments.


116-129: Using platform default charset for binary data is risky.

Line 127 uses Charset.defaultCharset() to convert S3 object bytes to a String. This is problematic because:

  • The default charset is platform-dependent and can vary between environments
  • For binary data (such as images), charset conversion doesn't make semantic sense

Consider refactoring based on the intended use case:

  • If returning binary data: Change return type to ByteArray and remove charset conversion
  • If returning text data: Explicitly use StandardCharsets.UTF_8 instead of the platform default
🔎 Proposed fixes

Option 1: For binary data (recommended for images)

 fun getObjectAsBytes(
     bucketName: String,
     filePath: String,
     fileName: String,
-): String {
+): ByteArray {
     val request =
         GetObjectRequest
             .builder()
             .bucket(bucketName)
             .key("$profile/$filePath/$fileName")
             .build()
-    return s3Client.getObjectAsBytes(request).asString(Charset.defaultCharset())
+    return s3Client.getObjectAsBytes(request).asByteArray()
 }

Option 2: For text data with explicit charset

+import java.nio.charset.StandardCharsets
+
 fun getObjectAsBytes(
     bucketName: String,
     filePath: String,
     fileName: String,
 ): String {
     val request =
         GetObjectRequest
             .builder()
             .bucket(bucketName)
             .key("$profile/$filePath/$fileName")
             .build()
-    return s3Client.getObjectAsBytes(request).asString(Charset.defaultCharset())
+    return s3Client.getObjectAsBytes(request).asString(StandardCharsets.UTF_8)
 }
🧹 Nitpick comments (2)
pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt (1)

14-19: Good fix for the memory leak concern!

The Caffeine cache with expireAfterAccess and maximumSize successfully addresses the unbounded bucket map growth flagged in the previous review. The configuration parameters (10 minutes TTL, 100k max size) are reasonable defaults.

💡 Optional: Externalize cache configuration

Consider externalizing the TTL and maximum size to application properties for operational flexibility:

@ConfigurationProperties(prefix = "pida.rate-limit.cache")
data class RateLimitCacheProperties(
    val expireAfterAccessMinutes: Long = 10,
    val maximumSize: Long = 100_000
)

@Component
class PresignedUrlRateLimiter(
    private val cacheProperties: RateLimitCacheProperties
) {
    private val buckets: Cache<Long, Bucket> =
        Caffeine
            .newBuilder()
            .expireAfterAccess(cacheProperties.expireAfterAccessMinutes, TimeUnit.MINUTES)
            .maximumSize(cacheProperties.maximumSize)
            .build()
    // ...
}
gradle/libs.versions.toml (1)

62-66: Versions are current and secure; consider SCA scanning for Bucket4j transitive dependencies.

Bucket4j 8.15.0 (released Aug 24, 2025) and Caffeine 3.2.3 (released Oct 28, 2025) are both current stable releases with no direct CVEs. However, run an SCA scan on transitive dependencies for the specific Bucket4j modules used (e.g., backend connectors) to catch any inherited vulnerabilities.

Also applies to: 195-199

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 753b386 and 730da99.

📒 Files selected for processing (4)
  • gradle/libs.versions.toml
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt
  • pida-core/core-domain/build.gradle.kts
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt
🧰 Additional context used
🧬 Code graph analysis (1)
pida-core/core-domain/build.gradle.kts (1)
build.gradle.kts (3)
  • implementation (43-56)
  • libs (23-83)
  • kotlin (58-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI (21, 2.1.0, corretto)
🔇 Additional comments (6)
pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt (3)

17-24: LGTM: Environment variable reading is appropriate for profile-based prefixing.

The companion object reads SPRING_PROFILES_ACTIVE once at class load time, which is appropriate since Spring profiles are typically set at application startup and don't change at runtime.


26-57: LGTM: Download URL generation is correctly implemented.

The method properly includes the profile prefix in the S3 key and uses appropriate content-type for binary downloads.


85-114: Verify the intentional separation of profile prefix between read and write/list operations.

The inconsistency exists but may be intentional. Analysis shows a pattern:

  • Read operations (generateUrl, getObjectAsBytes): include "$profile/$filePath/$fileName"
  • Write/List operations (generateUploadUrl, getBucketListObjects): use filePath directly without profile

Since generateUploadUrl (which is actively used) also omits the profile prefix, this suggests the design is intentional—profile prefixes organize existing files for reads, while write/list operations construct paths independently. Confirm whether this separation is the intended behavior.

pida-core/core-domain/build.gradle.kts (1)

18-22: LGTM!

The Bucket4j and Caffeine dependencies are correctly added with the appropriate implementation scope and align with the version catalog declarations.

pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt (1)

21-35: LGTM!

The consumeToken method is well-implemented with lazy bucket initialization, proper token consumption logic, and appropriate error handling. The separation of the rate limit policy (PresignedUrlRateLimitPolicy.PER_MINUTE) follows good design principles.

gradle/libs.versions.toml (1)

43-43: AWS SDK for Java version 2.40.13 is a valid release (published December 20, 2025) with no documented security vulnerabilities. The version upgrade is safe to proceed with.

Copy link
Member

@char-yb char-yb left a comment

Choose a reason for hiding this comment

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

궁금한 점 위주로 코멘트 달아보았습니다!
덕분에 bucket4j와 Caffeine 캐시에 대해서 학습할 수 있었어요:)

추후 Caffeine과 bucket4j 활용도가 높아진다면 Configuration으로 전역 설정을 해볼 수도 있을 거 같네요

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt (1)

36-49: Add comment documenting the intentional omission of image URLs in the list endpoint.

The findAllFlowerSpot method does not fetch imageUrls while readFlowerSpotDetails does. This appears intentional for performance reasons (list views typically don't require S3 URL fetching for each spot), but it should be documented with a comment explaining this design choice to prevent future maintainers from inadvertently changing the behavior.

♻️ Duplicate comments (3)
pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageFileConstructor.kt (2)

23-29: Path injection risk remains unaddressed.

The path construction still lacks validation for prefix and prefixId, allowing potential path traversal attacks via characters like ../ or /.


27-27: Unsafe profile fallback could mix environments.

The fallback to "dev" remains unchanged and could result in production data being written to the dev prefix in S3.

pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt (1)

28-34: 30-second TTL may be too restrictive for uploads.

The presigned upload URL expires in 30 seconds, which may not provide sufficient time for users on slower connections or uploading larger images.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 730da99 and a115c1c.

📒 Files selected for processing (8)
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageFileConstructor.kt
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt
  • pida-core/core-api/src/main/kotlin/com/pida/presentation/v1/flowerspot/response/FlowerSpotDetailsResponse.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotDetails.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt
  • pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/s3/AwsS3Client.kt
🧰 Additional context used
🧬 Code graph analysis (1)
pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotDetails.kt (2)
pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpot.kt (1)
  • id (5-16)
pida-storage/db-core/src/main/kotlin/com/pida/storage/db/core/flowerspot/FlowerSpotEntity.kt (1)
  • name (16-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CI (21, 2.1.0, corretto)
🔇 Additional comments (7)
pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotDetails.kt (1)

19-40: LGTM! Clean addition of imageUrls support.

The imageUrls property is properly added with sensible defaults and correctly propagated through the factory method.

pida-core/core-api/src/main/kotlin/com/pida/presentation/v1/flowerspot/response/FlowerSpotDetailsResponse.kt (1)

55-79: LGTM! Proper API response integration.

The imageUrls field is correctly added with appropriate schema documentation and properly mapped from the domain model.

pida-core/core-domain/src/main/kotlin/com/pida/support/aws/ImageS3Caller.kt (1)

1-25: LGTM! Clean interface design.

The interface clearly defines S3 image operations with appropriate method signatures. The mix of synchronous (createUploadUrl) and suspending (getImageUrl) functions aligns with their respective use cases.

pida-core/core-domain/src/main/kotlin/com/pida/flowerspot/FlowerSpotFacade.kt (1)

14-34: LGTM! Proper concurrent image URL fetching.

The imageS3Caller integration correctly uses async to fetch image URLs concurrently with other data, and properly passes the results to the factory method.

pida-core/core-domain/src/main/kotlin/com/pida/support/aws/PresignedUrlRateLimiter.kt (1)

14-39: LGTM! Robust rate limiting implementation.

The Caffeine-based cache with TTL and size limits properly addresses the memory leak concerns from previous reviews. The Bucket4j integration is correct, and the implementation appropriately throws an exception when rate limits are exceeded.

pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/image/ImageS3Processor.kt (2)

42-53: LGTM! Clean conditional logic.

The method correctly handles both single-file retrieval and directory listing cases with appropriate helper delegation.


67-82: LGTM! Proper S3 object listing.

The method correctly filters out directory markers and extracts filenames before generating presigned URLs.

Copy link
Member

@char-yb char-yb left a comment

Choose a reason for hiding this comment

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

레퍼런스와 답변 감사해용
imageUrls도 emptyList 처리된 것도 확인했습니다!
LGTM!

@LeeHanEum LeeHanEum merged commit 914716e into develop Dec 26, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ AWS s3 이미지 업로드 모듈 개발

3 participants