Skip to content

Conversation

@Gwonwoo-Nam
Copy link
Contributor

@Gwonwoo-Nam Gwonwoo-Nam commented Dec 31, 2025

🟢 구현내용

🧩 고민과 해결과정

Summary by CodeRabbit

  • New Features

    • Images are processed into three sizes (thumbnail, medium, large) and two formats (WebP, JPEG); six variants are uploaded and persisted.
    • API responses now include structured URLs for each size/format so clients can select optimized images.
    • Image processing sizes and quality are configurable via application settings.
    • Thumbnail selection now prefers WebP when available; deletions remove all size/format variants.
  • Tests

    • Updated tests and docs to cover multi-variant responses, upload, and delete flows.

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

@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

Adds configurable image processing and multi-variant storage: images are resized (thumbnail, medium, large) and encoded as WebP and JPEG, uploaded to S3 as six variants, and persisted as a JSON imageUrls field; DTOs, service flows, tests, and dependencies were updated accordingly.

Changes

Cohort / File(s) Summary
Build & Config
build.gradle, src/main/resources/application.yml, src/main/kotlin/upbrella/be/config/ImageProcessingConfig.kt
Added net.coobird:thumbnailator and org.sejda.imageio:webp-imageio dependencies; introduced image.processing config binding (sizes and quality defaults).
DTOs / Responses
src/main/kotlin/upbrella/be/store/dto/response/ImageUrlsResponse.kt, src/main/kotlin/upbrella/be/store/dto/response/SingleImageUrlResponse.kt
Added ImageUrlsResponse and ImageSizeUrls; SingleImageUrlResponse extended with imageUrls and companion factory updated to populate it.
Entity
src/main/kotlin/upbrella/be/store/entity/StoreImage.kt
Added imageUrls: String? JSON column, Jackson (de)serialization helper, createStoreImageWithUrls() factory, and getImageUrlsResponse() accessor with fallback to legacy single imageUrl.
Image Processing Service
src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt
New service producing three sizes and two formats per size; exposes ProcessedImageSet, ImageVariantData, and ImageSize enum; includes conversion and resize logic.
Store Image Flow
src/main/kotlin/upbrella/be/store/service/StoreImageService.kt
Integrates ImageProcessingService; uploads six variants to S3 (uploadAllVariantsToS3/uploadToS3), persists imageUrls, updates delete logic to remove multi-variant keys, and returns medium JPEG URL for compatibility. Constructor updated for new dependency and bucketName.
Tests
src/test/kotlin/upbrella/be/store/controller/StoreControllerTest.kt, src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt
Tests updated to mock ImageProcessingService, assert 6 uploads / 3 deletions, validate new imageUrls structure and thumbnail selection (prefers WebP).

Sequence Diagram

sequenceDiagram
    actor Client
    participant Controller
    participant StoreImageService
    participant ImageProcessingService
    participant S3
    participant Database

    Client->>Controller: POST /upload (file)
    Controller->>StoreImageService: uploadFile(file)

    rect rgb(230,245,255)
      note right of StoreImageService: Orchestrate processing
      StoreImageService->>ImageProcessingService: processImage(file)
      ImageProcessingService->>ImageProcessingService: validate, resize to 3 sizes
      ImageProcessingService->>ImageProcessingService: convert each size to WebP & JPEG
      ImageProcessingService-->>StoreImageService: ProcessedImageSet (6 byte arrays)
    end

    rect rgb(245,230,255)
      note right of StoreImageService: Upload variants to S3
      loop for each variant (6)
        StoreImageService->>S3: putObject(key, bytes, contentType)
        S3-->>StoreImageService: public URL
      end
      StoreImageService->>StoreImageService: build ImageUrlsResponse
    end

    rect rgb(230,255,245)
      note right of StoreImageService: Persist metadata
      StoreImageService->>Database: save StoreImage (imageUrls JSON)
      Database-->>StoreImageService: confirmation
    end

    StoreImageService-->>Controller: return medium JPEG URL (compat)
    Controller-->>Client: 200 OK (response includes imageUrls)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #490: Modifies StoreImageService — related due to overlapping service-layer changes and constructor/dependency edits.
  • PR #462: Modifies StoreImage entity — related because this PR further changes the same entity by adding imageUrls and serialization.

Suggested reviewers

  • birdieHyun

"I'm a rabbit with a tiny brush,
I paint thumbnails in a gentle rush,
WebP and JPEG in sizes three,
off to S3 each URL for me,
Hopping bytes to metadata — whee!" 🐇🎨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'feat: optimize image' is vague and does not clearly convey the specific changes made, which involve multi-format image processing, variant generation, and OOM optimization. Use a more specific title that reflects the main changes, such as 'feat: implement multi-variant image processing with WebP/JPEG conversion' or 'feat: optimize image storage with format variants and OOM handling'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

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: 9

