-
Notifications
You must be signed in to change notification settings - Fork 0
#8 Store CRUD 및 이미지 업로드 기능 추가 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
#14 이미지 업로드 기능 추가 및 store crud 리팩토링
Walkthrough이번 변경 사항에서는 AWS S3 연동, 비동기 실행, 탄력성 패턴 도입을 위한 의존성 추가와 함께, 매장 이미지 업로드 및 삭제 기능이 새롭게 구현되었습니다. 매장 관련 DTO와 엔티티 구조가 단일 이미지에서 다중 이미지로 확장되었으며, 이에 맞춰 서비스와 컨트롤러 계층도 전반적으로 개선되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant StoreImageController
participant StoreImageService
participant StoreRepository
participant S3Service
participant StoreImageRepository
Client->>StoreImageController: POST /stores/{storeId}/images (files, types)
StoreImageController->>StoreImageService: saveAll(storeId, files, types)
StoreImageService->>StoreRepository: findById(storeId)
StoreRepository-->>StoreImageService: Store
loop 각 파일
StoreImageService->>S3Service: upload(storeId, file)
S3Service-->>StoreImageService: S3UploadResult(key, url)
StoreImageService->>StoreImageRepository: save(StoreImage)
end
StoreImageService-->>StoreImageController: List<StoreImageUploadResponse>
StoreImageController-->>Client: 201 Created + 응답
Client->>StoreImageController: DELETE /stores/image/{imageId}
StoreImageController->>StoreImageService: delete(imageId)
StoreImageService->>StoreImageRepository: findById(imageId)
StoreImageRepository-->>StoreImageService: StoreImage
StoreImageService->>S3Service: delete(fileKey)
StoreImageService->>StoreImageRepository: delete(StoreImage)
StoreImageService-->>StoreImageController: void
StoreImageController-->>Client: 204 No Content
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (4)
src/main/java/com/example/gtable/storeImage/repository/StoreImageRepository.java (1)
11-15: 표준적인 Spring Data JPA 패턴을 잘 따르고 있습니다레포지토리 구현이 매우 깔끔하고 Spring Data JPA의 컨벤션을 잘 따르고 있어요.
findByStore메서드명도 직관적이고 명확합니다.한 가지 추가로 고려해볼 점은 성능 최적화인데요, 스토어 이미지가 많은 경우를 대비해서 다음과 같은 개선을 해보시면 어떨까요?
// 페이징 지원을 위한 메서드 추가 고려 Page<StoreImage> findByStore(Store store, Pageable pageable); // 또는 이미지 개수가 많지 않다면 정렬 추가 List<StoreImage> findByStoreOrderByCreatedAtDesc(Store store);현재 구현도 충분히 좋지만, 향후 확장성을 위해 참고해보시면 좋을 것 같습니다.
src/main/java/com/example/gtable/global/config/AwsS3Config.java (1)
24-31: 타입 캐스팅을 더 명시적인 방법으로 개선할 수 있습니다현재
AmazonS3ClientBuilder.standard().build()의 결과를AmazonS3Client로 캐스팅하고 있는데, 이는 잠재적인 ClassCastException을 유발할 수 있어요.다음과 같이 개선해보시면 어떨까요:
- @Bean - public AmazonS3Client amazonS3Client() { - BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); - return (AmazonS3Client)AmazonS3ClientBuilder.standard() - .withRegion(region) - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .build(); - } + @Bean + public AmazonS3 amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + }이렇게 하면 인터페이스 타입(
AmazonS3)을 반환하여 더 안전하고 유연한 코드가 됩니다.src/main/java/com/example/gtable/storeImage/dto/StoreImageUploadResponse.java (1)
8-13: 프로젝트 DTO 컨벤션과의 일관성을 위해 어노테이션 추가를 고려해보세요코드 구현이 깔끔하고 잘 작성되었습니다. 다만 프로젝트의 다른 DTO들(예:
UserResponseDto)과의 일관성을 위해@RequiredArgsConstructor어노테이션 추가를 고려해보시면 좋겠어요.import lombok.Builder; import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter @Builder +@RequiredArgsConstructor public class StoreImageUploadResponse {이렇게 하면 프로젝트 전체의 DTO 스타일이 더욱 일관되게 유지됩니다.
src/main/java/com/example/gtable/storeImage/model/StoreImage.java (1)
35-42: 컬럼 제약조건을 세밀하게 조정해 주세요.필드 정의는 잘 되어 있지만, 실제 사용 시나리오를 고려한 미세 조정이 필요해 보입니다.
다음과 같은 개선을 고려해 주세요:
-@Column(nullable = false, length = 500) +@Column(nullable = false, length = 2000) // S3 URL이 길어질 수 있음 private String imageUrl; -@Column(nullable = false, length = 500) +@Column(nullable = false, length = 1000) // S3 키도 경로가 깊어질 수 있음 private String fileKey; -@Column(length = 20) +@Column(length = 50) // 이미지 타입이 다양할 수 있음 private String type;또한 type 필드에 대한 enum 값 검증을 추가하시는 것도 좋겠습니다:
@Enumerated(EnumType.STRING) @Column(length = 50) private ImageType type;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
build.gradle(1 hunks)src/main/java/com/example/gtable/global/config/AsyncConfig.java(1 hunks)src/main/java/com/example/gtable/global/config/AwsS3Config.java(1 hunks)src/main/java/com/example/gtable/global/config/SecurityConfig.java(1 hunks)src/main/java/com/example/gtable/global/s3/S3Service.java(1 hunks)src/main/java/com/example/gtable/store/dto/StoreCreateRequest.java(0 hunks)src/main/java/com/example/gtable/store/dto/StoreCreateResponse.java(0 hunks)src/main/java/com/example/gtable/store/dto/StoreReadDto.java(2 hunks)src/main/java/com/example/gtable/store/dto/StoreReadResponse.java(1 hunks)src/main/java/com/example/gtable/store/dto/StoreUpdateRequest.java(0 hunks)src/main/java/com/example/gtable/store/model/Store.java(1 hunks)src/main/java/com/example/gtable/store/service/StoreServiceImpl.java(6 hunks)src/main/java/com/example/gtable/storeImage/controller/StoreImageController.java(1 hunks)src/main/java/com/example/gtable/storeImage/dto/StoreImageUploadResponse.java(1 hunks)src/main/java/com/example/gtable/storeImage/model/StoreImage.java(1 hunks)src/main/java/com/example/gtable/storeImage/repository/StoreImageRepository.java(1 hunks)src/main/java/com/example/gtable/storeImage/service/StoreImageService.java(1 hunks)
💤 Files with no reviewable changes (3)
- src/main/java/com/example/gtable/store/dto/StoreUpdateRequest.java
- src/main/java/com/example/gtable/store/dto/StoreCreateRequest.java
- src/main/java/com/example/gtable/store/dto/StoreCreateResponse.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/com/example/gtable/storeImage/dto/StoreImageUploadResponse.java (1)
src/main/java/com/example/gtable/user/dto/UserResponseDto.java (1)
Builder(9-18)
🔇 Additional comments (26)
src/main/java/com/example/gtable/store/dto/StoreReadResponse.java (1)
17-22: 좋은 리팩토링입니다!
fromEntity에서of로 메서드명을 변경한 것은 훌륭한 개선이에요.of는 정적 팩토리 메서드의 표준적인 네이밍 컨벤션이며, 더 간결하고 명확합니다. Joshua Bloch의 Effective Java에서도 권장하는 패턴이니까 코드의 가독성과 일관성이 향상될 것 같아요.src/main/java/com/example/gtable/global/config/AsyncConfig.java (1)
9-21:Details
✅ Verification successful
S3 업로드를 위한 비동기 설정이 잘 구현되었습니다
Bulkhead 패턴을 적절히 적용하여 동시 이미지 업로드 요청으로 인한 스레드 고갈 문제를 해결하는 좋은 접근입니다. 스레드 이름 프리픽스 설정과 초기화 과정도 올바르게 구현되어 있어요.
다만 현재 설정된 스레드 풀 매개변수들(코어: 5, 최대: 10, 큐: 100)이 실제 운영 환경에서 적절한지 확인이 필요할 것 같습니다.
다음 스크립트로 유사한 설정을 사용하는 다른 프로젝트 사례를 확인해보세요:
🏁 Script executed:
#!/bin/bash # 프로젝트 내 다른 ThreadPoolTaskExecutor 설정 확인 rg -A 10 -B 5 "ThreadPoolTaskExecutor|setCorePoolSize|setMaxPoolSize"Length of output: 1948
AsyncConfig 설정 독립성 확인 및 승인
프로젝트 내 다른 ThreadPoolTaskExecutor 설정을 사용하는 코드는 없으며, 해당 설정이 유일하게 적용되고 있음을 확인했습니다.
스레드 풀 매개변수(corePoolSize=5, maxPoolSize=10, queueCapacity=100)는 S3 업로드 부하 특성에 맞춰 운영 환경에서 모니터링 후 필요 시 조정해 주세요.• 검증 대상 파일:
– src/main/java/com/example/gtable/global/config/AsyncConfig.java해당 구현은 그대로 승인하며, 추가 코드 변경은 필요 없습니다.
src/main/java/com/example/gtable/global/config/AwsS3Config.java (1)
15-22: AWS 자격 증명 관리에 대한 보안 고려사항을 확인해주세요자격 증명을 application.properties에서 직접 주입하는 방식은 개발 환경에서는 괜찮지만, 운영 환경에서는 보안상 취약할 수 있습니다. AWS IAM 역할이나 환경 변수를 통한 자격 증명 관리를 고려해보시는 것이 좋겠어요.
src/main/java/com/example/gtable/storeImage/dto/StoreImageUploadResponse.java (1)
15-21: static factory method 패턴이 잘 적용되었습니다
fromEntity메서드가 엔티티에서 DTO로의 변환을 깔끔하게 처리하고 있어요. 빌더 패턴과 함께 사용하여 가독성과 유지보수성이 좋습니다.src/main/java/com/example/gtable/store/dto/StoreReadDto.java (3)
4-4: 새로운 다중 이미지 지원을 위한 import가 적절히 추가되었습니다단일 이미지에서 다중 이미지 구조로의 전환을 위해 필요한 import들이 정확하게 추가되었어요.
Also applies to: 7-7
22-22: 단일 이미지에서 다중 이미지로의 아키텍처 개선이 잘 반영되었습니다
String storeImageUrl에서List<StoreImageUploadResponse> images로의 변경이 새로운 이미지 관리 시스템의 요구사항을 적절히 반영하고 있습니다. 필드명도 더 명확하고 의미있게 변경되었어요.
27-27: fromEntity 메서드의 시그니처 변경이 논리적으로 일관됩니다다중 이미지 지원을 위해
fromEntity메서드에List<StoreImageUploadResponse> images파라미터를 추가하고, 빌더에서 이를 적절히 설정하는 방식이 매우 깔끔합니다. 기존 단일 이미지 구조에서 다중 이미지 구조로의 전환이 잘 구현되었어요.Also applies to: 37-37
src/main/java/com/example/gtable/global/s3/S3Service.java (2)
18-27: 클래스 설계가 잘 구성되어 있습니다!의존성 주입과 record 사용으로 깔끔한 구조를 만드셨네요. S3UploadResult record는 간결하고 immutable한 데이터 전달 객체로 적절합니다.
54-56: 파일 키 생성 로직이 잘 구현되어 있습니다!UUID 사용으로 파일명 중복을 방지하고, store 별로 디렉토리를 구분하는 것이 좋은 설계입니다.
src/main/java/com/example/gtable/storeImage/controller/StoreImageController.java (2)
21-27: 컨트롤러 구조가 깔끔하게 설계되어 있습니다!@RequestMapping으로 공통 경로를 설정하고 의존성 주입을 적절히 사용하셨네요.
44-55: 삭제 엔드포인트 구현이 적절합니다!HTTP 상태 코드 204 No Content 사용과 표준화된 응답 구조가 REST 규칙에 잘 맞습니다.
src/main/java/com/example/gtable/storeImage/service/StoreImageService.java (2)
20-27: 서비스 클래스 구조가 잘 설계되어 있습니다!필요한 의존성들이 적절히 주입되어 있고, Repository 패턴과 S3Service 연동이 깔끔합니다.
60-67: 삭제 메서드 구현이 적절합니다!엔티티 조회 후 S3와 DB에서 순차적으로 삭제하는 로직이 올바릅니다. @transactional 적용도 적절합니다.
src/main/java/com/example/gtable/storeImage/model/StoreImage.java (2)
20-25: 엔티티 클래스 설계가 훌륭합니다!Lombok 어노테이션 사용과 BaseTimeEntity 상속, 적절한 테이블 명명이 모범적입니다. @SuperBuilder 사용으로 상속 구조에서도 Builder 패턴을 잘 지원하고 있습니다.
27-33: 기본키와 연관관계 설정이 적절합니다!IDENTITY 전략 사용과 지연 로딩 설정이 좋습니다. Store와의 ManyToOne 관계도 올바르게 매핑되어 있습니다.
src/main/java/com/example/gtable/store/model/Store.java (4)
45-46: 생성자 시그니처 변경이 적절합니다.단일 이미지 URL에서 다중 이미지 지원으로 전환하면서 생성자에서
storeImageUrl파라미터를 제거한 것은 올바른 설계 변경입니다. 이미지 관리가 별도의 엔티티로 분리되어 관심사의 분리 원칙을 잘 따르고 있습니다.
57-61: 정보 업데이트 메서드 통합이 우수합니다.개별 setter 메서드들을
updateInfo메서드로 통합한 것은 매우 좋은 설계입니다. 이는 다음과 같은 장점을 제공합니다:
- 원자적 업데이트 보장
- 불변성 원칙 준수
- 도메인 로직의 명확한 캡슐화
메서드명도 의도를 명확히 드러내어 가독성이 뛰어납니다.
63-65: 소프트 삭제 메서드가 명확합니다.
markAsDeleted()메서드는 소프트 삭제 패턴을 명확히 표현하고 있습니다. 메서드명이 의도를 잘 드러내며, 도메인 로직을 엔티티 내부에 캡슐화하여 객체지향 설계 원칙을 잘 따르고 있습니다.
67-73: 상태 관리 메서드들이 잘 설계되었습니다.
activate()와deactivate()메서드는 매장의 활성화 상태를 명확하게 관리할 수 있게 해줍니다. 메서드명이 직관적이며, boolean 값을 직접 설정하는 것보다 의미가 명확합니다. 이러한 접근 방식은 도메인 주도 설계(DDD)의 모범 사례를 따르고 있습니다.src/main/java/com/example/gtable/store/service/StoreServiceImpl.java (7)
15-17: 이미지 관련 의존성 추가가 적절합니다.새로운 이미지 기능을 위한 import 문들이 올바르게 추가되었습니다. 패키지 구조가 명확하고 관심사의 분리가 잘 되어 있어 보입니다.
27-27: StoreImageRepository 의존성 주입이 올바릅니다.
StoreImageRepository를 final 필드로 선언하여 불변성을 보장하고,@RequiredArgsConstructor를 통한 생성자 주입 방식을 사용하는 것이 Spring의 모범 사례를 잘 따르고 있습니다.
56-56: 메서드명 변경이 일관성을 개선했습니다.
fromEntity에서of로 메서드명을 변경한 것은 정적 팩토리 메서드의 일반적인 네이밍 컨벤션을 따르는 좋은 개선입니다.
65-70: 단일 매장 조회에서도 동일한 패턴 적용이 좋습니다.단일 매장 조회에서도 이미지 데이터를 포함하는 일관된 패턴을 적용한 것이 좋습니다. 코드의 일관성과 예측 가능성을 높여줍니다.
79-83: 엔티티의 통합 업데이트 메서드 활용이 우수합니다.
updateInfo메서드를 사용하여 매장 정보를 업데이트하는 것은 엔티티의 캡슐화된 로직을 적절히 활용하는 좋은 예시입니다. 코드가 더 명확하고 유지보수하기 쉬워졌습니다.
87-92: 업데이트 응답에 이미지 포함이 일관성을 높입니다.업데이트 후 응답에도 이미지 데이터를 포함하는 것은 API의 일관성을 높이는 좋은 접근입니다. 클라이언트가 추가 요청 없이 최신 상태를 확인할 수 있어 효율적입니다.
101-101: 명시적인 삭제 메서드 사용이 훌륭합니다.
markAsDeleted()메서드를 사용하여 소프트 삭제를 수행하는 것은 의도를 명확히 표현하는 좋은 방법입니다. 도메인 로직이 엔티티에 잘 캡슐화되어 있습니다.
| @Bulkhead(name = "s3UploadBulkhead", type = Bulkhead.Type.THREADPOOL) | ||
| @Async("s3UploadExecutor") | ||
| public CompletableFuture<S3UploadResult> upload(Long storeId, MultipartFile file) { | ||
| try (InputStream inputStream = file.getInputStream()) { | ||
| String key = createFileKey(storeId, file.getOriginalFilename()); | ||
| ObjectMetadata metadata = new ObjectMetadata(); | ||
| metadata.setContentLength(file.getSize()); | ||
|
|
||
| amazonS3Client.putObject(bucket, key, inputStream, metadata); | ||
| String url = amazonS3Client.getUrl(bucket, key).toString(); | ||
|
|
||
| return CompletableFuture.completedFuture(new S3UploadResult(key, url)); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("S3 업로드 실패", e); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
비동기 업로드 메서드의 에러 처리와 리소스 관리를 개선해 주세요.
Bulkhead 패턴과 비동기 처리 적용은 좋습니다! 하지만 몇 가지 개선이 필요해 보입니다:
- try-with-resources 문에서 예외 발생 시 CompletableFuture.completedFuture 대신 CompletableFuture.failedFuture를 반환해야 합니다.
- 일반적인 RuntimeException보다 구체적인 예외 타입을 사용하는 것이 좋겠습니다.
다음과 같이 개선해 주세요:
@Bulkhead(name = "s3UploadBulkhead", type = Bulkhead.Type.THREADPOOL)
@Async("s3UploadExecutor")
public CompletableFuture<S3UploadResult> upload(Long storeId, MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
String key = createFileKey(storeId, file.getOriginalFilename());
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
amazonS3Client.putObject(bucket, key, inputStream, metadata);
String url = amazonS3Client.getUrl(bucket, key).toString();
return CompletableFuture.completedFuture(new S3UploadResult(key, url));
} catch (Exception e) {
- throw new RuntimeException("S3 업로드 실패", e);
+ return CompletableFuture.failedFuture(new S3UploadException("S3 업로드 실패: " + file.getOriginalFilename(), e));
}
}🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/global/s3/S3Service.java around lines 29 to
44, the asynchronous upload method currently throws a RuntimeException on error
instead of returning a failed CompletableFuture, which is inconsistent with
async error handling. Modify the catch block to return
CompletableFuture.failedFuture with a more specific exception type related to S3
upload failure. This ensures proper asynchronous error propagation and clearer
exception semantics.
src/main/java/com/example/gtable/storeImage/controller/StoreImageController.java
Show resolved
Hide resolved
src/main/java/com/example/gtable/storeImage/service/StoreImageService.java
Show resolved
Hide resolved
| .map(store -> { | ||
| List<StoreImage> images = storeImageRepository.findByStore(store); | ||
| List<StoreImageUploadResponse> imageDto = images.stream() | ||
| .map(StoreImageUploadResponse::fromEntity) | ||
| .toList(); | ||
| return StoreReadDto.fromEntity(store, imageDto); | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
이미지 데이터 통합 로직의 성능 최적화를 고려해보세요.
각 매장마다 개별적으로 이미지를 조회하는 방식은 N+1 쿼리 문제를 야기할 수 있습니다. 매장 수가 많아질 경우 성능에 영향을 줄 수 있습니다.
다음과 같은 개선을 고려해보시기 바랍니다:
// 배치 조회를 위한 개선된 방법
List<Long> storeIds = stores.stream()
.map(Store::getStoreId)
.toList();
Map<Long, List<StoreImage>> imagesByStoreId = storeImageRepository
.findByStoreIdIn(storeIds)
.stream()
.collect(Collectors.groupingBy(image -> image.getStore().getStoreId()));
List<StoreReadDto> storeRead = stores.stream()
.map(store -> {
List<StoreImage> images = imagesByStoreId.getOrDefault(store.getStoreId(), List.of());
List<StoreImageUploadResponse> imageDto = images.stream()
.map(StoreImageUploadResponse::fromEntity)
.toList();
return StoreReadDto.fromEntity(store, imageDto);
})
.toList();🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/store/service/StoreServiceImpl.java around
lines 45 to 51, the current code fetches images for each store individually
causing N+1 query issues. To fix this, first collect all store IDs, then fetch
all images for these stores in a single batch query using a repository method
like findByStoreIdIn. Group the images by store ID into a map, and then when
mapping stores to DTOs, retrieve the images from this map instead of querying
per store. This reduces the number of database queries and improves performance.
#14 S3 관련 dependency 버전 변경 및 이미지 업로드 비동기 처리
작업 요약
Issue Link
#15 #14 #8
문제점 및 어려움
해결 방안
Reference
Summary by CodeRabbit
신규 기능
기능 개선
버그 수정
보안