Conversation
Walkthrough메뉴 기능에 AWS S3 이미지 업로드를 추가합니다. S3Service와 파일 유틸리티를 도입하고 MenuController/MenuService/MenuRequest를 멀티파트 파일 처리로 변경하며, S3 관련 설정과 에러 코드를 추가합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant MenuController
participant MenuService
participant S3Service
participant S3
participant Database
Client->>MenuController: POST /api/admin/menus/{menu_id}/image (MultipartFile)
MenuController->>MenuService: uploadMenuImage(menuId, image)
MenuService->>S3Service: uploadFile(image, "menu/{cafeteria}")
S3Service->>S3Service: validateAndExtractFilename(image)
S3Service->>S3: putObject(s3Key, imageStream)
S3-->>S3Service: stored S3 URL
S3Service-->>MenuService: S3 URL
MenuService->>Database: updateMenuUrl(menuId, S3 URL)
opt 기존 이미지 존재
MenuService->>S3Service: deleteFile(oldImageUrl)
S3Service->>S3: deleteObject(oldS3Key)
S3-->>S3Service: delete success
end
Database-->>MenuService: updated Menu
MenuService-->>MenuController: MenuResponse
MenuController-->>Client: 200 OK (MenuResponse)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@build.gradle`:
- Around line 52-53: The project pulls
io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2 which brings Spring
Framework 6.2.7 with known CVEs; update your build to ensure Spring Framework is
at least 6.2.11 by either (A) switching to a fixed AWS starter/BOM that forces
newer Spring (e.g., import or update spring-cloud-aws-dependencies to a version
that resolves to Spring Framework 6.2.11+), or (B) explicitly override the
Spring Framework BOM/dependency to 6.2.11+ in your Gradle configuration so that
io.awspring.cloud:spring-cloud-aws-starter-s3 resolves against Spring Framework
>=6.2.11; ensure the chosen approach also addresses transitive CVE-2025-14763
from S3 encryption client.
In `@src/main/java/com/campustable/be/domain/menu/service/MenuService.java`:
- Around line 76-89: The code uses menuRepository.save(menu) inside
uploadMenuImage which may defer constraint violations until transaction commit
and prevent the S3 cleanup in the catch; replace menuRepository.save(menu) with
menuRepository.saveAndFlush(menu) so the persist is flushed immediately and any
constraint violations trigger the catch block allowing
s3Service.deleteFile(newUrl) cleanup to run; update the reference in the
try/catch around menuRepository.save(menu) (in uploadMenuImage) to call
saveAndFlush instead.
In `@src/main/java/com/campustable/be/domain/s3/service/S3Service.java`:
- Around line 54-76: The uploadFile method can let unchecked runtime exceptions
from s3Template.upload (e.g., network/credential issues) escape and break
ErrorCode mapping; add an additional catch for RuntimeException (after the
existing S3Exception and IOException catches) that logs the bucket, storedPath
and originalFilename along with e.getMessage(), and rethrow a CustomException
with the appropriate S3 error code (e.g.,
ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR or a new S3_UPLOAD_RUNTIME_ERROR) so all
runtime failures are mapped consistently for uploadFile / s3Template.upload.
🧹 Nitpick comments (4)
src/main/java/com/campustable/be/domain/s3/util/FileUtil.java (1)
9-10:isNullOrEmpty가 빈 파일명을 놓칠 수 있습니다.파일명이
""/공백인 경우도 empty로 간주하도록 보완하는 게 안전합니다.♻️ 제안 변경
- public boolean isNullOrEmpty(MultipartFile file){ - return file == null || file.isEmpty() || file.getOriginalFilename() == null; - } + public boolean isNullOrEmpty(MultipartFile file){ + if (file == null || file.isEmpty()) { + return true; + } + String originalFilename = file.getOriginalFilename(); + return originalFilename == null || originalFilename.isBlank(); + }src/main/java/com/campustable/be/domain/s3/service/S3Service.java (1)
107-113: URL 키 추출 로직을 URI 기반으로 보강하는 것을 권장합니다.도메인/쿼리 변화에 취약하므로
URI파싱 기반으로 키를 추출하는 편이 안전합니다.♻️ 제안 변경
private String extractKeyFromUrl(String fileUrl) { - if (fileUrl.contains(".com/")) { - String key = fileUrl.substring(fileUrl.lastIndexOf(".com/") + 5); - log.info("추출된 S3 Key: [{}]", key); - return key; - } - return fileUrl; + try { + var uri = java.net.URI.create(fileUrl); + String path = uri.getPath(); + if (path == null || path.isBlank()) { + return fileUrl; + } + String key = path.startsWith("/") ? path.substring(1) : path; + if (key.startsWith(bucket + "/")) { + key = key.substring(bucket.length() + 1); + } + log.info("추출된 S3 Key: [{}]", key); + return key; + } catch (IllegalArgumentException e) { + return fileUrl; + } }src/main/java/com/campustable/be/domain/menu/service/MenuService.java (1)
91-97: oldUrl과 newUrl이 동일할 때 삭제를 건너뛰어 주세요.
uploadFile이 동일 키를 재사용하는 경우 새 파일까지 삭제될 수 있습니다. 동일 여부 확인 후 삭제하도록 가드가 필요합니다.🛡️ 제안 수정
- if (oldUrl != null && !oldUrl.isBlank()) { + if (oldUrl != null && !oldUrl.isBlank() && !oldUrl.equals(newUrl)) {src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java (1)
72-109: 멀티파트 요청 스키마가 Swagger에 제대로 노출되는지 확인해주세요.
@Parameter만으로는image가 file(binary)로 표시되지 않을 수 있으니@RequestBody+@Content(mediaType="multipart/form-data")와@Schema(type="string", format="binary")적용 여부를 점검해주세요.📌 예시 (요지)
`@io.swagger.v3.oas.annotations.parameters.RequestBody`( content = `@Content`( mediaType = "multipart/form-data", schema = `@Schema`( type = "object" // image: type="string", format="binary" 등 ) ) )
build.gradle
Outdated
| // S3 | ||
| implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2' |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
spring-cloud-aws-starter-s3 3.4.2 Spring Boot 3.5.7 compatibility requirements
💡 Result:
For io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2 (Spring Cloud AWS 3.4.x) the expected compatibility stack is:
- Spring Boot: 3.5.x (so Spring Boot 3.5.7 is in-family and supported) [1][2]
- Spring Cloud release train: 2025.0.x (Northfields) [1][3]
- Spring Framework: 6.2.x [2]
- AWS SDK: AWS SDK for Java v2 (not v1) [2]
- Java: 17+ (baseline for Spring Boot 3 / Spring Framework 6) [4]
Use the matching BOM to keep versions aligned:
io.awspring.cloud:spring-cloud-aws-dependencies:3.4.2(then addspring-cloud-aws-starter-s3without a version). [2]
Sources: Spring Cloud release train ↔ Boot matrix [1], Spring Cloud AWS compatibility table [2], Spring Boot 3.5.7 release confirmation [3], Java 17 baseline for Spring 6 / Boot 3 [4].
🌐 Web query:
spring-cloud-aws-starter-s3 3.4.2 security vulnerabilities CVE
💡 Result:
For io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2 there are no widely reported “direct” CVEs in the starter itself, but scanners commonly flag transitive dependency CVEs pulled in via Spring Cloud AWS / Spring Framework / AWS libraries:
-
CVE-2025-14763 (Amazon S3 Encryption Client for Java) – “missing cryptographic key commitment”; mitigate by upgrading the Amazon S3 Encryption Client to 4.0.0+ (or ensuring your dependency/BOM resolves to a fixed version). This CVE is commonly reported as coming from Spring Cloud AWS 3.4.2 dependency graph. [1][2]
-
CVE-2025-41242 (Spring Framework path traversal on non-compliant Servlet containers) – affects Spring Framework 6.2.0–6.2.9; fixed in 6.2.10. If your resolved Spring Framework version is in that range, you’re affected. [3]
-
CVE-2025-41249 (Spring Framework annotation detection / authorization decisions) – affects Spring Framework 6.2.0–6.2.10; fixed in 6.2.11. If your resolved Spring Framework version is in that range, you’re affected. [4]
A Spring Cloud AWS maintainer issue notes scanners flagging Spring Cloud AWS (including spring-cloud-aws-starter-s3) pulling Spring Framework 6.2.7, which would fall into the affected ranges above for CVE-2025-41242 and CVE-2025-41249. [5]
What to do
- Check your resolved dependency tree (
mvn dependency:tree/ Gradledependencies) for:amazon-s3-encryption-client-java(or similarly named artifact) and bump to 4.0.0+ if present. [2]org.springframework:spring-*versions; upgrade Spring Framework to 6.2.11+ to cover both CVE-2025-41242 (via 6.2.10) and CVE-2025-41249 (via 6.2.11). [3][4]
Sources:
[1] MvnRepository (Spring Cloud AWS 3.4.2) vulnerability listing (CVE-2025-14763) (mvnrepository.com)
[2] NVD record for CVE-2025-14763 (nvd.nist.gov)
[3] Spring advisory for CVE-2025-41242 (spring.io)
[4] Spring advisory for CVE-2025-41249 (spring.io)
[5] awspring/spring-cloud-aws issue discussing Spring 6.2.7 and CVE-2025-41242/41249 exposure (github.com)
Citations:
- 1: https://mvnrepository.com/artifact/io.awspring.cloud/spring-cloud-aws/3.4.2?utm_source=openai
- 2: https://nvd.nist.gov/vuln/detail/CVE-2025-14763?utm_source=openai
- 3: https://spring.io/security/cve-2025-41242?utm_source=openai
- 4: https://spring.io/security/cve-2025-41249/?utm_source=openai
- 5: Upgrade to spring-core@6.2.11 to resolve CVE-2025-41249 awspring/spring-cloud-aws#1516
🌐 Web query:
io.awspring.cloud spring-cloud-aws-starter-s3 3.4.2 release notes
💡 Result:
io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2 is part of the Spring Cloud AWS 3.4.2 release (released Dec 1, 2025). [2]
Release notes (3.4.2)
The 3.4.2 release contains a single documented change (no S3-specific items are listed for this patch release):
- Core:
StsWebIdentityTokenFileCredentialsProvideris no longer auto-configured unless the required properties are provided (PR #1528). [1]
References
- Release entry / “What’s Changed” + reference docs for 3.4.2: [1]
- Release date (3.4.2): [2]
Spring Framework 보안 패치 필요
Spring Boot 3.5.7과의 호환성은 정상이나, spring-cloud-aws-starter-s3 3.4.2에서 가져오는 Spring Framework 6.2.7에 다음 보안 취약점이 있습니다:
- CVE-2025-41242 (경로 순회 취약점): 6.2.10+로 해결
- CVE-2025-41249 (인증 결정 우회): 6.2.11+로 해결
또한 전이 의존성에서 CVE-2025-14763 (Amazon S3 Encryption Client)이 보고될 수 있으므로, spring-cloud-aws-dependencies:3.4.2 BOM을 사용하거나 Spring Framework를 명시적으로 6.2.11+로 업그레이드하세요.
🤖 Prompt for AI Agents
In `@build.gradle` around lines 52 - 53, The project pulls
io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2 which brings Spring
Framework 6.2.7 with known CVEs; update your build to ensure Spring Framework is
at least 6.2.11 by either (A) switching to a fixed AWS starter/BOM that forces
newer Spring (e.g., import or update spring-cloud-aws-dependencies to a version
that resolves to Spring Framework 6.2.11+), or (B) explicitly override the
Spring Framework BOM/dependency to 6.2.11+ in your Gradle configuration so that
io.awspring.cloud:spring-cloud-aws-starter-s3 resolves against Spring Framework
>=6.2.11; ensure the chosen approach also addresses transitive CVE-2025-14763
from S3 encryption client.
| public String uploadFile(MultipartFile file, String dirName) { | ||
|
|
||
| String originalFilename = validateAndExtractFilename(file); | ||
|
|
||
| String storedPath = dirName + "/" + UUID.randomUUID() + "_" + originalFilename; | ||
| log.debug("생성된 파일명: {}", storedPath); | ||
|
|
||
| try (InputStream inputStream = file.getInputStream()) { | ||
|
|
||
| S3Resource resource = s3Template.upload(bucket, storedPath, inputStream); | ||
|
|
||
| String s3Url = resource.getURL().toString(); | ||
| log.info("S3 업로드 성공: {}", s3Url); | ||
|
|
||
| return s3Url; | ||
|
|
||
| } catch (S3Exception e) { | ||
| log.error("AmazonServiceException - S3 파일 업로드 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage()); | ||
| throw new CustomException(ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR); | ||
| } catch (IOException e) { | ||
| log.error("IOException - 파일 스트림 처리 중 에러 발생. 원본 파일명: {}, 파일명: {} 에러: {}", originalFilename, storedPath, e.getMessage()); | ||
| throw new CustomException(ErrorCode.S3_UPLOAD_ERROR); | ||
| } |
There was a problem hiding this comment.
업로드 경로에서 런타임 예외가 그대로 전파됩니다.
네트워크/자격증명 등 런타임 예외가 누락되어 에러코드 매핑이 깨질 수 있습니다.
🐛 제안 변경
} catch (S3Exception e) {
log.error("AmazonServiceException - S3 파일 업로드 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage());
throw new CustomException(ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR);
} catch (IOException e) {
log.error("IOException - 파일 스트림 처리 중 에러 발생. 원본 파일명: {}, 파일명: {} 에러: {}", originalFilename, storedPath, e.getMessage());
throw new CustomException(ErrorCode.S3_UPLOAD_ERROR);
+ } catch (RuntimeException e) {
+ log.error("RuntimeException - S3 파일 업로드 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage());
+ throw new CustomException(ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR);
}📝 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.
| public String uploadFile(MultipartFile file, String dirName) { | |
| String originalFilename = validateAndExtractFilename(file); | |
| String storedPath = dirName + "/" + UUID.randomUUID() + "_" + originalFilename; | |
| log.debug("생성된 파일명: {}", storedPath); | |
| try (InputStream inputStream = file.getInputStream()) { | |
| S3Resource resource = s3Template.upload(bucket, storedPath, inputStream); | |
| String s3Url = resource.getURL().toString(); | |
| log.info("S3 업로드 성공: {}", s3Url); | |
| return s3Url; | |
| } catch (S3Exception e) { | |
| log.error("AmazonServiceException - S3 파일 업로드 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage()); | |
| throw new CustomException(ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR); | |
| } catch (IOException e) { | |
| log.error("IOException - 파일 스트림 처리 중 에러 발생. 원본 파일명: {}, 파일명: {} 에러: {}", originalFilename, storedPath, e.getMessage()); | |
| throw new CustomException(ErrorCode.S3_UPLOAD_ERROR); | |
| } | |
| public String uploadFile(MultipartFile file, String dirName) { | |
| String originalFilename = validateAndExtractFilename(file); | |
| String storedPath = dirName + "/" + UUID.randomUUID() + "_" + originalFilename; | |
| log.debug("생성된 파일명: {}", storedPath); | |
| try (InputStream inputStream = file.getInputStream()) { | |
| S3Resource resource = s3Template.upload(bucket, storedPath, inputStream); | |
| String s3Url = resource.getURL().toString(); | |
| log.info("S3 업로드 성공: {}", s3Url); | |
| return s3Url; | |
| } catch (S3Exception e) { | |
| log.error("AmazonServiceException - S3 파일 업로드 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage()); | |
| throw new CustomException(ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR); | |
| } catch (IOException e) { | |
| log.error("IOException - 파일 스트림 처리 중 에러 발생. 원본 파일명: {}, 파일명: {} 에러: {}", originalFilename, storedPath, e.getMessage()); | |
| throw new CustomException(ErrorCode.S3_UPLOAD_ERROR); | |
| } catch (RuntimeException e) { | |
| log.error("RuntimeException - S3 파일 업로드 실패. 버킷: {}, 파일명: {}, 에러: {}", bucket, storedPath, e.getMessage()); | |
| throw new CustomException(ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR); | |
| } |
🤖 Prompt for AI Agents
In `@src/main/java/com/campustable/be/domain/s3/service/S3Service.java` around
lines 54 - 76, The uploadFile method can let unchecked runtime exceptions from
s3Template.upload (e.g., network/credential issues) escape and break ErrorCode
mapping; add an additional catch for RuntimeException (after the existing
S3Exception and IOException catches) that logs the bucket, storedPath and
originalFilename along with e.getMessage(), and rethrow a CustomException with
the appropriate S3 error code (e.g., ErrorCode.S3_UPLOAD_AMAZON_CLIENT_ERROR or
a new S3_UPLOAD_RUNTIME_ERROR) so all runtime failures are mapped consistently
for uploadFile / s3Template.upload.
…d시 runtime 예외 추가} #76
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/main/java/com/campustable/be/domain/menu/service/MenuService.java`:
- Around line 185-192: The current flow deletes the S3 file via
s3Service.deleteFile(menu.get().getMenuUrl()) before removing the DB row with
menuRepository.deleteById(menuId), which can leave the DB pointing at a
now-missing S3 object if deleteById fails; change the sequence to (1) capture
the menu URL into a local variable (e.g., String menuUrl =
menu.get().getMenuUrl()) so you have it after the DB operation, (2) call
menuRepository.deleteById(menuId) first, and only if that succeeds attempt
s3Service.deleteFile(menuUrl) inside a try/catch, logging failures with
log.warn("deleteMenu: 이미지 삭제 실패. menuId={}, url={}", menuId, menuUrl, e) so S3
errors don’t block DB deletion.
| if (menu.get().getMenuUrl() != null && !menu.get().getMenuUrl().isBlank()) { | ||
| try { | ||
| s3Service.deleteFile(menu.get().getMenuUrl()); | ||
| } catch (Exception e) { | ||
| log.warn("deleteMenu: 이미지 삭제 실패. menuId={}, url={}", menuId, menu.get().getMenuUrl(), e); | ||
| } | ||
| } | ||
| menuRepository.deleteById(menuId); |
There was a problem hiding this comment.
S3 삭제와 DB 삭제 순서 조정 필요
현재 S3 파일을 삭제한 후 DB 레코드를 삭제합니다. deleteById()가 실패할 경우 (예: 외래키 제약 위반), S3 파일은 이미 삭제되었지만 메뉴 레코드는 유효하지 않은 URL을 가진 채로 남게 됩니다.
DB 삭제를 먼저 수행하고, 성공 시 S3 파일을 삭제하는 것이 안전합니다.
수정 제안
`@Transactional`
public void deleteMenu(Long menuId) {
Optional<Menu> menu = menuRepository.findById(menuId);
if (menu.isEmpty()) {
log.error("menuId not found {}", menuId);
throw new CustomException(ErrorCode.MENU_NOT_FOUND);
}
+ String menuUrl = menu.get().getMenuUrl();
+ menuRepository.deleteById(menuId);
+
+ if (menuUrl != null && !menuUrl.isBlank()) {
- if (menu.get().getMenuUrl() != null && !menu.get().getMenuUrl().isBlank()) {
try {
- s3Service.deleteFile(menu.get().getMenuUrl());
+ s3Service.deleteFile(menuUrl);
} catch (Exception e) {
- log.warn("deleteMenu: 이미지 삭제 실패. menuId={}, url={}", menuId, menu.get().getMenuUrl(), e);
+ log.warn("deleteMenu: 이미지 삭제 실패. menuId={}, url={}", menuId, menuUrl, e);
}
}
- menuRepository.deleteById(menuId);
}🤖 Prompt for AI Agents
In `@src/main/java/com/campustable/be/domain/menu/service/MenuService.java` around
lines 185 - 192, The current flow deletes the S3 file via
s3Service.deleteFile(menu.get().getMenuUrl()) before removing the DB row with
menuRepository.deleteById(menuId), which can leave the DB pointing at a
now-missing S3 object if deleteById fails; change the sequence to (1) capture
the menu URL into a local variable (e.g., String menuUrl =
menu.get().getMenuUrl()) so you have it after the DB operation, (2) call
menuRepository.deleteById(menuId) first, and only if that succeeds attempt
s3Service.deleteFile(menuUrl) inside a try/catch, logging failures with
log.warn("deleteMenu: 이미지 삭제 실패. menuId={}, url={}", menuId, menuUrl, e) so S3
errors don’t block DB deletion.
✨ 변경 사항
✅ 테스트
Summary by CodeRabbit
New Features
Chores
Documentation