🧹 Nitpick comments (3)
src/main/kotlin/upbrella/be/store/entity/StoreImage.kt (2)

24-24: Consider extracting ObjectMapper to avoid static state in entity.

While functional, having a static ObjectMapper in the entity couples JSON serialization logic to the domain model. Consider injecting ObjectMapper via a utility class or service.

Alternative approach

Create a utility class for JSON operations:

@Component
class JsonUtil(private val objectMapper: ObjectMapper) {
    fun <T> writeValueAsString(value: T): String = objectMapper.writeValueAsString(value)
    fun <T> readValue(content: String, valueType: Class<T>): T = objectMapper.readValue(content, valueType)
}

Then use it in a service layer rather than directly in the entity.


16-17: Consider database-appropriate column type.

The TEXT column type may have different behaviors across databases. For MySQL, consider using LONGTEXT if URLs can exceed 65,535 bytes, or specify a VARCHAR with an explicit length if the JSON structure size is predictable.

src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt (1)

16-31: Unused code: ImageSize.from() is never invoked and may drift out of sync.

The from(configValue: Int) companion function hardcodes the same size values defined in the enum. Since this function isn't used anywhere and the config values are external, consider removing it to avoid maintenance burden.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42f63db and cc26b78.

📒 Files selected for processing (10)
  • build.gradle
  • src/main/kotlin/upbrella/be/config/ImageProcessingConfig.kt
  • src/main/kotlin/upbrella/be/store/dto/response/ImageUrlsResponse.kt
  • src/main/kotlin/upbrella/be/store/dto/response/SingleImageUrlResponse.kt
  • src/main/kotlin/upbrella/be/store/entity/StoreImage.kt
  • src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt
  • src/main/kotlin/upbrella/be/store/service/StoreImageService.kt
  • src/main/resources/application.yml
  • src/test/kotlin/upbrella/be/store/controller/StoreControllerTest.kt
  • src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt
🧰 Additional context used
🪛 detekt (1.23.8)
src/main/kotlin/upbrella/be/store/service/StoreImageService.kt

[warning] 130-130: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

⏰ 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: test
🔇 Additional comments (16)
src/main/resources/application.yml (1)

5-13: LGTM!

The image processing configuration is well-structured with reasonable defaults for sizes and quality settings. The values align with the configuration binding in ImageProcessingConfig.

src/main/kotlin/upbrella/be/store/dto/response/ImageUrlsResponse.kt (1)

1-13: LGTM!

The data classes are well-structured for representing multi-format, multi-size image URLs. The nullable webp field appropriately supports backward compatibility with existing images that may only have JPEG variants.

src/test/kotlin/upbrella/be/store/controller/StoreControllerTest.kt (2)

988-1002: LGTM!

Test data properly reflects the new multi-format, multi-size image URL structure with both WebP and JPEG variants across all three sizes.


1033-1054: LGTM!

REST documentation is comprehensive and properly documents the new nested imageUrls structure with all format and size variants. The optional markers for WebP fields correctly reflect backward compatibility.

src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt (3)

52-53: LGTM!

The ImageProcessingService mock is properly integrated into the test setup to support the new multi-variant image processing flow.


68-94: LGTM!

The test properly mocks the image processing service and verifies that all six image variants (3 sizes × 2 formats) are uploaded to S3. The assertion on the expected URL correctly validates backward compatibility by returning the medium JPEG URL.


146-190: LGTM!

Test data properly includes the complete imageUrls structure with both WebP and JPEG variants, and the test correctly validates that WebP thumbnail is preferred when available.

src/main/kotlin/upbrella/be/store/dto/response/SingleImageUrlResponse.kt (2)

5-9: LGTM!

The response structure properly extends the existing API with the new imageUrls field while maintaining backward compatibility through the preserved imageUrl field. The Korean comments clearly document the intent.


11-18: LGTM!

The factory method cleanly delegates to StoreImage.getImageUrlsResponse() to construct the complete response with both legacy and new image URL structures.

src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt (2)

36-46: LGTM!

The image processing flow is well-structured, clearly processing three sizes and returning a cohesive result set.


143-164: Good practice: Proper equals/hashCode implementation for ByteArray fields.

Kotlin data classes don't handle array equality correctly by default. This override using contentEquals and contentHashCode is the correct approach.

src/main/kotlin/upbrella/be/store/service/StoreImageService.kt (5)

32-48: LGTM!

The upload flow is well-structured: process → upload → persist → return compatible URL. Maintaining backward compatibility with the medium JPEG return value is a good approach.


