[Feat] 피드 작성 api 개발 및 S3 관련 설정 및 feed 패키지 구조 리펙토링#85
Conversation
|
""" Walkthrough피드 생성 API가 새로 구현되었습니다. 이를 위해 S3 연동 설정, S3 이미지 업로드/삭제 서비스, 피드 생성 요청·응답 DTO, 도메인 및 JPA 엔티티 확장, 서비스 및 포트/어댑터 계층 구현, 태그·카테고리 검증 및 외부 도서 API 연동 로직이 추가되었습니다. 관련 테스트 코드도 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant FeedCommandController
participant S3Service
participant FeedCreateService
participant FeedCommandPersistenceAdapter
participant RoomCommandPort
participant BookRepository/ExternalAPI
Client->>FeedCommandController: POST /feeds (multipart + JSON)
FeedCommandController->>S3Service: 이미지 파일 S3 업로드 (최대 3개)
S3Service-->>FeedCommandController: 업로드된 이미지 URL 리스트
FeedCommandController->>FeedCreateService: createFeed(command)
FeedCreateService->>RoomCommandPort: 카테고리/태그 검증
FeedCreateService->>BookRepository/ExternalAPI: ISBN으로 도서 조회/저장
FeedCreateService->>FeedCommandPersistenceAdapter: Feed, Content, FeedTag 저장
FeedCommandPersistenceAdapter-->>FeedCreateService: Feed ID 반환
FeedCreateService-->>FeedCommandController: Feed ID 반환
FeedCommandController-->>Client: 응답 (FeedCreateResponse)
Assessment against linked issues
Assessment against linked issues: Out-of-scope changesNo out-of-scope changes detected. Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
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. 🪧 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.
Actionable comments posted: 8
🔭 Outside diff range comments (2)
src/main/java/konkuk/thip/feed/domain/Content.java (1)
11-16:targetPostId명칭이 새 도메인 모델과 맞지 않습니다
Content가 이제 Feed 도메인으로 이동했는데, 여전히targetPostId라는 이름을 사용하고 있습니다.
- 용어 충돌로 오해를 일으킬 수 있고, 추후 리팩터링 시 빠뜨릴 위험이 큽니다.
Feed와의 연관을 명확히 드러내려면feedId(또는targetFeedId) 등으로 변경하는 것을 권장합니다.추가로,
BaseDomainEntity에 이미 PK 필드가 정의돼 있다면private Long id중복 선언 여부도 한번 더 확인해 주세요.src/main/java/konkuk/thip/feed/adapter/out/jpa/ContentJpaEntity.java (1)
5-27: 여전히PostJpaEntity를 참조하고 있어 새로운 Feed 구조와 불일치
ContentJpaEntity가 feed 패키지로 옮겨졌음에도 연관 엔티티가PostJpaEntity로 남아 있습니다.
현재 PR 설명에 따르면 Content-Feed 간 1:N 관계가 필요한 상황이므로, 아래와 같이FeedJpaEntity로 교체해야 합니다.-import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; ... -@JoinColumn(name = "post_id", nullable = false) -private PostJpaEntity postJpaEntity; +@JoinColumn(name = "feed_id", nullable = false) +private FeedJpaEntity feedJpaEntity;아울러 테이블 컬럼 명도
feed_id로 변경해야 schema 불일치가 발생하지 않습니다.
🧹 Nitpick comments (11)
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (1)
5-6: 인터페이스에 간단한 JavaDoc을 추가해 주세요추후 유지보수를 위해 “왜 이 쿼리가 필요한지 / 어떤 제약이 있는지” 정도를 짧게라도 주석으로 남겨두면 좋겠습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (1)
18-29: 중간 변수 없이 바로 반환해 간결성 향상 가능불필요한 지역 변수
userIds를 제거하고 한 줄로 반환하면 코드 가독성이 조금 더 좋아집니다.-Set<Long> userIds = new HashSet<>( - jpaQueryFactory - .select(feed.userJpaEntity.userId) - .distinct() - .from(feed) - .where(feed.bookJpaEntity.bookId.eq(bookId)) - .fetch() -); -return userIds; +return new HashSet<>( + jpaQueryFactory + .select(feed.userJpaEntity.userId) + .distinct() + .from(feed) + .where(feed.bookJpaEntity.bookId.eq(bookId)) + .fetch() +);src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java (1)
26-26: contentList 초기화로 NPE 방지가 잘 구현되었습니다.빈 ArrayList로 초기화하여 null pointer exception을 방지하는 것은 좋은 접근입니다. 다만 성능이 중요한 경우라면 lazy initialization을 고려해볼 수 있습니다.
src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1)
6-7: 포트 인터페이스 디자인이 깔끔합니다.save 메서드의 시그니처가 명확하고 적절합니다. 하지만 프로젝트 컨벤션에 따르면 CommandPort에는 findByXXX 메서드도 포함되어야 합니다. 필요한 경우 Feed 도메인 엔티티를 조회하는 메서드 추가를 고려해보세요.
src/main/java/konkuk/thip/config/S3Config.java (1)
24-33: AWS SDK 및 빌더 패턴 개선현재 구현에서 개선할 수 있는 부분들이 있습니다:
@Bean public AmazonS3Client amazonS3Client() { - //accessKey, secretKey, region 값으로 S3에 접근 가능한 객체 등록 - BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); - - return (AmazonS3Client) AmazonS3ClientBuilder - .standard() - .withRegion(region) - .withCredentials(new AWSStaticCredentialsProvider(credentials)) - .build(); + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); }추가로 예외 처리를 위한 try-catch 블록 추가를 고려해보세요.
src/main/java/konkuk/thip/feed/application/port/in/dto/FeedCreateCommand.java (1)
5-22: 검증 로직 및 문서화 개선 고려Command 객체의 데이터 무결성을 위한 개선을 고려해보세요:
+import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 피드 생성을 위한 Command 객체 + * @param isbn 도서 ISBN + * @param contentBody 피드 내용 + * @param imageUrls 이미지 URL 목록 + * @param isPublic 공개 여부 + * @param category 카테고리 + * @param tagList 태그 목록 + * @param userId 사용자 ID + */ public record FeedCreateCommand( - String isbn, - String contentBody, + @NotBlank String isbn, + @NotBlank String contentBody, List<String> imageUrls, - Boolean isPublic, - String category, + @NotNull Boolean isPublic, + @NotBlank String category, List<String> tagList, - Long userId + @NotNull Long userId ) { }Bean Validation을 사용하여 데이터 무결성을 보장할 수 있습니다.
src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java (1)
9-35: Request DTO 구현이 적절합니다.Record 타입 사용과 validation 어노테이션이 잘 적용되었습니다. 다만,
tagList의 크기 제한이나category의 유효성 검증도 고려해보시면 좋겠습니다.src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java (1)
66-78: 변수명 오타 수정 필요
allowedExtentionList를allowedExtensionList로 수정해주세요.-List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif"); +List<String> allowedExtensionList = Arrays.asList("jpg", "jpeg", "png", "gif"); -if (!allowedExtentionList.contains(extension)) { +if (!allowedExtensionList.contains(extension)) {src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (2)
97-97: TODO: 카테고리별 태그 검증 로직 필요카테고리에 따른 태그 유효성 검증이 누락되어 있습니다.
카테고리별 허용 태그를 검증하는 로직을 구현해드릴까요? 새 이슈를 생성하시겠습니까?
126-126: TODO: 베스트셀러 로직 구현 예정베스트셀러 여부 판단 로직이 추후 구현될 예정임을 확인했습니다.
src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateAPITest.java (1)
94-103: @transactional과 중복되는 수동 cleanup
@Transactional이 적용되어 있어 테스트 후 자동 롤백되므로@AfterEach의 수동 삭제는 불필요해 보입니다.특별한 이유가 없다면
@AfterEach메서드를 제거하는 것을 고려해보세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (41)
build.gradle(1 hunks)src/main/java/konkuk/thip/book/adapter/out/jpa/BookJpaEntity.java(1 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/config/S3Config.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/request/DummyRequest.java(0 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/response/DummyResponse.java(0 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedCreateResponse.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/jpa/ContentJpaEntity.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java(2 hunks)src/main/java/konkuk/thip/feed/adapter/out/jpa/TagJpaEntity.java(0 hunks)src/main/java/konkuk/thip/feed/adapter/out/mapper/ContentMapper.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java(2 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Tag/TagJpaRepository.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/in/DummyUseCase.java(0 hunks)src/main/java/konkuk/thip/feed/application/port/in/FeedCreateUseCase.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/in/dto/DummyCommand.java(0 hunks)src/main/java/konkuk/thip/feed/application/port/in/dto/FeedCreateCommand.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FeedService.java(0 hunks)src/main/java/konkuk/thip/feed/domain/Content.java(1 hunks)src/main/java/konkuk/thip/feed/domain/Feed.java(3 hunks)src/main/java/konkuk/thip/feed/domain/Tag.java(2 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java(1 hunks)src/test/java/konkuk/thip/common/util/TestEntityFactory.java(3 hunks)src/test/java/konkuk/thip/config/TestS3MockConfig.java(1 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateAPITest.java(1 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java(1 hunks)src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java(1 hunks)
💤 Files with no reviewable changes (6)
- src/main/java/konkuk/thip/feed/adapter/in/web/response/DummyResponse.java
- src/main/java/konkuk/thip/feed/adapter/in/web/request/DummyRequest.java
- src/main/java/konkuk/thip/feed/application/service/FeedService.java
- src/main/java/konkuk/thip/feed/application/port/in/dto/DummyCommand.java
- src/main/java/konkuk/thip/feed/application/port/in/DummyUseCase.java
- src/main/java/konkuk/thip/feed/adapter/out/jpa/TagJpaEntity.java
🧰 Additional context used
🧠 Learnings (7)
build.gradle (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/feed/application/port/in/FeedCreateUseCase.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Tag/TagJpaRepository.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
🧬 Code Graph Analysis (1)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateAPITest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(17-168)
🔇 Additional comments (41)
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)
7-8: 패키지 변경 반영 확인 완료리포지토리 패키지 변경에 따른 import 수정이 올바르게 반영되었습니다. 추가 조치 사항 없습니다.
src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java (1)
19-19: 패키지 구조 개선이 잘 적용되었습니다.repository 하위 패키지로 이동하여 persistence 계층의 구조가 더 명확해졌습니다.
src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java (1)
11-11: Import 추가가 적절합니다.ArrayList를 사용하여 contentList를 초기화하기 위한 import 추가가 적절합니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (2)
1-1: 패키지 구조 개선이 적절합니다.repository 하위 패키지로 이동하여 persistence 계층의 구조가 더 체계적으로 정리되었습니다.
6-6: 인터페이스 정의가 표준적이고 적절합니다.JpaRepository와 FeedQueryRepository를 올바르게 상속하고 있습니다.
src/main/java/konkuk/thip/feed/adapter/out/mapper/ContentMapper.java (2)
1-4: 패키지 리팩토링이 적절하게 수행되었습니다.post 패키지에서 feed 패키지로 이동과 관련 import 업데이트가 정확합니다.
11-16: PostJpaEntity 사용 유지: 변경 불필요
- FeedJpaEntity가
extends PostJpaEntity로 선언되어 있어,ContentJpaEntity의
@ManyToOne(fetch = LAZY) private PostJpaEntity postJpaEntity;매핑과 호환됩니다.toJpaEntity(Content, PostJpaEntity)에 FeedJpaEntity 인스턴스를 넘겨도 문제가 없으므로
비즈니스 요구가 “모든 Post 기반 콘텐츠”라면 그대로 두시는 것이 좋습니다.- 만약 “feed 전용” 타입으로 메서드 시그니처를 명확히 제한하고 싶다면,
파라미터 타입만FeedJpaEntity로 변경하는 방안을 검토해 보세요.src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java (2)
3-3: Category 도메인 import 추가가 적절합니다.
12-12: CommandPort 컨벤션을 잘 따르고 있습니다.findCategoryByValue 메서드 추가가 프로젝트의 CQRS 컨벤션에 부합합니다. 메서드 시그니처도 명확하고 적절합니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (1)
4-4: 리포지토리 패키지 경로 업데이트가 적절합니다.패키지 리팩토링에 따른 import 경로 변경이 정확합니다.
src/main/java/konkuk/thip/feed/application/port/in/FeedCreateUseCase.java (1)
1-7: Use Case 인터페이스 설계가 우수합니다.
- 헥사고날 아키텍처의 input port로 적절하게 설계되었습니다
- 메서드 시그니처가 명확하고 커맨드 패턴을 잘 따르고 있습니다
- 최소한의 import로 깔끔한 구조를 유지하고 있습니다
src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedCreateResponse.java (1)
3-6: 잘 구현된 응답 DTO입니다.Record 클래스를 사용한 구현과 정적 팩토리 메서드 패턴이 적절하게 적용되었습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java (1)
6-6: 표준 JPA 리포지토리 구현이 적절합니다.FeedTagJpaEntity에 대한 기본 CRUD 연산을 위한 표준 JPA 리포지토리 인터페이스가 올바르게 구현되었습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java (1)
6-6: 표준 JPA 리포지토리 구현이 적절합니다.ContentJpaEntity에 대한 기본 CRUD 연산을 위한 표준 JPA 리포지토리 인터페이스가 올바르게 구현되었습니다.
src/test/java/konkuk/thip/config/TestS3MockConfig.java (1)
8-26: 테스트용 S3 서비스 Mock 구성이 적절합니다.실제 S3 의존성 없이 테스트할 수 있도록 적절한 Mock 구성이 구현되었습니다. 가짜 URL 반환과 삭제 메서드 stubbing이 테스트 목적에 적합합니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java (1)
51-56: 카테고리 조회 메서드가 적절하게 구현되었습니다.기존 클래스의 패턴을 일관되게 따르고, 적절한 예외 처리와 도메인 객체 변환이 구현되었습니다. CQRS CommandPort 컨벤션에도 부합합니다.
src/main/java/konkuk/thip/feed/domain/Tag.java (2)
16-16: TODAYS_BOOK → TODAY_BOOK 네이밍 변경 안전 확인
rg를 통해 코드베이스에서TODAYS_BOOK사용처가 전혀 발견되지 않아 변경으로 인한 영향이 없습니다. 추가 조치는 필요하지 않습니다.
58-58: 리뷰 코멘트 무시 안내현재
Tag.java에서 발생시키는InvalidStateException(TAG_NAME_NOT_MATCH)는
– 태그 이름 불일치(400 Bad Request)를 나타내는 에러 코드이고,
TAG_NOT_FOUND는
– DB 조회 시 태그 미존재(404 Not Found)를 나타내는 별도 코드입니다.두 에러 코드는 의미와 HTTP 상태가 다르므로, 기존
TAG_NAME_NOT_MATCH를 유지해야 합니다. 해당 변경 제안은 부적절하니 무시해 주세요.Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Tag/TagJpaRepository.java (1)
8-10: Spring Data JPA 모범 사례 준수리포지토리 인터페이스가 올바르게 구현되었습니다:
Optional<TagJpaEntity>반환 타입으로 null 안전성 보장- Spring Data JPA 네이밍 컨벤션 준수
findByValue메서드는 자동으로 생성됨src/test/java/konkuk/thip/common/util/TestEntityFactory.java (2)
78-89: 테스트 팩토리 메서드 추가ISBN을 매개변수로 받는
createBookWithISBN메서드가 적절하게 추가되었습니다. 기존 패턴을 따르고 있으며 테스트에서 특정 ISBN을 가진 책 엔티티가 필요한 경우에 유용합니다.
162-167: 태그 엔티티 팩토리 메서드 추가
createTag메서드가 적절하게 추가되었습니다. 카테고리와 태그 값을 매개변수로 받아TagJpaEntity를 생성하는 구조가 올바르게 구현되었습니다.src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (2)
32-34: Feed-Content 연관관계 설정이 적절합니다.
CascadeType.ALL과orphanRemoval=true설정으로 Feed 엔티티의 생명주기에 따라 Content 엔티티가 적절히 관리됩니다.
35-42: 생성자 구현이 적절합니다.contentList 파라미터가 올바르게 추가되었습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
63-71: Content 저장 로직이 적절합니다.CascadeType.ALL 설정으로 인해 별도의 repository save 호출 없이도 Content 엔티티들이 자동으로 영속화됩니다.
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)
119-133: 에러코드 추가가 적절합니다.태그 및 이미지 파일 관련 에러코드가 체계적으로 추가되었습니다. 에러코드 번호도 기존 체계를 잘 따르고 있습니다.
src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java (2)
52-56: 이미지 개수 검증 로직이 적절합니다.null 체크와 함께 3개 제한을 명확히 검증하고 있습니다.
44-50: 예외 처리 흐름이 잘 설계되었습니다.비즈니스 로직 실패 시 S3에 업로드된 이미지를 삭제하여 고아 파일 발생을 방지하고, 예외를 재전파하여 적절한 에러 응답을 보장합니다.
src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java (3)
43-49: LGTM!파일 유효성 검증 로직이 적절하게 구현되었습니다.
54-61: 적절한 예외 처리IOException을 BusinessException으로 변환하는 처리가 적절합니다.
135-143: URL 파싱 로직 적절함S3 URL에서 키를 추출하는 로직이 올바르게 구현되었습니다.
src/main/java/konkuk/thip/feed/domain/Feed.java (2)
38-52: null 처리 개선 확인tagList의 null 처리가 추가되어 방어적 프로그래밍이 개선되었습니다.
54-62: 안전한 URL 변환 로직null 및 빈 URL 필터링이 적절하게 구현되었습니다.
src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java (4)
26-59: 테스트 구조가 잘 설계됨헬퍼 메서드를 활용한 테스트 코드 재사용이 잘 구현되었습니다.
61-89: 기본 필드 검증 테스트 커버리지 우수필수 필드에 대한 검증 테스트가 잘 작성되었습니다.
91-126: 카테고리/태그 검증 로직 테스트 완성도 높음비즈니스 규칙에 대한 검증 테스트가 포괄적으로 작성되었습니다.
128-165: 이미지 개수 제한 테스트 적절함이미지 업로드 개수 제한에 대한 테스트가 잘 구현되었습니다.
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (3)
35-58: 도메인 주도 설계 원칙이 잘 적용됨애그리거트 루트인 Feed가 Content 생성 책임을 가지도록 구현한 점이 좋습니다.
60-83: 검증 로직이 명확하고 포괄적임XOR 연산을 활용한 카테고리/태그 일관성 검증이 깔끔하게 구현되었습니다.
108-115: 책 조회/생성 로직 적절함기존 책 조회 실패 시 외부 API를 통한 생성 로직이 잘 구현되었습니다.
src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateAPITest.java (2)
106-262: 통합 테스트 시나리오가 포괄적임책 존재 여부, 외부 API 연동, 이미지 업로드 등 다양한 시나리오가 잘 테스트되고 있습니다.
303-393: 태그 매핑 검증 테스트 우수feed-tag 매핑 테이블의 정확한 저장 여부를 검증하는 테스트가 잘 작성되었습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
56-58: 변수명 불일치 수정이 필요합니다.
savedFeed변수를 사용하여 JPA 엔티티를 저장했지만, 이후 메서드 호출에서는feedJpaEntity를 전달하고 있습니다. 일관성을 위해 저장된 엔티티를 사용해야 합니다.// Content가 존재하면 ContentJpaEntity 생성 및 Feed 연관관계 설정 - saveContents(feed, feedJpaEntity); + saveContents(feed, savedFeed); // 태그가 존재하면 태그 피드 매핑 생성 및 저장 - saveFeedTags(feed, feedJpaEntity); + saveFeedTags(feed, savedFeed);
🧹 Nitpick comments (4)
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
85-85: 배치 처리로 성능 개선을 고려해보세요.태그 매핑을 하나씩 저장하는 대신 배치 처리를 사용하면 성능을 향상시킬 수 있습니다.
- feedTagJpaRepository.save(feedTagJpaEntity); + feedTagJpaRepository.saveAll(feedTagJpaEntities);이를 위해 메서드 구조를 다음과 같이 변경할 수 있습니다:
private void saveFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { if (feed.getTagList().isEmpty()) return; - for (Tag tag : feed.getTagList()) { - TagJpaEntity tagJpaEntity = tagJpaRepository.findByValue(tag.getValue()) - .orElseThrow(() -> new EntityNotFoundException(TAG_NOT_FOUND)); - - FeedTagJpaEntity feedTagJpaEntity = FeedTagJpaEntity.builder() - .feedJpaEntity(feedJpaEntity) - .tagJpaEntity(tagJpaEntity) - .build(); - - feedTagJpaRepository.save(feedTagJpaEntity); - } + List<FeedTagJpaEntity> feedTagJpaEntities = feed.getTagList().stream() + .map(tag -> { + TagJpaEntity tagJpaEntity = tagJpaRepository.findByValue(tag.getValue()) + .orElseThrow(() -> new EntityNotFoundException(TAG_NOT_FOUND)); + return FeedTagJpaEntity.builder() + .feedJpaEntity(feedJpaEntity) + .tagJpaEntity(tagJpaEntity) + .build(); + }) + .toList(); + + feedTagJpaRepository.saveAll(feedTagJpaEntities); }src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java (3)
75-75: 오타 수정이 필요합니다.변수명에 오타가 있습니다.
- List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif"); + List<String> allowedExtensionList = Arrays.asList("jpg", "jpeg", "png", "gif");
90-90: 파일명 생성 로직 개선을 제안합니다.현재 UUID의 처음 10자리만 사용하고 있어 중복 가능성이 있습니다. 또한 원본 파일명을 그대로 사용하면 특수문자로 인한 문제가 발생할 수 있습니다.
- String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; + String s3FileName = UUID.randomUUID().toString() + "_" + sanitizeFileName(originalFilename);그리고 파일명 정제 메서드를 추가하세요:
private String sanitizeFileName(String originalFilename) { // 특수문자 제거 및 안전한 파일명 생성 return originalFilename.replaceAll("[^a-zA-Z0-9.-]", "_"); }
45-51: 메서드 구조 개선을 제안합니다.현재
getImageFromUser메서드가 단순히 검증 후uploadImage를 호출하는 구조입니다. 메서드 이름도 더 명확하게 하고 구조를 개선할 수 있습니다.- public String getImageFromUser(MultipartFile image) { + public String uploadUserImage(MultipartFile image) { //입력받은 이미지 파일이 빈 파일인지 검증 if(image.isEmpty() || Objects.isNull(image.getOriginalFilename())){ throw new BusinessException(EMPTY_FILE_EXCEPTION); } + validateImageFileExtension(image.getOriginalFilename()); - return uploadImage(image); + try { + return uploadImageToS3(image); + } catch (IOException e) { + throw new BusinessException(EXCEPTION_ON_IMAGE_UPLOAD); + } }이렇게 하면
uploadImage메서드를 제거하고 로직을 단순화할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java(1 hunks)src/main/java/konkuk/thip/feed/domain/Tag.java(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/konkuk/thip/feed/domain/Tag.java
🧰 Additional context used
🧠 Learnings (2)
src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#85
File: src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java:65-73
Timestamp: 2025-07-16T11:01:16.687Z
Learning: S3Service.deleteImageFromS3() 메서드는 이미 비동기(@Async) 함수로 구현되어 있어서 호출하는 쪽에서 별도의 비동기 처리가 필요하지 않습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
| String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8"); | ||
| return decodingKey.substring(1); // 맨 앞의 '/' 제거 | ||
| }catch (MalformedURLException | UnsupportedEncodingException e){ | ||
| throw new BusinessException(IO_EXCEPTION_ON_IMAGE_DELETE); |
There was a problem hiding this comment.
비동기 메서드에서 예외 처리 개선이 필요합니다.
getKeyFromImageAddress 메서드에서 예외가 발생하면 비동기 메서드(deleteImageFromS3)에서 처리할 수 없습니다. 예외를 로깅하도록 수정해야 합니다.
private String getKeyFromImageAddress(String imageAddress){
try{
URL url = new URL(imageAddress);
String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
return decodingKey.substring(1); // 맨 앞의 '/' 제거
}catch (MalformedURLException | UnsupportedEncodingException e){
- throw new BusinessException(IO_EXCEPTION_ON_IMAGE_DELETE);
+ log.error("Failed to parse image address. URL: {}, Error: {}", imageAddress, e.getMessage(), e);
+ return null; // 또는 적절한 기본값
}
}그리고 deleteImageFromS3 메서드에서 null 체크를 추가해야 합니다:
@Async
public void deleteImageFromS3(String imageAddress){
String key = getKeyFromImageAddress(imageAddress);
+ if (key == null) {
+ log.warn("Cannot delete image due to invalid address: {}", imageAddress);
+ return;
+ }
try{
amazonS3.deleteObject(new DeleteObjectRequest(bucket, key));
}catch (Exception e){
log.error("Failed to delete image from S3. Key: {}, Error: {}", key, e.getMessage(), e);
}
}📝 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.
| throw new BusinessException(IO_EXCEPTION_ON_IMAGE_DELETE); | |
| private String getKeyFromImageAddress(String imageAddress){ | |
| try{ | |
| URL url = new URL(imageAddress); | |
| String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8"); | |
| return decodingKey.substring(1); // 맨 앞의 '/' 제거 | |
| }catch (MalformedURLException | UnsupportedEncodingException e){ | |
| log.error("Failed to parse image address. URL: {}, Error: {}", imageAddress, e.getMessage(), e); | |
| return null; // 또는 적절한 기본값 | |
| } | |
| } | |
| @Async | |
| public void deleteImageFromS3(String imageAddress){ | |
| String key = getKeyFromImageAddress(imageAddress); | |
| if (key == null) { | |
| log.warn("Cannot delete image due to invalid address: {}", imageAddress); | |
| return; | |
| } | |
| try{ | |
| amazonS3.deleteObject(new DeleteObjectRequest(bucket, key)); | |
| }catch (Exception e){ | |
| log.error("Failed to delete image from S3. Key: {}, Error: {}", key, e.getMessage(), e); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java at line 143, the
current code throws a BusinessException directly inside an asynchronous method,
which prevents proper exception handling in deleteImageFromS3. Modify
getKeyFromImageAddress to catch exceptions internally, log the error, and return
null or an optional to indicate failure. Then, in deleteImageFromS3, add a null
check after calling getKeyFromImageAddress to avoid proceeding with a null key
and handle the case gracefully.
buzz0331
left a comment
There was a problem hiding this comment.
거의 3개 분량의 api를 구현하셨네요,, 수고하셨습니다 ㅠㅠ
PR 볼륨이 좀 커서 리뷰가 다소 긴데 검토해주시면 감사하겠습니다!!
src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java
Show resolved
Hide resolved
| @PostMapping("/feeds") | ||
| public BaseResponse<FeedCreateResponse> createFeed(@RequestPart("request") @Valid final FeedCreateRequest request, | ||
| @RequestPart(value = "images", required = false) final List<MultipartFile> images, | ||
| @UserId final Long userId) { | ||
|
|
||
| // 이미지 개수 제한: 3개 초과 입력 시 예외 발생 | ||
| validateImageCount(images); | ||
|
|
||
| //S3에 이미지 업로드 | ||
| List<String> imageUrls = uploadImages(images); | ||
|
|
||
| FeedCreateCommand command = request.toCommand(imageUrls,userId); | ||
|
|
||
| try { | ||
| return BaseResponse.ok(FeedCreateResponse.of(feedCreateUseCase.createFeed(command))); | ||
| } catch (Exception businessException) { | ||
| deleteUploadedImages(imageUrls); // 비즈니스 로직 실패 시 S3에 업로드된 이미지 삭제(고아파일 방지) | ||
| throw businessException; | ||
| } | ||
| } |
There was a problem hiding this comment.
현재 Controller에서 FeedCreateUseCase와 S3Service를 함께 조합하여 사용하는 구조는 기능적으로는 문제가 없지만, 헥사고날 아키텍처 관점에서는 관심사의 분리가 다소 모호해질 수 있어 개선을 제안드리고자 합니다.
S3 업로드/삭제는 외부 시스템과의 통신을 포함한 작업이지만, 현재 코드에서 해당 로직이 모두 S3Service에서 담당하고 있는 상태입니다. 또 “이미지는 최대 3개까지”와 같은 제약은 도메인에 가까운 정책으로 판단되어, 해당 검증을 Service 클래스에서 담당하는게 더 적절하지 않을까 생각됩니다. 이러한 맥락을 고려하여 아래 두 가지 방식 중 하나로 리팩토링을 검토해보면 좋을 것 같습니다.
- FeedCreateService에서 S3CommandPort를 주입받아 이미지 업로드 수행
• Feed에 이미지가 최대 3개라는 제약이 Feed 도메인 고유의 정책이라 판단된다면, 이 제약과 업로드 책임을 FeedCreateService 내에서 처리하는 것이 자연스러울 것 같습니다.
• 이 경우 의존성 주입이 FeedCreateService <- S3CommandPort(ContentCommandPort) <- S3CommandPersistenceAdapter(ContentCommandPersistenceAdapter) 흐름으로 이어집니다.
- 장점
• 피드 생성이라는 비즈니스 흐름이 한 곳에 응집됨
• 이미지 개수 제한 등 도메인 정책을 서비스 내에서 통제 가능
- Controller에서 FeedCreateUseCase와 ContentUploadUseCase를 함께 주입받아 호출
• 이미지 업로드는 피드 생성뿐만 아니라 다른 API들에서도 공통적으로 사용될 가능성이 있고, 이미지 개수 제한도 다른 도메인에도 적용될 수 있는 정책이라고 판단되면 ContentUploadUseCase라는 별도의 유즈케이스로 분리해두는 것도 적절할 것 같습니다.
• 이 경우 Controller는 두 개의 UseCase를 조합하여 orchestrator의 역할을 하게 되며, 코드 재사용성이 높아질 것 같아요.
- 장점
• 이미지 업로드 도메인과 피드 도메인이 명확히 분리됨
• 이미지 검증/업로드 로직을 재사용 가능
두 방식 모두 장단점이 뚜렷하므로,
• “이미지 3개 제한이 Feed 도메인에 속한 정책인지”
혹은
• “다른 도메인에서도 동일한 이미지 제한이 적용될 가능성이 있는지”
에 따라 적절한 방향을 선택하시면 좋을 것 같습니다.
There was a problem hiding this comment.
저도 이미지 처리나 검증 책임이 서비스와 가까운 영역이라는 점에 공감합니다.
실제로 고민 끝에 Controller에서 업로드/검증을 한 이유는 멀티파트 파일 자료형이 infra(어댑터) 계층의 타입이기도 하고
도메인 로직(Service)에는 이미지의 url만 전달하는 것이 의존성 분리가 더 명확해진다고 생각했기 때문에 위와 같이 구현을 했던 거였습니다
다만 의견 주신 대로, 이미지는 실제로 피드(Feed) 도메인에서만 사용하고 있고, 이미지 개수 제한같은 정책이 Feed라는 도메인의 비즈니스 규칙에 해당한다면 해당 책임을 Service(UseCase) 쪽으로 이동시키는 것도 충분히 타당하다고 생각합니다.
ContentUploadUseCase와 같이 이미지 업로드에 특화된 유즈케이스를 별도로 두는 구조도 고려해봤으나,
저는 이미지 업로드를 별도의 UseCase로 분리하기보다는, "이미지 업로드가 포함된 피드 생성" 자체가 피드 도메인의 비즈니스 유즈케이스라고 생각합니다. 즉, 이미지는 피드 생성 시에만 함께 관리되며, 그 정책도 오직 피드(Feed) 도메인에 속해 있습니다.
따라서 헥사고날 아키텍처 관점에서도 FeedCreateService(UseCase)에서 S3CommandPort를 주입받아 이미지 업로드 및 검증을 한 곳에서 담당하도록 구조를 수정하는 것이 도메인 정책을 서비스 계층에서 명확히 통제하고, 계층간 의존성 분리도 더 깔끔하게 이뤄진다고 생각합니다.
좋은 리뷰 감사드리며 위와같이 수정해보록 하겠습니다!🤩
| /** | ||
| * 외부 API(Naver)를 통해 상세 책 정보를 조회하고 Book 도메인으로 저장 | ||
| */ | ||
| private Long saveNewBookWithFromExternalApi(String isbn) { | ||
| NaverDetailBookParseResult detailBookByKeyword = bookApiQueryPort.findDetailBookByIsbn(isbn); | ||
| Book newBook = Book.withoutId( | ||
| detailBookByKeyword.title(), | ||
| detailBookByKeyword.isbn(), | ||
| detailBookByKeyword.author(), | ||
| false, // TODO : 추후 BestSeller 도입 시 로직 수정 | ||
| detailBookByKeyword.publisher(), | ||
| detailBookByKeyword.imageUrl(), | ||
| null, | ||
| detailBookByKeyword.description()); | ||
| return bookCommandPort.save(newBook); | ||
| } |
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (1)
35-40: 도메인 검증 로직의 위치에 대한 이전 리뷰 의견을 고려해주세요.이전 리뷰에서 언급된 대로, 카테고리와 태그 검증 로직을 Feed 도메인 내부로 이동시키는 것을 고려해보세요. 현재 서비스 계층에서 수행하고 있는 검증들은 도메인 규칙에 해당합니다.
🧹 Nitpick comments (3)
src/main/java/konkuk/thip/feed/adapter/out/persistence/S3CommandPersistenceAdapter.java (1)
24-27: 이미지 삭제 실패에 대한 로깅 추가를 고려해보세요.개별 이미지 삭제 실패가 전체 작업을 중단시키지 않도록 하되, 실패한 경우 로깅을 통해 추적할 수 있도록 개선하는 것이 좋겠습니다.
@Override public void deleteImages(List<String> imageUrls) { - imageUrls.forEach(s3Service::deleteImageFromS3); + imageUrls.forEach(url -> { + try { + s3Service.deleteImageFromS3(url); + } catch (Exception e) { + log.warn("Failed to delete image from S3: {}", url, e); + } + }); }src/main/java/konkuk/thip/feed/domain/Feed.java (2)
59-66: 헬퍼 메서드 구현 승인
convertToContentList메서드가 잘 구현되었습니다. null 체크와 빈 문자열 필터링이 적절하며, 스트림 연산을 효과적으로 사용하고 있습니다.Java 16+ 버전을 사용하는 경우
Collectors.toList()대신toList()를 사용할 수 있습니다:- .collect(Collectors.toList()); + .toList();
68-91: 카테고리와 태그 검증 로직 개선 제안검증 로직이 전반적으로 잘 구현되었으나, 몇 가지 개선사항이 있습니다:
- 메서드 분리: 복잡한 검증 로직을 더 작은 메서드로 분리하여 가독성을 높일 수 있습니다.
- 성능 최적화: 중복 검증에서
HashSet을 사용하면 더 효율적입니다.+private static void validateTagDuplication(List<String> tagList) { + if (tagList.size() != new HashSet<>(tagList).size()) { + throw new InvalidStateException(INVALID_FEED_CREATE, + new IllegalArgumentException("태그는 중복 될 수 없습니다.")); + } +}
- 일관된 에러 메시지: 프로젝트 전체에서 에러 메시지 언어가 일관되는지 확인해보세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/S3CommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/in/FeedCreateUseCase.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/in/dto/FeedCreateCommand.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/S3CommandPort.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java(1 hunks)src/main/java/konkuk/thip/feed/domain/Feed.java(3 hunks)src/main/java/konkuk/thip/feed/domain/Tag.java(2 hunks)src/test/java/konkuk/thip/config/TestS3MockConfig.java(1 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/main/java/konkuk/thip/feed/application/port/out/S3CommandPort.java
🚧 Files skipped from review as they are similar to previous changes (6)
- src/main/java/konkuk/thip/feed/domain/Tag.java
- src/test/java/konkuk/thip/config/TestS3MockConfig.java
- src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java
- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
- src/main/java/konkuk/thip/feed/adapter/out/s3/S3Service.java
- src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (2)
src/main/java/konkuk/thip/common/exception/EntityNotFoundException.java (1)
EntityNotFoundException(5-10)src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java (1)
RequiredArgsConstructor(18-123)
🔇 Additional comments (13)
src/main/java/konkuk/thip/feed/application/port/in/FeedCreateUseCase.java (1)
8-10: 깔끔한 인터페이스 정의입니다.유스케이스 인터페이스가 명확하게 정의되어 있으며, 매개변수와 반환 타입이 적절합니다.
src/main/java/konkuk/thip/feed/application/port/in/dto/FeedCreateCommand.java (1)
5-20: 잘 구조화된 커맨드 객체입니다.Record를 사용한 불변 객체로 피드 생성에 필요한 모든 데이터를 포함하고 있습니다.
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (4)
46-49: 이미지 업로드 로직의 null 안전성이 잘 구현되어 있습니다.null 체크와 빈 리스트 처리가 적절히 구현되어 있어 안전합니다.
51-68: S3 이미지 정리 로직이 잘 구현되어 있습니다.피드 생성 실패 시 업로드된 이미지를 정리하는 로직이 적절히 구현되어 있어 리소스 누수를 방지합니다.
85-92: 외부 API 통합 전략이 잘 구성되어 있습니다.로컬 데이터베이스에서 먼저 조회하고, 없을 경우 외부 API를 호출하는 전략이 효율적입니다.
97-109: 예외 처리는 어댑터 계층의 BusinessException으로 이미 처리됨
NaverApiUtil 및 NaverBookXmlParser에서 네트워크 오류, URL 오류, 응답 파싱 오류 등 주요 예외를 BusinessException으로 감싸서 throw하고 있으므로, FeedCreateService에서는 별도의 try–catch 없이 예외가 상위로 전파됩니다.
글로벌 예외 핸들러에서 이 BusinessException들이 적절히 매핑되는지만 확인해주세요. 추가 구현은 필요하지 않습니다.src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java (5)
40-48: 테스트 헬퍼 메서드가 잘 구성되어 있습니다.유효한 요청 데이터를 생성하는 헬퍼 메서드가 명확하고 재사용 가능합니다.
50-72: 테스트 검증 헬퍼 메서드들이 효과적입니다.서로 다른 에러 코드에 대한 별도 검증 메서드를 제공하여 테스트의 명확성을 높였습니다.
74-102: 기본 필드 검증 테스트가 포괄적입니다.필수 필드들에 대한 검증 테스트가 잘 커버되어 있으며, 적절한 에러 메시지를 확인하고 있습니다.
104-139: 비즈니스 규칙 검증 테스트가 철저합니다.카테고리와 태그의 동시 존재 규칙, 태그 개수 제한, 중복 방지 등의 비즈니스 규칙이 잘 테스트되어 있습니다.
141-178: 이미지 업로드 제한 테스트가 적절합니다.이미지 개수 제한에 대한 테스트가 실제 파일 업로드 시나리오를 모방하여 잘 구현되어 있습니다.
src/main/java/konkuk/thip/feed/domain/Feed.java (2)
40-40: 새로운 contentList 필드 추가 승인이미지 콘텐츠를 관리하기 위한
contentList필드 추가가 적절합니다. 기존 코드 스타일과 일관성을 유지하고 있습니다.
42-57: 팩토리 메서드 업데이트 승인
withoutId메서드의 시그니처 변경이 적절합니다. 태그 값과 이미지 URL을 분리하여 처리하는 것이 명확하고, 각각을 적절한 도메인 객체로 변환하는 로직이 잘 구현되었습니다.
| @Override | ||
| public List<String> uploadImages(List<MultipartFile> images) { | ||
| return images.stream() | ||
| .map(s3Service::uploadUserImageAndGetUrl) | ||
| .toList(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
부분 실패 시나리오에 대한 에러 처리 개선이 필요합니다.
현재 구현에서 여러 이미지 중 일부가 업로드 실패할 경우 예외 처리가 부족합니다. 부분 실패 시 이미 업로드된 이미지들에 대한 정리 로직이 필요합니다.
다음과 같이 개선을 제안합니다:
@Override
public List<String> uploadImages(List<MultipartFile> images) {
- return images.stream()
- .map(s3Service::uploadUserImageAndGetUrl)
- .toList();
+ List<String> uploadedUrls = new ArrayList<>();
+ try {
+ for (MultipartFile image : images) {
+ String url = s3Service.uploadUserImageAndGetUrl(image);
+ uploadedUrls.add(url);
+ }
+ return uploadedUrls;
+ } catch (Exception e) {
+ // 부분 업로드된 이미지들 정리
+ if (!uploadedUrls.isEmpty()) {
+ deleteImages(uploadedUrls);
+ }
+ throw e;
+ }
}🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/feed/adapter/out/persistence/S3CommandPersistenceAdapter.java
around lines 17 to 22, the uploadImages method lacks error handling for partial
failures during multiple image uploads. To fix this, wrap the upload calls in a
try-catch block, track successfully uploaded images, and if any upload fails,
delete the already uploaded images to clean up. Then rethrow or handle the
exception appropriately to ensure no orphaned uploads remain.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/feed/domain/Feed.java (1)
92-96: 이미지 개수 검증 메서드 개선 확인이전 리뷰 피드백이 성공적으로 반영되었습니다:
MultipartFile의존성 제거로 도메인 계층 순수성 유지- 간단한
int매개변수 사용으로 프레임워크 독립성 확보- 검증 로직은 명확하고 적절함
Clean Architecture 원칙에 부합하는 개선된 구현입니다.
🧹 Nitpick comments (1)
src/main/java/konkuk/thip/feed/domain/Feed.java (1)
67-90: 카테고리와 태그 검증 로직 승인검증 로직이 포괄적이고 잘 구현되었습니다:
- XOR 로직으로 카테고리/태그 동시 입력 규칙 구현
- 태그 개수 제한 (최대 5개)
- 중복 태그 검증
- 적절한 예외 처리
다만 69번 줄의 주석을 더 명확하게 개선할 수 있습니다:
- // 둘 다 없으면 카테고리도 태그도 없는 새 게시글 (예외 상황 아님) + // 둘 다 없는 경우는 유효함 (카테고리와 태그 없이 피드 작성 가능)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java(1 hunks)src/main/java/konkuk/thip/feed/domain/Feed.java(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java
🔇 Additional comments (3)
src/main/java/konkuk/thip/feed/domain/Feed.java (3)
4-4: 새로운 임포트와 필드 추가 승인도메인 계층에 적절한 의존성들이 추가되었습니다:
InvalidStateException: 비즈니스 규칙 위반 처리에 적합Collectors: 표준 Java 라이브러리 사용ERROR_CODE: 도메인별 오류 코드 상수contentList: 이미지 콘텐츠 관리를 위한 도메인 모델링Also applies to: 10-10, 12-12, 39-39
41-56: 정적 팩토리 메서드 개선 승인메서드 시그니처 변경이 도메인 API를 개선했습니다:
- 프리미티브 타입(
List<String>)을 받아 내부에서 도메인 객체로 변환- 이미지 URL 처리를 위한 새로운 매개변수 추가
- 캡슐화 원칙을 잘 따르고 있음
58-65: 헬퍼 메서드 구현 승인
convertToContentList메서드가 잘 구현되었습니다:
- null 안전성 보장
- 잘못된 URL 필터링
- 함수형 프로그래밍 스타일로 깔끔한 변환
- 적절한 캡슐화 (private)
| * @return S3의 public 이미지 URL | ||
| */ | ||
| public String getImageFromUser(MultipartFile image) { | ||
| public String uploadUserImageAndGetUrl(MultipartFile image) { |
| if (!ALLOWED_EXTENSIONS.contains(extension)) { | ||
| throw new InvalidStateException(INVALID_FILE_EXTENSION); | ||
| } |
#️⃣ 연관된 이슈
📝 작업 내용
📸 스크린샷
💬 리뷰 요구사항
비지니스 에러 발생시, 이미 업로드 된 s3버킷의 객체(이미지) 삭제는 일괄 삭제할지, 지금처럼 예외를 캐치해서 바로 비동기적으로 삭제할지는 상의를 해봐야할것같습니다!
혹시나 빠진 예외처리가 있을수도 있습니다 확인해주시면 감사하겠습니다!
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
Summary by CodeRabbit
신규 기능
버그 수정
개선 및 리팩터링
테스트
기타