Conversation
개요AWS S3 SDK v2로 업그레이드하고, 클럽·게시글·사용자 프로필 이미지 업로드 기능을 추가했습니다. S3 서비스, 이미지 검증, 멀티파트 요청 처리, 관련 DTO 및 엔드포인트가 구현되었습니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
participant Client as Client
participant Controller as ClubController
participant Service as ClubServiceImpl
participant S3Svc as S3Service
participant S3 as AWS S3
participant DB as Database
Client->>Controller: POST /clubs (multipart: request, image)
Controller->>Controller: 요청 검증
Controller->>Service: createClub(request, image, user)
alt 이미지 제공됨
Service->>S3Svc: uploadFile(PathName.CLUB, image)
S3Svc->>S3Svc: 파일 검증 (확장자, 크기)
S3Svc->>S3Svc: S3 키 생성
S3Svc->>S3: putObject (파일 업로드)
S3->>S3Svc: 업로드 완료
S3Svc->>S3Svc: 파일 URL 생성
S3Svc-->>Service: 파일 URL 반환
end
Service->>Service: Club 엔티티 생성 (clubImageUrl 포함)
Service->>DB: save(club)
DB-->>Service: 저장된 club
Service-->>Controller: ClubResponse
Controller-->>Client: 201 Created (ClubResponse)
예상 코드 리뷰 시간🎯 4 (복잡함) | ⏱️ ~45분 관련 PR
제안 리뷰어
축하 시
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java (1)
62-74:⚠️ Potential issue | 🟡 Minor게시글 삭제 시 S3 이미지 정리 누락
deletePost에서 게시글을 삭제할 때 연관된 S3 이미지(post.getImgUrl())를 삭제하지 않습니다. 이로 인해 S3에 orphan 파일이 축적될 수 있습니다.🧹 S3 이미지 삭제 추가 제안
`@Override` `@Transactional` public void deletePost(Long postId, User user) { Post post = postRepository.findById(postId) .orElseThrow(() -> new CustomException(PostErrorCode.POST_NOT_FOUND)); // 작성자 확인 if (post.getUser().getId() != user.getId()) { throw new CustomException(PostErrorCode.POST_DELETE_DENIED); } + // S3 이미지 삭제 + if (post.getImgUrl() != null && !post.getImgUrl().isEmpty()) { + s3Service.deleteFile(post.getImgUrl()); + } + postRepository.delete(post); }
🤖 Fix all issues with AI agents
In `@src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java`:
- Around line 87-110: The current updateClubImage deletes the old S3 file before
attempting to upload the new one, risking image loss if upload fails; change
updateClubImage (in ClubServiceImpl) to first call
s3Service.uploadFile(PathName.CLUB, image) and get the new clubImageUrl, then
update the entity (club.updateClubImage(clubImageUrl)), persist, and only after
successful upload/update call s3Service.deleteFile(oldClubImageUrl) to remove
the previous file; also add a guard to avoid deleting when oldClubImageUrl is
null or equals the new clubImageUrl and consider wrapping upload+update in
appropriate try/catch to avoid leaking the newly uploaded file on subsequent DB
failures.
In `@src/main/java/com/be/sportizebe/domain/user/controller/UserController.java`:
- Line 28: The controller is injecting the concrete class UserServiceImpl via
the field userService; change the dependency to the interface type UserService
and update any constructor or field injection points in UserController to accept
UserService instead of UserServiceImpl (and adjust imports) so Spring injects
the implementation while the controller depends on the interface for better DIP,
testability, and flexibility.
In `@src/main/java/com/be/sportizebe/domain/user/service/UserServiceImpl.java`:
- Around line 64-70: The current flow in UserServiceImpl deletes the existing S3
file via s3Service.deleteFile(user.getProfileImage()) before calling
s3Service.uploadFile(...), which risks losing the profile image if the upload
fails; change the sequence to first upload the new file with
s3Service.uploadFile(PathName.PROFILE, file), obtain the new profileImageUrl,
update the user's profileImage and persist the user, and only after a successful
persist attempt to delete the old image (user.getProfileImage()) via
s3Service.deleteFile; ensure you compare the old and new URLs to avoid deleting
the newly uploaded file and handle/log upload/persist failures accordingly.
In `@src/main/java/com/be/sportizebe/global/config/S3Config.java`:
- Around line 12-32: The class-level `@Getter` on S3Config is exposing sensitive
fields; update the class to avoid public getters for accessKey and secretKey by
either removing the class-level `@Getter` and adding explicit getters only for
non-sensitive fields (bucket, profile, club, post) or keep `@Getter` on the class
but add `@Getter`(AccessLevel.NONE) (Lombok) to the accessKey and secretKey fields
so they do not generate public getters; ensure you reference the S3Config class
and the accessKey and secretKey fields when making the change.
🧹 Nitpick comments (10)
src/main/java/com/be/sportizebe/global/s3/enums/PathName.java (1)
7-15: 필드가 없는 enum에는 Lombok 어노테이션이 불필요합니다.이 enum은 필드가 없으므로
@Getter는 코드를 생성하지 않고,@RequiredArgsConstructor는 Java enum의 기본 private 생성자와 동일한 private 생성자만 생성합니다. 두 어노테이션 모두 제거해도 코드 동작에는 영향이 없습니다.🧹 제안 수정안
-import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor public enum PathName {src/main/java/com/be/sportizebe/global/config/S3Config.java (1)
34-43: 자격 증명 공급자와 리전을 환경별로 분리하는 것을 권장합니다.현재 코드는
StaticCredentialsProvider로 고정된 자격 증명을 사용하고 리전도AP_NORTHEAST_2로 하드코딩되어 있어, 운영 환경에서 자격 증명 체인 활용과 리전 변경이 제한됩니다. AWS SDK for Java v2의 공식 가이드에 따르면 프로덕션에서는DefaultCredentialsProvider를 기본값으로 사용하는 것이 권장되며, 이는 환경 변수, IAM 역할, OIDC 등 보안 자격 증명 체인을 활용합니다. 개발 환경에서만 필요시 정적 키를 사용하도록 분리하면 더 안전합니다.♻️ 제안 수정안
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; + `@Value`("${spring.cloud.aws.region:ap-northeast-2}") + private String region; `@Bean` S3Client s3Client() { - AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); + AwsCredentialsProvider credentialsProvider = + (accessKey == null || accessKey.isBlank() || secretKey == null || secretKey.isBlank()) + ? DefaultCredentialsProvider.create() + : StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)); return S3Client.builder() - .region(Region.AP_NORTHEAST_2) + .region(Region.of(region)) - .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .credentialsProvider(credentialsProvider) .build(); }src/main/java/com/be/sportizebe/global/s3/convertor/MultipartJackson2HttpMessageConverter.java (2)
1-1: 패키지명 오타:convertor→converter패키지명에 오타가 있습니다. 영어 철자법에 따르면
converter가 올바른 표기입니다.
12-17: 주석과 실제 구현 간 불일치주석은 "multipart/form-data" 헤더 지원을 언급하지만, 실제로는
MediaType.APPLICATION_OCTET_STREAM으로 등록되어 있습니다. 이 컨버터의 목적은 Swagger/OpenAPI와 함께 multipart 요청을 처리할 때 Jackson이application/octet-stream타입을 읽을 수 있도록 하면서 쓰기는 방지하는 것입니다. 주석을 실제 동작에 맞게 수정하는 것을 권장합니다.📝 주석 수정 제안
`@Component` public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { /** - * "Content-Type: multipart/form-data" 헤더를 지원하는 HTTP 요청 변환기 + * Swagger/OpenAPI multipart 요청 처리를 위한 HTTP 메시지 변환기. + * APPLICATION_OCTET_STREAM 타입 읽기를 허용하고, 쓰기는 비활성화합니다. */ public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) { super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); }src/main/java/com/be/sportizebe/domain/user/service/UserServiceImpl.java (1)
29-56: 코드 들여쓰기 불일치메서드 내 들여쓰기가 일관되지 않습니다. Lines 33-36은 4칸 들여쓰기를, Lines 38-41은 2칸 들여쓰기를 사용합니다. 일관성을 위해 들여쓰기를 통일해 주세요.
src/main/java/com/be/sportizebe/global/s3/service/S3ServiceImpl.java (3)
113-117: 하드코딩된 AWS 리전리전
ap-northeast-2가 하드코딩되어 있습니다. 환경별로 다른 리전을 사용할 수 있으므로,S3Config에서 리전 값을 주입받도록 수정하는 것을 권장합니다.♻️ 리전 설정 외부화 제안
// s3 url : https://{버킷명}.s3.ap-northeast-2.amazonaws.com/{파일명} private String getFileUrl(String fileName) { - return String.format("https://%s.s3.ap-northeast-2.amazonaws.com/%s", - s3Config.getBucket(), fileName); + return String.format("https://%s.s3.%s.amazonaws.com/%s", + s3Config.getBucket(), s3Config.getRegion(), fileName); }
54-69: deleteFile 메서드 예외 처리 부재
deleteFile에서 S3 클라이언트 호출 시 발생할 수 있는 예외(S3Exception등)에 대한 처리가 없습니다.uploadFile과 동일하게 try-catch로 감싸서 로깅하고 적절한 에러를 반환하는 것이 좋습니다.🛡️ 예외 처리 추가 제안
`@Override` public void deleteFile(String fileUrl) { if (fileUrl == null || !fileUrl.contains(".com/")) { throw new CustomException(S3ErrorCode.FILE_URL_INVALID); } String fileName = extractFileName(fileUrl); - DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() - .bucket(s3Config.getBucket()) - .key(fileName) - .build(); - - s3Client.deleteObject(deleteObjectRequest); - log.info("S3 파일 삭제 완료: {}", fileName); + try { + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(s3Config.getBucket()) + .key(fileName) + .build(); + + s3Client.deleteObject(deleteObjectRequest); + log.info("S3 파일 삭제 완료: {}", fileName); + } catch (Exception e) { + log.error("S3 파일 삭제 실패: {}", e.getMessage()); + throw new CustomException(S3ErrorCode.FILE_SERVER_ERROR); + } }
107-111: URL 파싱 로직의 취약성
extractFileName이.com/문자열을 기준으로 파싱합니다. 이 방식은 S3 URL 형식이 변경되거나 다른 형식의 URL이 전달될 경우 오작동할 수 있습니다. 보다 견고한 URL 파싱을 위해java.net.URI를 사용하는 것을 고려해 보세요.♻️ URI 파싱 사용 제안
// 파일명 추출 private String extractFileName(String fileUrl) { - // https://bucket.s3.region.amazonaws.com/folder/filename.ext -> folder/filename.ext - return fileUrl.substring(fileUrl.indexOf(".com/") + 5); + try { + java.net.URI uri = java.net.URI.create(fileUrl); + String path = uri.getPath(); + // path starts with "/" so remove it + return path.startsWith("/") ? path.substring(1) : path; + } catch (IllegalArgumentException e) { + throw new CustomException(S3ErrorCode.FILE_URL_INVALID); + } }src/main/java/com/be/sportizebe/domain/user/controller/UserController.java (1)
3-3: 사용되지 않는 import
AuthErrorCode가 import되었지만 이 파일에서 사용되지 않습니다.🧹 사용되지 않는 import 제거
-import com.be.sportizebe.domain.auth.exception.AuthErrorCode;src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java (1)
30-46: DB 저장 실패 시 S3 파일이 고아로 남을 수 있습니다.업로드 후 저장 과정에서 예외가 나면 파일이 정리되지 않습니다. 업로드 성공 후 저장 단계에서 예외가 발생할 때 S3 정리를 보장해 주세요.
🔧 제안 수정안
- String clubImageUrl = null; - if (image != null && !image.isEmpty()) { - clubImageUrl = s3Service.uploadFile(PathName.CLUB, image); - } - - // 동호회 엔티티 생성 - Club club = request.toEntity(user, clubImageUrl); - clubRepository.save(club); + String clubImageUrl = null; + try { + if (image != null && !image.isEmpty()) { + clubImageUrl = s3Service.uploadFile(PathName.CLUB, image); + } + + // 동호회 엔티티 생성 + Club club = request.toEntity(user, clubImageUrl); + clubRepository.save(club); + } catch (RuntimeException e) { + if (clubImageUrl != null) { + s3Service.deleteFile(clubImageUrl); + } + throw e; + }
| @Override | ||
| @Transactional | ||
| public ClubImageResponse updateClubImage(Long clubId, MultipartFile image, User user) { | ||
| Club club = clubRepository.findById(clubId) | ||
| .orElseThrow(() -> new CustomException(ClubErrorCode.CLUB_NOT_FOUND)); | ||
|
|
||
| // 동호회 방장만 수정 가능하도록 검증 | ||
| if (club.getLeader().getId() != user.getId()) { | ||
| throw new CustomException(ClubErrorCode.CLUB_UPDATE_DENIED); | ||
| } | ||
|
|
||
| // 기존 이미지가 있으면 S3에서 삭제 | ||
| if (club.getClubImage() != null) { | ||
| s3Service.deleteFile(club.getClubImage()); | ||
| } | ||
|
|
||
| // 새 이미지 S3에 업로드 | ||
| String clubImageUrl = s3Service.uploadFile(PathName.CLUB, image); | ||
|
|
||
| // 동호회 이미지 URL 업데이트 | ||
| club.updateClubImage(clubImageUrl); | ||
|
|
||
| return ClubImageResponse.from(clubImageUrl); | ||
| } |
There was a problem hiding this comment.
기존 이미지 삭제를 업로드 성공 이후로 미루세요.
현재 순서는 업로드 실패 시 기존 이미지가 소실됩니다. 새 이미지 업로드 성공 후에 기존 이미지를 삭제하는 방식이 더 안전합니다.
✅ 제안 수정안
- // 기존 이미지가 있으면 S3에서 삭제
- if (club.getClubImage() != null) {
- s3Service.deleteFile(club.getClubImage());
- }
-
- // 새 이미지 S3에 업로드
- String clubImageUrl = s3Service.uploadFile(PathName.CLUB, image);
-
- // 동호회 이미지 URL 업데이트
- club.updateClubImage(clubImageUrl);
-
- return ClubImageResponse.from(clubImageUrl);
+ String oldImageUrl = club.getClubImage();
+
+ // 새 이미지 S3에 업로드
+ String clubImageUrl = s3Service.uploadFile(PathName.CLUB, image);
+
+ // 동호회 이미지 URL 업데이트
+ club.updateClubImage(clubImageUrl);
+
+ // 기존 이미지가 있으면 S3에서 삭제
+ if (oldImageUrl != null) {
+ s3Service.deleteFile(oldImageUrl);
+ }
+
+ return ClubImageResponse.from(clubImageUrl);🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java`
around lines 87 - 110, The current updateClubImage deletes the old S3 file
before attempting to upload the new one, risking image loss if upload fails;
change updateClubImage (in ClubServiceImpl) to first call
s3Service.uploadFile(PathName.CLUB, image) and get the new clubImageUrl, then
update the entity (club.updateClubImage(clubImageUrl)), persist, and only after
successful upload/update call s3Service.deleteFile(oldClubImageUrl) to remove
the previous file; also add a guard to avoid deleting when oldClubImageUrl is
null or equals the new clubImageUrl and consider wrapping upload+update in
appropriate try/catch to avoid leaking the newly uploaded file on subsequent DB
failures.
| public class UserController { | ||
|
|
||
| private final UserService userService; | ||
| private final UserServiceImpl userService; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
인터페이스 대신 구체 클래스 주입 사용
UserServiceImpl 구체 클래스를 직접 주입하고 있습니다. 의존성 역전 원칙(DIP)에 따라 인터페이스 타입(UserService)을 사용하는 것이 권장됩니다. 이는 테스트 용이성과 유연성을 높여줍니다.
♻️ 인터페이스 타입 사용 제안
- private final UserServiceImpl userService;
+ private final UserService userService;import도 함께 수정:
-import com.be.sportizebe.domain.user.service.UserServiceImpl;
+import com.be.sportizebe.domain.user.service.UserService;📝 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.
| private final UserServiceImpl userService; | |
| private final UserService userService; |
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/user/controller/UserController.java`
at line 28, The controller is injecting the concrete class UserServiceImpl via
the field userService; change the dependency to the interface type UserService
and update any constructor or field injection points in UserController to accept
UserService instead of UserServiceImpl (and adjust imports) so Spring injects
the implementation while the controller depends on the interface for better DIP,
testability, and flexibility.
| // 기존 프로필 이미지가 있으면 S3에서 삭제 | ||
| if (user.getProfileImage() != null) { | ||
| s3Service.deleteFile(user.getProfileImage()); | ||
| } | ||
|
|
||
| // 새 프로필 이미지 S3에 업로드 | ||
| String profileImageUrl = s3Service.uploadFile(PathName.PROFILE, file); |
There was a problem hiding this comment.
기존 이미지 삭제 후 업로드 실패 시 데이터 손실 가능성
현재 로직에서 기존 프로필 이미지를 먼저 S3에서 삭제한 후 새 이미지를 업로드합니다. 만약 새 이미지 업로드가 실패하면 사용자는 프로필 이미지를 잃게 됩니다. 업로드 성공 후 기존 이미지를 삭제하는 순서로 변경하는 것을 권장합니다.
🛡️ 순서 변경 제안
`@Override`
`@Transactional`
public ProfileImageResponse uploadProfileImage(Long userId, MultipartFile file) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));
- // 기존 프로필 이미지가 있으면 S3에서 삭제
- if (user.getProfileImage() != null) {
- s3Service.deleteFile(user.getProfileImage());
- }
+ String oldImageUrl = user.getProfileImage();
// 새 프로필 이미지 S3에 업로드
String profileImageUrl = s3Service.uploadFile(PathName.PROFILE, file);
// 사용자 프로필 이미지 URL 업데이트
user.updateProfileImage(profileImageUrl);
+ // 새 이미지 업로드 성공 후 기존 이미지 삭제
+ if (oldImageUrl != null) {
+ s3Service.deleteFile(oldImageUrl);
+ }
+
log.info("사용자 프로필 이미지 업로드 완료: userId={}, url={}", userId, profileImageUrl);
return ProfileImageResponse.from(profileImageUrl);
}🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/user/service/UserServiceImpl.java`
around lines 64 - 70, The current flow in UserServiceImpl deletes the existing
S3 file via s3Service.deleteFile(user.getProfileImage()) before calling
s3Service.uploadFile(...), which risks losing the profile image if the upload
fails; change the sequence to first upload the new file with
s3Service.uploadFile(PathName.PROFILE, file), obtain the new profileImageUrl,
update the user's profileImage and persist the user, and only after a successful
persist attempt to delete the old image (user.getProfileImage()) via
s3Service.deleteFile; ensure you compare the old and new URLs to avoid deleting
the newly uploaded file and handle/log upload/persist failures accordingly.
| @Getter | ||
| @Configuration | ||
| public class S3Config { | ||
|
|
||
| @Value("${spring.cloud.aws.s3.bucket}") | ||
| private String bucket; // 버킷 명 | ||
|
|
||
| @Value("${spring.cloud.aws.s3.path.profile}") | ||
| private String profile; // 프로필 사진 | ||
|
|
||
| @Value("${spring.cloud.aws.s3.path.club}") | ||
| private String club; | ||
|
|
||
| @Value("${spring.cloud.aws.s3.path.post}") | ||
| private String post; | ||
|
|
||
| @Value("${spring.cloud.aws.credentials.access-key}") | ||
| private String accessKey; | ||
|
|
||
| @Value("${spring.cloud.aws.credentials.secret-key}") | ||
| private String secretKey; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# 전체 S3Config.java 파일 내용 확인
find . -name "S3Config.java" -type fRepository: Sportize/be
Length of output: 116
🏁 Script executed:
# S3Config 사용 패턴 검색 - getAccessKey/getSecretKey 호출 확인
rg "S3Config|getAccessKey|getSecretKey" --type java -B 2 -A 2Repository: Sportize/be
Length of output: 1429
🌐 Web query:
Lombok @Getter annotation class-level behavior on all fields including private credentials
💡 Result:
Putting Lombok’s @Getter on a class is equivalent to putting @Getter on every non-static field in that class (unless that field already has its own @Getter). That includes private fields, so a private credential like private String password; will get a getter method generated (by default, public) such as public String getPassword(). (projectlombok.org)
To avoid generating a getter for sensitive fields, override at the field level:
import lombok.AccessLevel;
import lombok.Getter;
`@Getter`
class User {
private String username;
`@Getter`(AccessLevel.NONE)
private String password; // no getter generated
}AccessLevel.NONE is the Lombok-supported way to suppress generation for a specific field, even when the class is annotated. (projectlombok.org)
Citations:
- 1: https://projectlombok.org/features/GetterSetter?utm_source=openai
- 2: https://projectlombok.org/features/GetterSetter?utm_source=openai
🌐 Web query:
Spring Boot AWS S3 credentials security best practices sensitive data exposure
💡 Result:
Spring Boot + AWS S3 credentials security best practices (avoid sensitive data exposure)
1) Don’t hardcode AWS keys anywhere (code, application.yml, repos)
- AWS explicitly warns not to put literal access keys in application files and not to include credential files in your project (they’re often plaintext). [1]
2) Prefer temporary credentials via IAM roles (no long‑term keys)
- For workloads on AWS (EC2/ECS/EKS/Lambda), use an IAM role so the app automatically receives short‑term STS credentials and you don’t distribute long-lived keys. [2]
- S3 guidance similarly recommends IAM roles and not storing credentials in the app or instance. [3]
- Using short-term credentials is a core mitigation for credential compromise. [7]
3) In Spring Boot/Spring Cloud AWS, use the default provider chain (not static properties)
- Spring Cloud AWS can use a static credentials provider when
cloud.aws.credentials.access-keyis set—avoid that for anything beyond local throwaway testing. [4] - Prefer enabling the default AWS credentials chain, which will pick up role-based credentials (and other standard sources) without embedding secrets:
[4]
cloud.aws.credentials.use-default-aws-credentials-chain=true
4) For local development: use SSO / Identity Center or scoped dev keys (only if necessary)
- AWS guidance recommends avoiding IAM users for “purpose-built software” and using federation (for example, IAM Identity Center) instead of long-term credentials. [1]
- If dev keys are unavoidable in a sandbox: don’t share them, enforce least privilege, rotate/audit, and enable MFA. [5]
5) Reduce blast radius: least privilege + tight S3 access controls
- Grant the app role only the required S3 actions on the required bucket/prefix (least privilege is a primary IAM best practice). [2]
6) Detect and respond to exposure
- Use monitoring/classification tooling (for example, Macie) to detect credentials or other sensitive data that may have landed in S3 and remediate quickly. [6]
🏁 Script executed:
cat -n ./src/main/java/com/be/sportizebe/global/config/S3Config.javaRepository: Sportize/be
Length of output: 1781
🏁 Script executed:
# S3Client bean 생성 및 credentials 사용 패턴 검색
rg "S3Client|AwsBasicCredentials|Credentials" -B 3 -A 3 --type javaRepository: Sportize/be
Length of output: 6058
🏁 Script executed:
# S3Config 내에서 accessKey/secretKey 실제 사용 확인
rg -A 20 "public class S3Config" src/main/java/com/be/sportizebe/global/config/S3Config.javaRepository: Sportize/be
Length of output: 554
클래스 레벨 @Getter로 인한 비밀키 public getter 자동 생성 제거 필요
클래스 레벨 @Getter는 accessKey/secretKey 포함 모든 필드에 대해 public getter를 자동 생성하므로, 의도하지 않은 노출 위험이 있습니다. 필드 레벨에서 선택적으로 getter를 제어하거나 민감정보 필드에 @Getter(AccessLevel.NONE)을 명시하는 구성이 필요합니다.
🔒 제안 수정안
-@Getter
`@Configuration`
public class S3Config {
+ `@Getter`
`@Value`("${spring.cloud.aws.s3.bucket}")
private String bucket; // 버킷 명
+ `@Getter`
`@Value`("${spring.cloud.aws.s3.path.profile}")
private String profile; // 프로필 사진
+ `@Getter`
`@Value`("${spring.cloud.aws.s3.path.club}")
private String club;
+ `@Getter`
`@Value`("${spring.cloud.aws.s3.path.post}")
private String post;
+ `@Getter`(AccessLevel.NONE)
`@Value`("${spring.cloud.aws.credentials.access-key}")
private String accessKey;
+ `@Getter`(AccessLevel.NONE)
`@Value`("${spring.cloud.aws.credentials.secret-key}")
private String secretKey;🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/global/config/S3Config.java` around lines 12
- 32, The class-level `@Getter` on S3Config is exposing sensitive fields; update
the class to avoid public getters for accessKey and secretKey by either removing
the class-level `@Getter` and adding explicit getters only for non-sensitive
fields (bucket, profile, club, post) or keep `@Getter` on the class but add
`@Getter`(AccessLevel.NONE) (Lombok) to the accessKey and secretKey fields so they
do not generate public getters; ensure you reference the S3Config class and the
accessKey and secretKey fields when making the change.
#️⃣ Issue Number
📝 요약(Summary)
S3 설정 관련
게시글 파일 업로드 관련
API 사용법
POST /api/posts/{property}
Content-Type: multipart/form-data
request: {"title": "제목", "content": "내용", "isAnonymous": false} (JSON)
image: [이미지 파일] (선택사항)
동호회 파일 업로드 관련
ClubImageResponse - 새로 생성
ClubController
API 사용법
동호회 생성
POST /api/clubs
Content-Type: multipart/form-data
request: {"name": "축구 동호회", "introduce": "소개", "clubType": "SOCCER", "maxMembers": 20}
image: [이미지 파일] (선택사항)
동호회 사진 수정
POST /api/clubs/{clubId}/image
Content-Type: multipart/form-data
image: [이미지 파일] (선택)
🛠️ PR 유형
어떤 변경 사항이 있나요?
📸스크린샷 (선택)
💬 공유사항 to 리뷰어
✅ PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
Summary by CodeRabbit
릴리스 노트
✏️ Tip: You can customize this high-level summary in your review settings.