50-71: LGTM!

The null-safe handling for optional WebP variants and the order of operations (S3 deletions before DB) are appropriate.


97-124: Good rollback pattern for partial upload failures.

Tracking uploaded keys and cleaning them up on failure is a solid resilience pattern for multi-file uploads.


73-78: LGTM!

WebP-first with JPEG fallback is the right approach for modern browser compatibility.


170-173: The StoreImage.createStoreImageWithUrls factory method exists and correctly handles ImageUrlsResponse by extracting the medium JPEG URL for backward compatibility while serializing the complete response object to JSON for storage.

Comment on lines +96 to +99

// Image Processing
implementation 'net.coobird:thumbnailator:0.4.20'
implementation 'org.sejda.imageio:webp-imageio:0.1.6'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Checking Thumbnailator Latest Version ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:net.coobird+AND+a:thumbnailator&rows=1&wt=json" | jq -r '.response.docs[0] | "\(.latestVersion) - Released: \(.timestamp)"'

echo -e "\n=== Checking webp-imageio Latest Version ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:org.sejda.imageio+AND+a:webp-imageio&rows=1&wt=json" | jq -r '.response.docs[0] | "\(.latestVersion) - Released: \(.timestamp)"'

echo -e "\n=== Checking Thumbnailator Version History ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:net.coobird+AND+a:thumbnailator&rows=20&wt=json" | jq -r '.response.docs[] | "v\(.v) - \(.timestamp)"' | head -10

echo -e "\n=== Checking webp-imageio Version History ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:org.sejda.imageio+AND+a:webp-imageio&rows=20&wt=json" | jq -r '.response.docs[] | "v\(.v) - \(.timestamp)"' | head -10

Repository: UPbrella/UPbrella_back

Length of output: 361


🌐 Web query:

Thumbnailator security vulnerabilities CVE 2024 2025

💡 Result:

Short answer: I found CVEs affecting "thumbnail" software in 2024–2025 (notably Matrix Media Repo and several WordPress thumbnail plugins), but I found no CVE entries for the Java Thumbnailator library (coobird/thumbnailator).

Notable CVEs (2024–2025)

  • CVE-2024-56515 — Matrix Media Repo (thumbnailers could invoke ImageMagick/ffmpeg decoders; fixed in MMR v1.3.8). [1]
  • CVE-2025-53282 — Stored XSS in the "Thumbnail Editor" WordPress plugin. [2]
  • CVE-2025-12154 — Arbitrary file upload in the "Auto Thumbnailer" WordPress plugin (authenticated). [3]

No CVE for Java Thumbnailator

  • The official Thumbnailator GitHub repository (coobird/thumbnailator) and its security/advisory records show no published CVE/security advisory for the library as of now. [4]

Sources

  1. CVE-2024-56515 details (Recorded Future / NVD references). [1]
  2. CVE-2025-53282 advisory summary (Wiz / Patchstack reporting). [2]
  3. CVE-2025-12154 (NVD / CVE Details summary). [3]
  4. Thumbnailator (coobird/thumbnailator) GitHub repository — no CVE listed in repo advisories. [4]

If you want, I can:

  • search NVD/GHSA/NPM/Maven Central again and return exact NVD/GHSA links and publish dates, or
  • monitor for new CVEs for Thumbnailator and notify you. Which would you prefer?

🌐 Web query:

webp-imageio org.sejda.imageio security advisories vulnerabilities

💡 Result:

Summary — webp-imageio (org.sejda.imageio)

  • I found no public CVE or vendor security advisory that names org.sejda.imageio/webp-imageio specifically. [1][2]
  • The project’s GitHub issues show a few crash/segfault and libwebp-related issues (including an open issue mentioning updating libwebp for CVE-2023-1999), so some security-relevant dependency concerns have been raised upstream. Check the repo issues for details. [3]
  • Maven Central metadata lists webp-imageio 0.1.6 (May 2020) and shows dependency-vulnerability metadata (e.g., CVE-2020-15250 appears in the package page), indicating you should also audit its dependencies. [2]

Relevant sources

  1. Maven / package page for org.sejda.imageio:webp-imageio (artifact, version + vulnerability metadata). [2]
  2. N/A for org.sejda.imageio-specific CVE — but ImageIO-related CVEs (Java ImageIO component) that are relevant to image-processing libraries: CVE-2020-14562, CVE-2022-21277, CVE-2021-35586 (examples from NVD/Rapid7/Wiz). These are general ImageIO CVEs to be aware of. [4][5][6]
  3. sejda-pdf/webp-imageio GitHub issues (open issues include crashes and “Update libwebp CVE-2023-1999”). Monitor this repo for fixes. [3]

If you want, I can:

  • Run a live dependency scan (OSS advisories/CVE lookup) against a specific webp-imageio version (e.g., 0.1.6) and list any dependency CVEs.
  • Watch the GitHub repo and notify you of new security issues.

Address unmaintained webp-imageio dependency.

Thumbnailator 0.4.20 is the latest version (released June 2023) with no known security advisories. However, webp-imageio 0.1.6 has not been updated since May 2020 and appears unmaintained. Its underlying libwebp dependency is outdated and has known vulnerabilities (e.g., CVE-2023-1999). Consider evaluating alternative WebP processing libraries or submitting upstream patches to the sejda project to address the stale dependencies.

🤖 Prompt for AI Agents
In build.gradle around lines 96–99 the project pulls in the unmaintained
org.sejda.imageio:webp-imageio:0.1.6 which relies on an outdated libwebp with
known vulnerabilities; remove this dependency and replace it with a maintained
WebP solution (either a maintained ImageIO WebP plugin or a native library
wrapper) by: (1) researching and selecting a maintained WebP processing library
or using a native toolchain (e.g., a maintained imageio-webp fork or a JNI
binding to libwebp), (2) updating build.gradle to remove the sejda:webp-imageio
entry and add the chosen dependency, (3) adapting image loading/encoding call
sites to the new API and adding tests, and if you must keep sejda temporarily
open an issue/PR against their repo and add a dependency exclusion/pinning and a
comment documenting the risk until a fix is merged.

Comment on lines +7 to +13
@Configuration
@ConfigurationProperties(prefix = "image.processing")
@ConstructorBinding
data class ImageProcessingConfig(
val sizes: ImageSizes = ImageSizes(),
val quality: ImageQuality = ImageQuality()
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect usage of @ConstructorBinding with @configuration.

In Spring Boot 2.7+, @ConstructorBinding should not be combined with @Configuration. This annotation combination can prevent proper configuration binding.

🔎 Recommended fix

Option 1 (Preferred): Remove @configuration and enable via @EnableConfigurationProperties

Remove @Configuration from the data class and enable it elsewhere (e.g., in a main configuration class or application class):

-@Configuration
 @ConfigurationProperties(prefix = "image.processing")
 @ConstructorBinding
 data class ImageProcessingConfig(
     val sizes: ImageSizes = ImageSizes(),
     val quality: ImageQuality = ImageQuality()
 )

Then add @EnableConfigurationProperties(ImageProcessingConfig::class) to your main application class or another configuration class.

Option 2: Remove @ConstructorBinding and keep @configuration

 @Configuration
 @ConfigurationProperties(prefix = "image.processing")
-@ConstructorBinding
 data class ImageProcessingConfig(
     val sizes: ImageSizes = ImageSizes(),
     val quality: ImageQuality = ImageQuality()
 )

This allows Spring to use setter-based binding instead of constructor binding.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Configuration
@ConfigurationProperties(prefix = "image.processing")
@ConstructorBinding
data class ImageProcessingConfig(
val sizes: ImageSizes = ImageSizes(),
val quality: ImageQuality = ImageQuality()
)
@Configuration
@ConfigurationProperties(prefix = "image.processing")
data class ImageProcessingConfig(
val sizes: ImageSizes = ImageSizes(),
val quality: ImageQuality = ImageQuality()
)
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/config/ImageProcessingConfig.kt around lines 7 to
13, the data class is annotated with @ConstructorBinding and @Configuration
which is incorrect for Spring Boot 2.7+; remove the @Configuration annotation
from this data class and register the properties class via
@EnableConfigurationProperties(ImageProcessingConfig::class) on your main
application or a configuration class (alternatively, if you prefer to keep
@Configuration, remove @ConstructorBinding to use setter-based binding),
ensuring ImageProcessingConfig remains a pure @ConfigurationProperties data
class.

Comment on lines +50 to 66
fun getImageUrlsResponse(): ImageUrlsResponse {
return if (imageUrls != null) {
// 새 포맷: JSON 파싱
objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
} else {
// 기존 이미지: 단일 URL을 모든 크기에 사용
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for JSON parsing.

The getImageUrlsResponse() method performs JSON parsing without error handling. If the stored JSON is malformed or incompatible, this will throw unchecked exceptions during entity access, potentially breaking application flows.

🔎 Recommended fix
 fun getImageUrlsResponse(): ImageUrlsResponse {
     return if (imageUrls != null) {
-        // 새 포맷: JSON 파싱
-        objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
+        try {
+            // 새 포맷: JSON 파싱
+            objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
+        } catch (e: Exception) {
+            // JSON 파싱 실패 시 fallback
+            ImageUrlsResponse(
+                id = id,
+                webp = null,
+                jpeg = ImageSizeUrls(
+                    thumb = imageUrl ?: "",
+                    medium = imageUrl ?: "",
+                    large = imageUrl ?: ""
+                )
+            )
+        }
     } else {
         // 기존 이미지: 단일 URL을 모든 크기에 사용
         ImageUrlsResponse(
             id = id,
             webp = null,
             jpeg = ImageSizeUrls(
                 thumb = imageUrl ?: "",
                 medium = imageUrl ?: "",
                 large = imageUrl ?: ""
             )
         )
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun getImageUrlsResponse(): ImageUrlsResponse {
return if (imageUrls != null) {
// 새 포맷: JSON 파싱
objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
} else {
// 기존 이미지: 단일 URL을 모든 크기에 사용
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
}
fun getImageUrlsResponse(): ImageUrlsResponse {
return if (imageUrls != null) {
try {
// 새 포맷: JSON 파싱
objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
} catch (e: Exception) {
// JSON 파싱 실패 시 fallback
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
} else {
// 기존 이미지: 단일 URL을 모든 크기에 사용
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
}
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/store/entity/StoreImage.kt around lines 50 to 66,
the JSON parsing of imageUrls is performed without error handling; wrap the
objectMapper.readValue call in a try-catch (catch
JsonProcessingException/IOException or Exception), log the parse error with
context (id and the raw imageUrls) using the class logger, and on error return
the same legacy fallback ImageUrlsResponse (using imageUrl for all sizes) so
malformed JSON does not throw and entity access continues.

Comment on lines +125 to +135
} catch (e: Exception) {
// 업로드 실패 시 이미 업로드된 파일들 삭제 (rollback)
uploadedKeys.forEach { url ->
try {
deleteFileInS3(url)
} catch (deleteError: Exception) {
// 로그만 남기고 계속 진행
}
}
throw e
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Swallowed exception during rollback cleanup.

The comment states "로그만 남기고 계속 진행" (only log and continue), but no logging is implemented. Silent failures make debugging difficult when partial cleanups fail.

🔎 Proposed fix
         } catch (deleteError: Exception) {
-            // 로그만 남기고 계속 진행
+            // 로그만 남기고 계속 진행
+            logger.warn("Failed to rollback uploaded file: $url", deleteError)
         }

Add a logger to the class:

companion object {
    private val logger = LoggerFactory.getLogger(StoreImageService::class.java)
}

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 130-130: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/store/service/StoreImageService.kt around lines
125 to 135, the rollback catch for deleteFileInS3 swallows exceptions without
logging; add a logger to the class (companion object with
LoggerFactory.getLogger(StoreImageService::class.java>) and update the inner
catch to call logger.error(...) including the deleteError and a clear message
about which uploadedKeys/url failed to delete, then continue as before.

Comment on lines 175 to 182
private fun parseKey(url: String): String {
val splitUrl = url.split("/")
return splitUrl[splitUrl.size - 2] + "/" + splitUrl[splitUrl.size - 1]
return if (splitUrl.size >= 2) {
splitUrl[splitUrl.size - 3] + "/" + splitUrl[splitUrl.size - 2] + "/" + splitUrl[splitUrl.size - 1]
} else {
splitUrl[splitUrl.size - 2] + "/" + splitUrl[splitUrl.size - 1]
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Incorrect bounds check causes ArrayIndexOutOfBoundsException.

When splitUrl.size is 2 or 3, accessing splitUrl[splitUrl.size - 3] throws an exception. The condition should require at least 4 elements to safely access the last 3 segments. The else branch also fails when size < 2.

🔎 Proposed fix
 private fun parseKey(url: String): String {
     val splitUrl = url.split("/")
-    return if (splitUrl.size >= 2) {
+    return if (splitUrl.size >= 4) {
         splitUrl[splitUrl.size - 3] + "/" + splitUrl[splitUrl.size - 2] + "/" + splitUrl[splitUrl.size - 1]
-    } else {
+    } else if (splitUrl.size >= 2) {
         splitUrl[splitUrl.size - 2] + "/" + splitUrl[splitUrl.size - 1]
+    } else {
+        splitUrl.lastOrNull() ?: throw IllegalArgumentException("Invalid URL: $url")
     }
 }
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/store/service/StoreImageService.kt around lines
175-182, the current bounds check is incorrect and can index out of range;
change the logic to: if splitUrl.size >= 3, return the last three segments
joined with "/", else if splitUrl.size >= 2, return the last two segments joined
with "/", otherwise return the original url (or splitUrl[0]) to avoid any
ArrayIndexOutOfBoundsException. Ensure you use size-1, size-2, size-3 only when
those indices exist and handle size < 2 safely.

{ assertEquals(testUrl, testImage.imageUrl) },
{ verify(storeImageWriter, times(1)).deleteById(testImage.id!!) },
{ verify(s3Client, times(1)).deleteObject(any(DeleteObjectRequest::class.java)) },
{ verify(s3Client, times(3)).deleteObject(any(DeleteObjectRequest::class.java)) }, // 3 sizes x 1 format (JPEG only, no WebP for old images)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# First, let's find the StoreImageService file
echo "=== Finding StoreImageService file ==="
fd -t f 'StoreImageService\.kt' | grep -v Test

# Then check the deletion logic
echo -e "\n=== Checking deleteFile method implementation ==="
rg -n -A 20 'fun deleteFile' --type=kt -g '!Test'

# Look for deleteObject calls to understand deletion patterns
echo -e "\n=== Looking for S3 deleteObject calls in implementation ==="
rg -n -B 3 -A 3 'deleteObject' --type=kt -g '!Test' -g 'StoreImageService'

# Also check the test file for both old and new image scenarios
echo -e "\n=== Checking test file for image format scenarios ==="
rg -n -B 5 -A 5 'verify.*deleteObject.*times' --type=kt -g 'StoreImageServiceTest'

Repository: UPbrella/UPbrella_back

Length of output: 432


🏁 Script executed:

#!/bin/bash

# Let's also check the entire test file structure to understand what scenarios are tested
echo "=== Examining StoreImageServiceTest structure ==="
cat -n src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt | head -150

# Look for test functions related to deletion
echo -e "\n=== Finding deletion-related test functions ==="
rg -n 'fun.*delete|fun.*remove' --type=kt -g 'StoreImageServiceTest'

Repository: UPbrella/UPbrella_back

Length of output: 6749


🏁 Script executed:

#!/bin/bash

# Read the StoreImageService implementation
echo "=== Reading StoreImageService.kt ==="
cat -n src/main/kotlin/upbrella/be/store/service/StoreImageService.kt

# Search for deleteFile in the service
echo -e "\n=== Searching for deleteFile method ==="
grep -n 'deleteFile\|deleteObject' src/main/kotlin/upbrella/be/store/service/StoreImageService.kt

Repository: UPbrella/UPbrella_back

Length of output: 8845


🏁 Script executed:

#!/bin/bash

# Check the entire test file for all test methods
echo "=== All test methods in StoreImageServiceTest ==="
grep -n '@Test\|fun.*Test\|fun.*test' src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt

# Look for any WebP or format-related tests
echo -e "\n=== Looking for WebP/format-related content in tests ==="
grep -n -i 'webp\|new.*image\|old.*image\|format' src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt

Repository: UPbrella/UPbrella_back

Length of output: 1542


Add test case for new images with WebP deletion.

The implementation correctly handles both old and new image formats: old images (JPEG only) delete 3 files, while new images (JPEG + WebP) delete 6 files. However, the current test deleteFileTest() only covers the old image scenario. Add a test case that verifies 6 deleteObject calls when deleting new images with both JPEG and WebP variants.

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 (3)
src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt (1)

126-131: Add test case for new images with WebP deletion.

The current deleteFileTest() only verifies 3 deleteObject calls for old images (JPEG only). Add a test case that verifies 6 deleteObject calls when deleting new images that have both JPEG and WebP variants stored in imageUrls.

src/main/kotlin/upbrella/be/store/service/StoreImageService.kt (2)

128-136: Swallowed exception during rollback cleanup.

The comment states "로그만 남기고 계속 진행" but no logging is implemented. Silent failures make debugging partial cleanup failures difficult.


178-185: Bug: Incorrect bounds check causes ArrayIndexOutOfBoundsException.

When splitUrl.size is 2 or 3, accessing splitUrl[splitUrl.size - 3] throws an exception. The condition should require at least 4 elements to safely access the last 3 segments.

🧹 Nitpick comments (3)
src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt (1)

153-201: Add test case for JPEG fallback when WebP is null.

The createThumbnailTest only covers the happy path where WebP URLs exist. Consider adding a test case where webp is null to verify the fallback to jpeg.thumb works correctly.

🔎 Proposed additional test case
@Test
@DisplayName("WebP가 없는 경우 JPEG 썸네일을 반환한다.")
fun createThumbnailFallbackToJpegTest() {
    // given
    val imageWithoutWebp = SingleImageUrlResponse(
        id = 1L,
        imageUrl = "https://null.s3.ap-northeast-2.amazonaws.com/store-image/filename.jpg",
        imageUrls = ImageUrlsResponse(
            id = 1L,
            webp = null,
            jpeg = ImageSizeUrls(
                thumb = "https://null.s3.ap-northeast-2.amazonaws.com/store-image/filename-thumb.jpg",
                medium = "https://null.s3.ap-northeast-2.amazonaws.com/store-image/filename-medium.jpg",
                large = "https://null.s3.ap-northeast-2.amazonaws.com/store-image/filename-large.jpg"
            )
        )
    )
    val imageUrls = listOf(imageWithoutWebp)

    // when
    val thumbnail = storeImageService.createThumbnail(imageUrls)

    // then
    assertThat(thumbnail).isEqualTo(imageUrls[0].imageUrls.jpeg.thumb)
}
src/main/kotlin/upbrella/be/store/service/StoreImageService.kt (2)

144-155: Consider making the CDN domain configurable.

The domain https://file.upbrella.co.kr is hardcoded on line 154. If you need different domains for different environments (staging, production), extract this to a configuration property.

🔎 Proposed fix
 class StoreImageService(
     private val s3Client: S3Client,
     private val storeImageReader: StoreImageReader,
     private val storeImageWriter: StoreImageWriter,
     private val storeDetailReader: StoreDetailReader,
     private val imageProcessingService: ImageProcessingService,
     @Value("\${AWS_S3_BUCKET}")
     private val bucketName: String,
+    @Value("\${cdn.base-url:https://file.upbrella.co.kr}")
+    private val cdnBaseUrl: String,
 )
     s3Client.putObject(putObjectRequest, RequestBody.fromBytes(data))
-    return "https://file.upbrella.co.kr/$key"
+    return "$cdnBaseUrl/$key"

168-171: Remove unused method saveStoreImage.

This private method is never called anywhere in the codebase and has been replaced by saveStoreImageWithUrls.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc26b78 and 220a8b1.

📒 Files selected for processing (2)
  • src/main/kotlin/upbrella/be/store/service/StoreImageService.kt
  • src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt
⏰ 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: test
🔇 Additional comments (5)
src/test/kotlin/upbrella/be/store/service/StoreImageServiceTest.kt (2)

52-67: LGTM on manual service construction.

The manual @BeforeEach setup is appropriate here since it allows injecting the bucketName configuration value that would otherwise require @InjectMocks workarounds.


69-106: LGTM on upload test updates.

The test correctly mocks ProcessedImageSet with all variants and verifies 6 putObject calls for the multi-variant upload flow. The expected URL construction aligns with the service implementation.

src/main/kotlin/upbrella/be/store/service/StoreImageService.kt (3)

29-31: LGTM on externalized bucket name.

The bucket name is now properly injected via @Value, addressing the hardcoded bucket name issue. Note: Ensure AWS_S3_BUCKET is set in all deployment environments.


34-51: LGTM on the refactored upload flow.

The multi-variant upload pipeline is well-structured: process → upload all variants → persist → return medium JPEG URL for backward compatibility.


57-73: LGTM on multi-variant deletion logic.

The deletion correctly handles both old images (JPEG only, when webp is null) and new images (both WebP and JPEG variants). The null-safe ?.let handles backward compatibility gracefully.

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: 1

🧹 Nitpick comments (2)
src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt (2)

16-31: Consider removing ImageSize enum or relaxing the from() mapping.

The from(configValue: Int) function uses exact value matching, which creates brittle coupling between configuration and code. If configuration values change slightly (e.g., 305 instead of 300), the application will fail at runtime.

Additionally, since config.sizes already provides the target sizes and the enum's maxSize duplicates those values, the enum appears unnecessary.

🔎 Option 1: Remove the enum entirely (recommended)

The enum doesn't add value since configuration already provides the sizes directly.

-    enum class ImageSize(val maxSize: Int) {
-        THUMBNAIL(300),
-        MEDIUM(800),
-        LARGE(1200);
-
-        companion object {
-            fun from(configValue: Int): ImageSize {
-                return when (configValue) {
-                    300 -> THUMBNAIL
-                    800 -> MEDIUM
-                    1200 -> LARGE
-                    else -> throw IllegalArgumentException("Unknown image size: $configValue")
-                }
-            }
-        }
-    }
🔎 Option 2: Make from() use range-based matching

If the enum must be kept, use ranges instead of exact values:

 companion object {
     fun from(configValue: Int): ImageSize {
         return when {
-            configValue == 300 -> THUMBNAIL
-            configValue == 800 -> MEDIUM
-            configValue == 1200 -> LARGE
+            configValue <= 400 -> THUMBNAIL
+            configValue <= 900 -> MEDIUM
+            configValue <= 1300 -> LARGE
             else -> throw IllegalArgumentException("Unknown image size: $configValue")
         }
     }
 }

46-54: Consider making the downsampling threshold configurable.

The hardcoded 2000px threshold may not suit all deployment environments. Environments with limited heap might need a lower threshold, while those with more memory could handle larger images.

🔎 Proposed enhancement

Add a configuration property for the maximum working size:

In ImageProcessingConfig:

val maxWorkingSize: Int = 2000

Then use it here:

-                val workingImage = if (originalImage.width > 2000 || originalImage.height > 2000) {
+                val workingImage = if (originalImage.width > config.maxWorkingSize || 
+                                       originalImage.height > config.maxWorkingSize) {
                     Thumbnails.of(originalImage)
-                        .size(2000, 2000)
+                        .size(config.maxWorkingSize, config.maxWorkingSize)
                         .asBufferedImage()
                         .also { originalImage.flush() }
                 } else {
                     originalImage
                 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 220a8b1 and b8ab800.

📒 Files selected for processing (1)
  • src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt
⏰ 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: test
🔇 Additional comments (5)
src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt (5)

78-94: LGTM!

The function correctly handles image processing for each size with proper memory management. The try-finally ensures the resized image is flushed after format conversions.


99-103: LGTM!

Clean and correct implementation. Thumbnailator automatically maintains aspect ratio with the .size() method.


108-132: LGTM! Resource management concerns resolved.

The resource leaks flagged in the previous review have been properly addressed:

  • ByteArrayOutputStream and ImageOutputStream use .use {} for automatic closing
  • ImageWriter is disposed in a finally block
  • Compression type setting safely handles null with ?.firstOrNull() ?: "Lossy"

137-179: LGTM! Resource management concerns resolved.

The resource leaks flagged in the previous review have been properly addressed:

  • ByteArrayOutputStream and ImageOutputStream use .use {} for automatic closing
  • ImageWriter is disposed in a finally block
  • RGB conversion properly disposes Graphics
  • Converted RGB image is flushed when needed

The nested try-finally structure correctly ensures all resources are cleaned up.


186-216: LGTM!

The data classes are well-structured. ImageVariantData correctly overrides equals() and hashCode() using contentEquals() and contentHashCode() for proper ByteArray comparison.

Comment on lines +67 to +72
} catch (e: OutOfMemoryError) {
throw IllegalStateException("Image is too large to process. Please use a smaller image (max 10MB).", e)
}
} catch (e: OutOfMemoryError) {
throw IllegalStateException("Image is too large to process. Please use a smaller image (max 10MB).", e)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate OutOfMemoryError handling.

Both catch blocks (lines 67-69 and 70-72) perform identical error handling. Keep only the outer catch block to avoid duplication.

🔎 Proposed fix
             try {
                 // 이미지가 너무 크면 먼저 다운샘플링 (메모리 절약)
                 val workingImage = if (originalImage.width > 2000 || originalImage.height > 2000) {
                     // Thumbnailator는 자동으로 EXIF orientation 적용
                     Thumbnails.of(originalImage)
                         .size(2000, 2000)
                         .asBufferedImage()
                         .also { originalImage.flush() }
                 } else {
                     originalImage
                 }

-                try {
-                    // 3가지 크기로 리사이징 (순차 처리)
-                    val thumbnail = processSize(workingImage, config.sizes.thumbnail)
-                    val medium = processSize(workingImage, config.sizes.medium)
-                    val large = processSize(workingImage, config.sizes.large)
-
-                    return ProcessedImageSet(thumbnail, medium, large)
-                } finally {
-                    // 작업 이미지 메모리 해제
-                    workingImage.flush()
-                }
-            } catch (e: OutOfMemoryError) {
-                throw IllegalStateException("Image is too large to process. Please use a smaller image (max 10MB).", e)
-            }
+                // 3가지 크기로 리사이징 (순차 처리)
+                val thumbnail = processSize(workingImage, config.sizes.thumbnail)
+                val medium = processSize(workingImage, config.sizes.medium)
+                val large = processSize(workingImage, config.sizes.large)
+
+                return ProcessedImageSet(thumbnail, medium, large)
+            } finally {
+                // 작업 이미지 메모리 해제
+                workingImage.flush()
+            }
         } catch (e: OutOfMemoryError) {
             throw IllegalStateException("Image is too large to process. Please use a smaller image (max 10MB).", e)
         }
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt around
lines 67 to 72, there are two identical catch blocks for OutOfMemoryError;
remove the inner (duplicate) catch so only the outer catch remains, keeping the
single IllegalStateException wrapping with the same message and cause, and
ensure surrounding braces/indentation remain correct after deletion.

@Gwonwoo-Nam Gwonwoo-Nam merged commit 11b2d82 into dev Dec 31, 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.

2 participants