Skip to content

[refactor] Content 테이블 삭제 후 Feed에 종속#260

Merged
seongjunnoh merged 19 commits intodevelopfrom
refactor/#251-delete-content-table
Aug 20, 2025
Merged

[refactor] Content 테이블 삭제 후 Feed에 종속#260
seongjunnoh merged 19 commits intodevelopfrom
refactor/#251-delete-content-table

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Aug 18, 2025

#️⃣ 연관된 이슈

closes #251

📝 작업 내용

이미지 url을 담아두던 Content 테이블을 제거하고 Feed 도메인에 완전 종속시켰습니다.
Feed와 Content는 1:N 관계였기 때문에 이를 풀어내기 위해 AttributeConverter를 적용하여 ContentList라는 일급 컬렉션을 JSON 형태로 파싱하여 DB에 저장되도록 하였습니다.

ContentList라는 일급 컬렉션을 도입한 효과는 다음과 같습니다.

  1. Content만의 도메인 규칙 적용 가능 (예를 들어, Feed에 있던 이미지 최대 개수 초과 같은 유효성 검증 로직을 ContentList에서 수행할 수 있습니다.)
  2. 추후에 메타데이터 추가시 유지보수성 굿
  3. 쿼리문 작성시 fetch join 제거
    a. 페이징 처리시 구현 난이도 down
    b. 성능 개선 (N+1 문제 자동 해결)

📸 스크린샷

💬 리뷰 요구사항

현재 FeedQueryRepositoryImpl에 있는 쿼리문의 쿼리 호출 자체는 변경하지 않고 내부적으로 조건만 바꿔둔 상태입니다. 초반에 저희는 Content 테이블이 있다는 전제하에 fetch join + 페이징 처리시 발생하는 문제를 우회하기 위해 다음과 같은 2가지 방법을 사용했었습니다.

  1. 한 페이지에 해당하는 Feed의 id를 정렬 순서대로 조회 -> 정렬된 feedId에 해당하는 Feed들을 조회하여 원하는 필드를 조합 -> 1번 순서대로 재정렬
  2. Content 서브 쿼리를 통해 ','로 구분하여 String으로 반환 -> FeedQueryDto 내부에서 String[]으로 변환

2번에 해당하는 방법은 어느정도 해결되었지만, 1번은 생각보다 쿼리가 복잡해서 바로 해결하지는 못하였습니다. 추후에, 차차 리팩토링을 진행해봐야될 것 같습니다!

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • 신기능

    • 이미지 3개 초과 업로드 시 CONTENT_LIST_SIZE_OVERFLOW 오류 코드와 관련 메시지로 안내합니다.
    • 내 피드 및 팔로우한 공개 피드를 우선 노출하도록 정렬이 개선되었습니다.
  • 버그 수정

    • 피드의 이미지 반환 및 비교 로직이 URL 기반으로 일관화되어 관련 오류가 줄었습니다.
  • 리팩터링

    • 이미지 저장/조회 모델을 단순화해 콘텐츠 관련 저장 경로와 의존성을 정리했습니다.
  • 테스트

    • 콘텐츠 표현 변경에 맞춰 관련 테스트와 검증 기대값을 갱신했습니다.

@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

Content 테이블/엔티티/레포지토리를 제거하고 Feed에 문자열 콘텐츠 리스트를 TEXT(JSON) 칼럼으로 내재화했습니다. ContentList 값객체와 JPA AttributeConverter를 도입하고, 매퍼·쿼리·서비스·도메인·테스트 전반을 ContentList 기반으로 마이그레이션했으며 이미지 개수 초과 에러 코드를 추가했습니다.

Changes

Cohort / File(s) Change summary
Error code
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
CONTENT_LIST_SIZE_OVERFLOW 추가 (BAD_REQUEST, 165000, 메시지 추가).
Content 엔티티/매퍼/레포지토리 제거
src/main/java/konkuk/thip/feed/adapter/out/jpa/ContentJpaEntity.java, src/main/java/konkuk/thip/feed/adapter/out/mapper/ContentMapper.java, src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java
ContentJpaEntity, ContentMapper, ContentJpaRepository 삭제(콘텐츠 별도 엔티티/DAO 제거).
Feed JPA 매핑 전환 + 컨버터 도입
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java, src/main/java/konkuk/thip/feed/adapter/out/jpa/converter/ContentListJsonConverter.java
OneToMany → TEXT(JSON) 컬럼으로 변경. ContentList 필드 도입 및 ContentListJsonConverter로 직렬화/역직렬화. 생성자·updateFrom 반영.
퍼시스턴스 어댑터 수정
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java
Content 관련 리포지토리 의존·저장·삭제 로직 제거. 태그/저장글 처리만 유지.
쿼리·DTO·매퍼 변경
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java, src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java, src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java
GROUP_CONCAT/서브쿼리 제거, Feed.contentList 직접 사용. DTO/매퍼는 ContentList 기반 배열 변환으로 변경. 우선순위 CASE 식 추가.
서비스 변경
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java, src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java
이미지 개수 검증을 ContentList.validateImageCount로 호출하도록 변경. 업데이트 시 URL 비교·삭제 로직을 ContentList 기준으로 단순화.
도메인 모델 변화
src/main/java/konkuk/thip/feed/domain/Feed.java, src/main/java/konkuk/thip/feed/domain/value/ContentList.java
Feed.contentList 타입을 ContentList로 전환. ContentList 값 객체 추가(최대 3개 검증 포함). Feed 변환/소유 검증 로직 갱신.
테스트 수정
src/test/java/.../TestEntityFactory.java, src/test/java/.../feed/adapter/in/web/*ApiTest.java, src/test/java/.../FeedCreateControllerTest.java, src/test/java/.../FeedUpdateControllerTest.java, src/test/java/.../feed/domain/FeedTest.java
테스트에서 ContentJpaRepository/ContentJpaEntity 의존 제거. ContentList 사용으로 팩토리·단언 변경. 과다 이미지 관련 테스트 기대값을 새 에러 코드/메시지로 갱신.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Controller
  participant Service
  participant Repo as FeedCommandPersistenceAdapter
  participant DB as Database

  Client->>Controller: Create/Update Feed (image URLs)
  Controller->>Service: DTO -> Domain(Feed with ContentList)
  Service->>Service: ContentList.validateImageCount()
  Service->>Repo: save/update(Feed)
  Repo->>DB: persist Feed (content_list TEXT via Converter)
  DB-->>Repo: persisted
  Repo-->>Service: saved entity
  Service-->>Controller: Response
  Controller-->>Client: Result
Loading
sequenceDiagram
  autonumber
  participant Client
  participant Controller
  participant QueryRepo as FeedQueryRepositoryImpl
  participant DB as Database

  Client->>Controller: Fetch Feeds
  Controller->>QueryRepo: query feeds
  QueryRepo->>DB: select feeds (content_list)
  DB-->>QueryRepo: rows with content_list
  QueryRepo->>QueryRepo: ContentList JSON -> ContentList 객체
  QueryRepo-->>Controller: DTO(contentUrls[])
  Controller-->>Client: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Content 테이블 및 관련 코드 제거 (#251)
Feed에 문자열 컬럼으로 콘텐츠 내재화 (#251)
AttributeConverter로 리스트 형태로 인식되도록 적용 (#251)
자바 타입을 List으로 유지 (#251) ContentList 래퍼 타입을 도입하여 순수한 List 직접 사용과 차이가 있음.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
이미지 개수 초과 에러 코드 추가 (src/main/java/konkuk/thip/common/exception/code/ErrorCode.java) 이슈는 테이블 제거·내재화 중심이며, 에러 코드 추가는 요구 명시사항에 포함되어 있지 않음.
우선순위 CASE 표현식 추가 (src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java) 쿼리 정렬/우선순위 로직 확장은 콘텐츠 내재화 목적과 직접적 연관이 없는 추가 변경임.

Possibly related PRs

Suggested reviewers

  • hd0rable
  • seongjunnoh

Poem

바삭한 JSON 잎사귀 밟으며 나는 토끼 🐇
세 줄의 URL만 허용해요, 넷은 넘지 마요
컨버터가 살포시 감싸 안아 줄 때
피드 속 문자열들이 춤추네, 찰칵 찰칵
깡총! 코드 들판에 봄이 왔구나

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#251-delete-content-table

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

github-actions bot commented Aug 18, 2025

Test Results

415 tests   415 ✅  31s ⏱️
124 suites    0 💤
124 files      0 ❌

Results for commit 45f78a9.

♻️ This comment has been updated with latest results.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (18)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)

163-167: 메시지 용어 통일 제안: '컨텐츠' → '콘텐츠'

프로젝트 전반의 메시지에서 '콘텐츠' 표기가 다수이며(예: 테스트 메시지), 본 항목만 '컨텐츠'로 표기되어 있습니다. 사용자 메시지 일관성을 위해 용어 통일을 권장합니다.

적용 diff:

-    CONTENT_LIST_SIZE_OVERFLOW(HttpStatus.BAD_REQUEST, 165000, "컨텐츠 리스트의 크기가 초과되었습니다. 최대 3개까지 가능합니다."),
+    CONTENT_LIST_SIZE_OVERFLOW(HttpStatus.BAD_REQUEST, 165000, "콘텐츠 리스트의 크기가 초과되었습니다. 최대 3개까지 가능합니다."),
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (1)

59-64: S3 업로드 중 예외 시 부분 업로드 누수 가능성 점검 권장

현재 imageUrls 변수는 uploadImages가 성공적으로 반환된 후에만 채워지므로, 업로드 과정에서 예외가 발생하면 이미 업로드된 일부 객체를 추적/정리할 수 없습니다. S3 포트에 원자적 업로드 또는 부분 성공 시 반환/롤백 기능을 부여해 누수 가능성을 줄이는 것을 권장합니다.

가능한 대안:

  • S3 포트에 트랜잭션성 제공(업로드 실패 시 내부에서 이미 업로드된 객체를 롤백).
  • 업로드를 개별 단위로 수행하며 성공한 키를 누적, 예외 시 누적 키를 기반으로 삭제 시도.

예시(개념 스케치):

List<String> imageUrls = List.of();
try {
    imageUrls = s3CommandPort.uploadImages(images); // 실패 시 내부적으로 롤백 또는 부분 성공 리스트 제공
    // ...
} catch (Exception e) {
    if (!imageUrls.isEmpty()) {
        s3CommandPort.deleteImages(imageUrls);
    }
    throw e;
}
src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java (1)

28-29: null-safe 매핑은 좋습니다. 단, 도메인/엔티티 간 ContentList 인스턴스 공유를 피하기 위한 방어적 복사 고려

현재 도메인 Feed의 ContentList 인스턴스를 JPA 엔티티에 그대로 주입하면, 동일 객체 참조 공유로 인한 후속 변경 사이드이펙트 가능성이 있습니다. 매핑 시 불변 스냅샷을 기반으로 새 ContentList를 생성해 주입하는 방식이 안전합니다.

아래처럼 방어적 복사를 적용해 주세요.

-                .contentList(feed.getContentList() != null ? feed.getContentList() : ContentList.empty())
+                .contentList(feed.getContentList() != null
+                        ? ContentList.of(feed.getContentList().toUnmodifiableList())
+                        : ContentList.empty())
-                .contentList(feedJpaEntity.getContentList() != null ? feedJpaEntity.getContentList() : ContentList.empty())
+                .contentList(feedJpaEntity.getContentList() != null
+                        ? ContentList.of(feedJpaEntity.getContentList().toUnmodifiableList())
+                        : ContentList.empty())

Also applies to: 46-47

src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java (2)

149-150: 인덱스 기반 단일 요소 비교 대신 전체 일치 검증으로 의도를 명확히

현재 검증은 첫 요소만 확인합니다. 리스트 전체가 기대와 정확히 일치함을 보장하려면 containsExactly 사용이 더 명확합니다.

-        assertThat(updatedFeed.getContentList().get(0)).isEqualTo("https://s3-mock/image-2.jpg");
+        assertThat(updatedFeed.getContentList()).containsExactly("https://s3-mock/image-2.jpg");

196-197: 이미지 검증도 전체 일치 검증으로 통일

위 테스트와 동일하게, 전체 일치 검증이 의도를 더 잘 드러냅니다.

-        assertThat(updated.getContentList().get(0)).isEqualTo("https://s3-mock/image-2.jpg");
+        assertThat(updated.getContentList()).containsExactly("https://s3-mock/image-2.jpg");
src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java (2)

26-29: remainImageUrls 길이 사전 검증은 유용하지만, 중복 검증 최소화 고려

ContentList가 이미 크기 제약을 강제하므로, 서비스 레벨의 동일 검증은 중복일 수 있습니다. 빠른 실패가 목적이면 유지, 단일 책임 원칙을 선호하면 Feed.updateImages 내부(또는 ContentList 생성 시)에만 위임하는 것도 고려해볼 만합니다.


58-64: 이미지 삭제 대상 계산은 업데이트 전 스냅샷 기준으로 수행 필요 (현재 구현은 오동작 소지 + 미사용 메서드)

  • 현재 로직은 feed.getContentList() 기준으로 필터링하는데, 업데이트 이후 호출되면 항상 빈 목록이 되어 S3 삭제가 누락될 수 있습니다.
  • 또한 이 메서드는 현재 호출되지 않습니다. 혼란 방지를 위해 호출부를 추가하거나 메서드를 제거하는 것이 좋습니다.

아래처럼 업데이트 전 스냅샷을 취해 삭제 대상을 계산하도록 변경을 권고합니다.

@@
     private void applyPartialFeedUpdate(Feed feed, FeedUpdateCommand command) {
 
         if (command.remainImageUrls() != null) {
-            feed.updateImages(command.userId(), command.remainImageUrls());
+            // 업데이트 이전 이미지 스냅샷
+            List<String> before = feed.getContentList().toUnmodifiableList();
+            // 도메인 상태 변경
+            feed.updateImages(command.userId(), command.remainImageUrls());
+            // S3 정리(임시). TODO 주석 참고: 이벤트 기반/배치로 이관 시 제거
+            handleFeedImageDelete(before, command.remainImageUrls());
         }
@@
-    private void handleFeedImageDelete(Feed feed, List<String> remainImageUrls) {
-        List<String> toDelete = feed.getContentList().stream()
-                .filter(url -> !remainImageUrls.contains(url))
-                .toList();
+    private void handleFeedImageDelete(List<String> beforeImageUrls, List<String> remainImageUrls) {
+        if (remainImageUrls == null) {
+            return;
+        }
+        List<String> toDelete = beforeImageUrls.stream()
+                .filter(url -> !remainImageUrls.contains(url))
+                .toList();
         if (!toDelete.isEmpty()) {
             s3CommandPort.deleteImages(toDelete);
         }
     }

만약 현재 PR 단계에서 S3 삭제를 의도적으로 보류하는 것이라면, 메서드 자체를 제거하거나 @SuppressWarnings("unused") 및 Javadoc으로 의도를 명시해 혼란을 줄일 것을 제안드립니다.

src/main/java/konkuk/thip/feed/adapter/out/jpa/converter/ContentListJsonConverter.java (2)

19-25: null을 DB에 '[]'로 저장하는 정책, 의도 확인 필요

현재 null ContentList는 DB에 빈 배열("[]")로 직렬화됩니다. 도메인에서 "없음(null)"과 "빈 목록([])"을 동일하게 취급한다면 괜찮지만, 구분이 필요하다면 null을 그대로 저장하는 편이 낫습니다.

     @Override
     public String convertToDatabaseColumn(ContentList attribute) {
         try {
-            List<String> list = attribute == null ? List.of() : attribute.toUnmodifiableList();
-            return objectMapper.writeValueAsString(list);
+            if (attribute == null) {
+                return null;
+            }
+            return objectMapper.writeValueAsString(attribute.toUnmodifiableList());
         } catch (JsonProcessingException e) {
             throw new IllegalStateException("ContentList 직렬화 실패", e);
         }
     }

33-37: 역직렬화 타입 지정 간결화 및 가독성 향상 제안

TypeFactory 대신 TypeReference를 사용하면 가독성이 좋아집니다.

-            List<String> list = objectMapper.readValue(
-                    dbData,
-                    objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)
-            );
+            List<String> list = objectMapper.readValue(
+                    dbData,
+                    new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {}
+            );

또는 import를 추가해 더 간결하게 사용할 수 있습니다:

// 파일 상단 import 추가
import com.fasterxml.jackson.core.type.TypeReference;
src/test/java/konkuk/thip/feed/domain/FeedTest.java (1)

39-41: ContentList 적용 LGTM + 이미지 개수 초과 케이스 테스트 보강 제안

ContentList.of(...) 사용은 적절합니다. 다만, ContentList가 최대 3개 제한을 강제하므로(값 객체 내부 검증), 4개 이상 입력 시 CONTENT_LIST_SIZE_OVERFLOW가 발생하는 도메인 규칙 테스트를 추가해 주세요.

예시 테스트 코드(참고):

@Test
@DisplayName("ContentList: 이미지가 4개 이상이면 InvalidStateException(CONTENT_LIST_SIZE_OVERFLOW)이 발생한다.")
void contentList_size_overflow_throws() {
    assertThrows(InvalidStateException.class, () ->
            Feed.builder()
                .id(1L)
                .creatorId(1L)
                .content("내용")
                .isPublic(true)
                .tagList(List.of())
                .contentList(ContentList.of(List.of("u1","u2","u3","u4")))
                .build()
    );
}
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)

66-68: 코드 정리 및 null-안전성 강화 제안

  • FeedCommandPersistenceAdapter의 레거시 Content 처리용 주석(66–67, 80–81줄)을 삭제해 가독성을 높여주세요.
  • FeedJpaEntity.updateFrom(Feed)에서 contentList를 올바르게 반영하고 있으므로(컨버터 적용 포함), 별도 조치 불필요합니다.
  • applyFeedTags(feed, …) 호출 전에 feed.getTagList()가 null일 수 있으니, 아래 중 하나를 선택해 NPE를 방지해주세요.
    • 도메인 Feed 생성 시 tagList를 항상 빈 리스트로 초기화(@Builder.Default 등)
    • applyFeedTags 진입부에 null 검사 추가:
      List<Tag> tags = feed.getTagList();
      if (tags == null || tags.isEmpty()) return;
      …  
  • 변경 위치
    • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java: 66–67, 80–81줄 주석 삭제
    • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java: applyFeedTags 호출 직전에 null/empty 검증 추가
src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java (2)

41-41: 주석 갱신 필요: GROUP_CONCAT 언급은 현재 구조와 불일치

이제 ContentList(JSON 컬럼 → 컨버터)를 그대로 투영하므로, “GROUP_CONCAT 결과” 주석은 오해의 소지가 있습니다. JSON/컨버터 기반 매핑으로 주석을 업데이트해 주세요.

적용 예:

-            ContentList contentList, // GROUP_CONCAT 결과
+            ContentList contentList, // JSON 컬럼(ContentList) 컨버터 결과

59-66: isPublic null-세이프 처리 권장

레코드 필드는 primitive boolean이므로, 생성자 인자의 Boolean isPublic가 null이면 NPE가 발생합니다. 기본값 false로 안전하게 변환하세요.

적용 diff:

-                isPublic,
+                Boolean.TRUE.equals(isPublic),
src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java (1)

91-97: isSaved 양의 케이스 커버 OK + 음의 케이스 보강 제안

me가 f1을 저장했음을 명시적으로 구성해 isSaved=true를 검증한 점 좋습니다. 보완으로 isSaved=false 케이스(저장하지 않은 사용자 또는 저장하지 않은 피드)에 대한 단일 테스트를 추가하면 회귀 방지에 도움이 됩니다.

원하시면 음의 케이스 테스트 메서드 뼈대를 제공하겠습니다.

src/main/java/konkuk/thip/feed/domain/Feed.java (2)

153-159: updateImages가 ‘추가’를 허용하지 않는 도메인 규칙 확인 필요

현재 구현은 새 이미지 목록을 설정하기 전에 validateOwnsImages(newImageUrls)를 호출하여 “새 목록이 기존 목록의 부분집합”임을 강제합니다. 이는 이미지 ‘추가’를 불허하고 ‘제거/재정렬’만 허용합니다. 요구사항과 일치하는지 확인 부탁드립니다.

  • 만약 ‘추가’도 허용해야 한다면:
    • 추가는 Content 업로드/검증 후에 feed.contentList에 병합하는 별도 유스케이스로 분리하거나,
    • updateImages에서는 max size 검증만 하고 소유 검증은 “제거/교체” 시나리오에 한정하는 방식을 고려해 볼 수 있습니다.
      원하시면 두 가지 유스케이스(추가/정렬·제거)로 메서드를 분리한 예시를 제안드리겠습니다.

161-170: 소유 검증 메시지/필터링 합리적이나 빈 문자열 허용 여부 점검 권장

  • null/blank는 myImageUrls에서 제외되어 후보에 포함되면 예외가 발생합니다. 의도된 정책인지 확인 바랍니다.
  • 에러 메시지에 문제 URL을 포함시키는 것은 디버깅에 유용합니다.

필요 시 빈 문자열/공백 URL은 사전에 필터링하거나, 메시지에 따옴표로 감싸 가독성을 높일 수 있습니다.

src/main/java/konkuk/thip/feed/domain/value/ContentList.java (2)

11-21: 값객체 설계 자체는 적절, 다만 maxSize 동등성 포함 여부는 의도 확인

현재 equals/hashCodecontents만 비교하고 있어, maxSize가 달라도 동등하다고 판단됩니다. 의도된 설계라면 OK이며, 아니라면 maxSize도 비교에 포함해야 합니다.

원하시면 동등성에 maxSize를 포함하는 패치도 제공하겠습니다.


40-44: 공용 정적 검증기의 정책 고정은 OK, 단 JPA 더티체킹 주의

정적 검증기에서 기본 정책(3개)을 노출하는 것은 유틸리티 용도로 합리적입니다. 다만 이 클래스는 가변 컬렉션을 감싸며 JPA AttributeConverter로 저장됩니다. JPA 더티체킹이 인스턴스 내부 변경을 감지하는지(스냅샷 비교 시 equals가 호출되는지) 반드시 확인해 주세요. 필요 시, 엔티티에서는 교체 할당(새 인스턴스 생성) 패턴을 권장합니다.

간단한 통합 테스트 권장 시나리오:

  1. 엔티티 로드 → getContentList().add("x") → flush
  2. SQL UPDATE가 발생하는지 확인

테스트 템플릿 제공 가능합니다. 원하시면 추가해 드릴게요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7967e04 and 7618bae.

📒 Files selected for processing (27)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/jpa/ContentJpaEntity.java (0 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (4 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/jpa/converter/ContentListJsonConverter.java (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/mapper/ContentMapper.java (0 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java (3 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (3 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java (0 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (4 hunks)
  • src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java (2 hunks)
  • src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java (4 hunks)
  • src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (2 hunks)
  • src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java (3 hunks)
  • src/main/java/konkuk/thip/feed/domain/Feed.java (6 hunks)
  • src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1 hunks)
  • src/test/java/konkuk/thip/common/util/TestEntityFactory.java (4 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java (0 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java (0 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java (2 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java (0 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java (0 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java (0 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java (3 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (0 hunks)
  • src/test/java/konkuk/thip/feed/domain/FeedTest.java (3 hunks)
💤 Files with no reviewable changes (9)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedDeleteApiTest.java
  • src/main/java/konkuk/thip/feed/adapter/out/mapper/ContentMapper.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateApiTest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Content/ContentJpaRepository.java
  • src/main/java/konkuk/thip/feed/adapter/out/jpa/ContentJpaEntity.java
🧰 Additional context used
🧬 Code Graph Analysis (9)
src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
src/main/java/konkuk/thip/feed/adapter/out/jpa/converter/ContentListJsonConverter.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
src/main/java/konkuk/thip/feed/domain/Feed.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java (1)
src/main/java/konkuk/thip/feed/domain/value/ContentList.java (1)
  • ContentList (11-133)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (27)
src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java (2)

20-20: 와일드카드 static import 사용: OK

테스트 내 ErrorCode 참조 가독성이 개선되고, 충돌 여지가 낮아 보입니다. 현재 컨텍스트에서는 적절합니다.


156-159: 새 에러코드(CONTENT_LIST_SIZE_OVERFLOW) 기반 검증으로 테스트 의도 명확해짐

하드코딩된 메시지 대신 enum의 코드/메시지를 사용하여 테스트 안정성이 좋아졌습니다.

src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java (1)

36-37: 업로드 전 이미지 개수 도메인 검증으로 fail-fast 달성: 굿

ContentList.validateImageCount 호출로 도메인 규칙과 에러코드가 일관되게 적용됩니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java (1)

183-184: 이미지 개수 초과 검증의 에러코드/메시지 정합성 확보: OK

도메인 규칙과 에러코드를 반영한 단언으로 변경되어 일관성이 좋아졌습니다.

src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java (1)

108-111: ContentList → String[] 매핑 단순화 및 null-safe 처리: 좋습니다

toArray(String[]::new) 사용으로 불필요한 스트림/복사를 피하면서 깔끔하게 변환되었습니다. null 시 빈 배열 반환도 적절합니다.

src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java (1)

8-8: ContentList 도입에 따른 매핑 의존성 정리, 적절합니다

도메인 값 객체(ContentList)를 직접 매핑하도록 정리된 점 좋습니다. null 회피 전략과 일관성 있는 타입 사용에 도움이 됩니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateApiTest.java (1)

40-40: MockMvc patch 정적 임포트 사용 적절합니다

가독성 측면에서 요청 빌더가 간결해집니다. 유지해도 무방합니다.

src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java (1)

8-8: ContentList로 검증 책임을 값 객체로 모은 점 좋습니다

도메인 불변 조건을 값 객체로 축약해 응집도를 높였습니다.

src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)

311-312: 테스트 엔티티에서 ContentList.empty()/of(...) 사용으로 일관성 및 null-안전성 확보, 좋습니다

  • 빈 콘텐츠는 empty(), URL 리스트는 of(...)로 감싸 도메인 제약을 공유합니다.
  • ContentList.of는 내부적으로 null을 빈 리스트로 처리하므로, 테스트 데이터 생성 시 NPE 가능성도 낮아집니다.

Also applies to: 333-334, 357-358

src/test/java/konkuk/thip/feed/domain/FeedTest.java (2)

5-5: 도메인 값 객체(ContentList)로의 테스트 마이그레이션 적절

Feed 도메인 공개 API 변경을 테스트에 잘 반영했습니다. ContentList 사용으로 도메인 규칙을 테스트 레벨에서 명확히 표현할 수 있어 좋습니다.


259-260: 빈 첨부 리스트 기본값 명시 적절

makeFeedWithPublicStatus에서 ContentList.empty()를 명시해 초기 상태를 분명히 한 점 좋습니다. 테스트 가독성과 의도 표현에 도움이 됩니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)

6-9: 와일드카드 제거로 의존성 명시성 향상

구체 import로 변경하여 의존성 경계를 명확히 한 점 좋습니다.

src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java (1)

68-73: ContentList → String[] 변환 구현 깔끔함

null/빈 컬렉션 처리 후 toArray(String[]::new) 사용은 명확하고 효율적입니다.

src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java (3)

7-8: SavedFeed 의존 추가 및 import 정리 적절

  • SavedFeedJpaEntity/Repository 도입으로 isSaved 검증이 자연스럽습니다.
  • Hamcrest 와일드카드 제거도 가독성에 도움 됩니다.

Also applies to: 11-11


56-57: Repository 주입 추가 적절

isSaved 시나리오 구성을 위해 SavedFeedJpaRepository 주입이 필요하며 적절히 반영되어 있습니다.


62-62: 정리 순서에 SavedFeed 포함 OK

테스트 간 간섭 방지를 위해 저장 피드 매핑을 teardown에 포함한 점 좋습니다.

src/main/java/konkuk/thip/feed/domain/Feed.java (3)

46-46: contentList 기본값을 값 객체로 초기화한 점 적절

ContentList.empty()로 초기화하여 불변 규칙을 한 곳에서 보장하도록 한 선택이 바람직합니다.


61-79: 이미지 개수 검증 이관 자연스러움

Feed 레벨의 이미지 개수 검증 주석 처리 후 ContentList 생성으로 위임하는 구조가 일관되고 중복 검증을 제거합니다.


81-84: null → empty 매핑 명확

convertToContentList에서 null 입력을 empty로 처리해 NPE를 방지하는 방식이 안정적입니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (6)

4-7: 명시적 QueryDSL 타입 임포트로 가독성 향상

와일드카드 대신 구체 타입을 임포트하여 의존 타입이 명확해졌습니다. 유지보수에 유리합니다.


12-15: Q타입/엔티티 임포트 정리 적절

컨텍스트에 필요한 Q타입만 명시적으로 임포트했습니다. 쿼리 가독성에 도움 됩니다.


269-279: ContentList → String[] 변환 방식 적절

f.getContentList().toArray(String[]::new) 사용은 타입 안정적이고 성능/가독성 면에서 적절합니다.


435-435: SavedFeed용 프로젝션도 동일한 타입 확인 필요

위와 동일하게 toSavedFeedQueryDto()에서도 feed.contentList를 넘깁니다. 두 경로 간 타입 불일치가 없도록 일관성을 확인해 주세요.

동일 스크립트로 함께 검증 가능합니다.


265-285: Alias 매핑은 항상 존재하므로 null 체크 불필요합니다
JPA 매핑(@JoinColumn(nullable = false))과 DB 제약조건, 그리고 THIP 프로젝트의 1:1 User–Alias 관계(모든 사용자는 반드시 alias 보유)로 인해 f.getUserJpaEntity().getAliasForUserJpaEntity()는 null이 될 수 없습니다. 따라서 제안하신 null 세이프티 코드는 실제로 불필요합니다.

Likely an incorrect or invalid review comment.


361-361: 확인: QFeedQueryDto 생성자 ContentList 파라미터 정합성 확인됨

FeedQueryDto.java의 @QueryProjection 생성자 시그니처를 검토한 결과, String[] contentUrls 대신 ContentList contentList를 파라미터로 받고 있으며 내부에서 convertToArray(contentList)로 변환하고 있어, feed.contentList를 그대로 전달해도 컴파일 및 런타임 매핑 문제가 없습니다. 추가 조치는 불필요합니다.

src/main/java/konkuk/thip/feed/domain/value/ContentList.java (2)

80-99: remove/clear에 대한 검증 생략은 합리적

삭제/비우기에서는 상한 검증이 불필요하므로 현재 구현(검증 미적용)은 타당합니다.


129-133: 불변 스냅샷 제공 API 유용

toUnmodifiableList()로 외부 노출 시 불변 스냅샷을 제공하는 접근이 좋습니다. 위에서 제안한 방어적 복사에도 활용 가능합니다.

Comment on lines +37 to 41
// JSON 문자열로 저장되는 단일 컬럼
@Convert(converter = ContentListJsonConverter.class)
@Column(name = "content_list", columnDefinition = "TEXT", nullable = false)
private ContentList contentList = ContentList.empty();

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

ContentList JSON 컬럼: null 처리와 마이그레이션 확인 필요

  • 컬럼을 nullable = false로 선언하셨는데, 기존 데이터에 NULL이 남아 있다면 로딩 시 contentListnull로 들어와 NPE가 발생할 수 있습니다. AttributeConverter에서 DB NULL/빈 문자열('')에 대해 ContentList.empty()를 반환하도록 보장되는지 확인 부탁드립니다. 또한 마이그레이션 시 NULL을 전부 '[]'로 업데이트하는 데이터 마이그레이션이 필요합니다.
  • columnDefinition = "TEXT"는 MySQL/H2/PostgreSQL에서 일반적으로 호환되지만, Dialect에 따라 길이/인덱싱 전략이 달라질 수 있으므로 대용량(이미지 URL 다수) 환경에서의 쿼리 비용도 고려 바랍니다.

다음 스크립트로 컨버터의 null 처리와 마이그레이션 스크립트 유무를 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# ContentListJsonConverter의 null 처리 확인
fd -t f 'ContentListJsonConverter.java' | xargs -I{} sh -c 'echo "=== {} ==="; sed -n "1,200p" "{}"'

# Flyway/Liquibase 마이그레이션 내 content_list 초기화 여부 확인
rg -n -C2 'content_list|feeds' --type sql

Length of output: 1875


🏁 Script executed:

#!/bin/bash
echo "마이그레이션 스크립트(.sql, .xml, .yml, .yaml)에서 content_list 및 feeds 참조 검색"
rg -nC2 --regexp='content_list' --regexp='feeds' --glob '*.sql' --glob '*.xml' --glob '*.yml' --glob '*.yaml'

Length of output: 397


ContentList JSON 컬럼: Converter 안전 처리 확인 완료 및 기존 데이터 마이그레이션 추가 필요

  • ContentListJsonConverter.convertToEntityAttribute에서
    dbData == null || dbData.isBlank()인 경우 ContentList.empty()를 반환하므로 NPE 위험이 없습니다.
    (확인 위치:
    src/main/java/konkuk/thip/feed/adapter/out/jpa/converter/ContentListJsonConverter.java)

  • 하지만 기존 테이블에 NULL 또는 빈 문자열('')로 저장된 레코드가 있다면, 애플리케이션 업그레이드 후 최초 조회 시 빈 리스트가 아닌 의도치 않은 값이 들어올 수 있으므로 반드시 데이터 마이그레이션을 적용해야 합니다.
    예시 SQL:

    UPDATE feed
       SET content_list = '[]'
     WHERE content_list IS NULL
        OR TRIM(content_list) = '';

    – migration 스크립트( Flyway·Liquibase·직접 SQL 등 )에 위 쿼리를 추가해 주세요.

  • @Column(columnDefinition = "TEXT")는 대부분 DB에서 호환되나, Dialect별로 대용량 TEXT 타입 인덱싱·조회 비용이 달라질 수 있습니다.
    이미지 URL 다수 저장 시 성능 영향이 예상된다면, 별도 프로파일링 또는 별도 저장소 사용을 검토해 보시기 바랍니다.

Comment on lines +51 to 57
public FeedJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Boolean isPublic, int reportCount, BookJpaEntity bookJpaEntity, ContentList contentList) {
super(content, likeCount, commentCount, userJpaEntity);
this.isPublic = isPublic;
this.reportCount = reportCount;
this.bookJpaEntity = bookJpaEntity;
this.contentList = contentList;
if(contentList != null) this.contentList = contentList;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ContentList 참조의 방어적 복사 필요

현재 빌더에서 외부로부터 전달된 ContentList 인스턴스를 그대로 필드에 대입하고 있습니다. 이 인스턴스가 다른 곳에서도 공유/변경될 경우, 영속성 컨텍스트 내부 엔티티의 상태가 의도치 않게 변할 수 있습니다. 값객체는 가급적 방어적 복사를 적용하세요.

적용 제안:

-        if(contentList != null) this.contentList = contentList;
+        if (contentList != null) {
+            // 외부 참조 차단을 위한 방어적 복사
+            this.contentList = ContentList.of(contentList.toUnmodifiableList());
+        }
📝 Committable suggestion

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

Suggested change
public FeedJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity, Boolean isPublic, int reportCount, BookJpaEntity bookJpaEntity, ContentList contentList) {
super(content, likeCount, commentCount, userJpaEntity);
this.isPublic = isPublic;
this.reportCount = reportCount;
this.bookJpaEntity = bookJpaEntity;
this.contentList = contentList;
if(contentList != null) this.contentList = contentList;
}
public FeedJpaEntity(String content,
Integer likeCount,
Integer commentCount,
UserJpaEntity userJpaEntity,
Boolean isPublic,
int reportCount,
BookJpaEntity bookJpaEntity,
ContentList contentList) {
super(content, likeCount, commentCount, userJpaEntity);
this.isPublic = isPublic;
this.reportCount = reportCount;
this.bookJpaEntity = bookJpaEntity;
if (contentList != null) {
// 외부 참조 차단을 위한 방어적 복사
this.contentList = ContentList.of(contentList.toUnmodifiableList());
}
}

Comment on lines +65 to 66
this.contentList = feed.getContentList();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

updateFrom에서도 ContentList 방어적 복사 권장

도메인 Feed로부터 받은 ContentList를 그대로 참조하면 동일한 공유 문제(엔티티 외부에서의 변경 전파)가 발생할 수 있습니다.

적용 제안:

-        this.contentList = feed.getContentList();
+        this.contentList = ContentList.of(feed.getContentList().toUnmodifiableList());
📝 Committable suggestion

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

Suggested change
this.contentList = feed.getContentList();
}
this.contentList = ContentList.of(feed.getContentList().toUnmodifiableList());
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java around
lines 65-66, updateFrom currently assigns this.contentList =
feed.getContentList(); which exposes the internal collection to external
mutation; change this to perform a defensive copy (e.g., assign a new list
populated from feed.getContentList() or use Collections.emptyList() when null)
so the JPA entity holds its own list instance and external changes to the domain
Feed won't mutate the entity's state.

Comment on lines +22 to +27
private ContentList(List<String> values, int maxSize) {
this.maxSize = maxSize;
this.contents = new ArrayList<>(values == null ? List.of() : values);
validate();
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

maxSize 유효성 검증 추가 필요

maxSize가 음수인 경우를 방어하지 않습니다. 도메인 상 한도는 0 이상이어야 하므로 즉시 예외를 던지도록 해주세요.

적용 제안:

-    private ContentList(List<String> values, int maxSize) {
-        this.maxSize = maxSize;
-        this.contents = new ArrayList<>(values == null ? List.of() : values);
-        validate();
-    }
+    private ContentList(List<String> values, int maxSize) {
+        if (maxSize < 0) {
+            throw new IllegalArgumentException("maxSize must be >= 0");
+        }
+        this.maxSize = maxSize;
+        this.contents = new ArrayList<>(values == null ? List.of() : values);
+        validate();
+    }
📝 Committable suggestion

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

Suggested change
private ContentList(List<String> values, int maxSize) {
this.maxSize = maxSize;
this.contents = new ArrayList<>(values == null ? List.of() : values);
validate();
}
private ContentList(List<String> values, int maxSize) {
if (maxSize < 0) {
throw new IllegalArgumentException("maxSize must be >= 0");
}
this.maxSize = maxSize;
this.contents = new ArrayList<>(values == null ? List.of() : values);
validate();
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/feed/domain/value/ContentList.java around lines 22
to 27, add validation to reject negative maxSize: check if maxSize < 0 at the
start of the constructor and immediately throw an IllegalArgumentException (with
a clear message like "maxSize must be non-negative") before assigning fields or
calling validate(), so the domain invariant (maxSize >= 0) is enforced early.

Comment on lines +32 to +34
public static ContentList of(List<String> values, int maxSize) {
return new ContentList(new ArrayList<>(values), maxSize);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

of(values, maxSize)에서 null 입력 NPE 가능성

현재 new ArrayList<>(values)로 즉시 복사하여 values == null일 때 NPE가 발생합니다. 다른 팩토리(of(List))와 동일하게 null-세이프하게 처리하세요.

패치:

-    public static ContentList of(List<String> values, int maxSize) {
-        return new ContentList(new ArrayList<>(values), maxSize);
-    }
+    public static ContentList of(List<String> values, int maxSize) {
+        return new ContentList(values, maxSize);
+    }

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

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/feed/domain/value/ContentList.java around lines 32
to 34, the factory method of(List<String> values, int maxSize) currently calls
new ArrayList<>(values) which throws NPE when values is null; make it null-safe
like the other of(List) by treating a null values as an empty list (e.g., use
values == null ? new ArrayList<>() : new ArrayList<>(values) or
Objects.requireNonNullElseGet(values, ArrayList::new) to create the copy) and
then construct the ContentList with that copy and the provided maxSize.

Comment on lines +46 to +50
private void validate() {
validateImageCount(contents.size());
//todo 필요 시 URL 형식 검증 추가 가능
}

Copy link

@coderabbitai coderabbitai bot Aug 18, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

인스턴스별 maxSize가 실제로 적용되지 않는 버그

validate()에서 DEFAULT_MAX_SIZE를 사용하는 validateImageCount()를 호출하고 있어, 생성자에서 전달한 maxSize가 전혀 반영되지 않습니다. 즉, of(values, maxSize)가 의미가 없어집니다. 인스턴스 필드 maxSize를 사용하도록 수정해야 합니다.

수정 제안:

-    private void validate() {
-        validateImageCount(contents.size());
-        //todo 필요 시 URL 형식 검증 추가 가능
-    }
+    private void validate() {
+        if (contents.size() > maxSize) {
+            throw new InvalidStateException(CONTENT_LIST_SIZE_OVERFLOW);
+        }
+        // todo 필요 시 URL 형식 검증 추가 가능
+    }

참고: 외부에서 기본 정책 검증용으로 validateImageCount(int size)를 계속 사용할 수 있도록 유지하되, 내부 인스턴스 검증은 maxSize를 준수해야 합니다.

📝 Committable suggestion

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

Suggested change
private void validate() {
validateImageCount(contents.size());
//todo 필요 시 URL 형식 검증 추가 가능
}
private void validate() {
if (contents.size() > maxSize) {
throw new InvalidStateException(CONTENT_LIST_SIZE_OVERFLOW);
}
// todo 필요 시 URL 형식 검증 추가 가능
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/feed/domain/value/ContentList.java around lines 46
to 50, the validate() method incorrectly calls
validateImageCount(contents.size()) which uses DEFAULT_MAX_SIZE, so the
instance-level maxSize passed to the constructor is ignored; change validate()
to call the instance field (validateImageCount(contents.size(), this.maxSize) or
call an instance overload) so the object's maxSize is enforced, while keeping
the existing validateImageCount(int size) overload for external default-policy
checks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

뒤 성준님 pr에서 의견 남겨두었습니다! 참고 부탁드릴게여

Copy link

Choose a reason for hiding this comment

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

@buzz0331 네, 알겠습니다! 성준님께서 해당 PR의 피드백을 확인하실 것 같습니다. 팀 간 의견 공유 감사드립니다.


🧠 Learnings used
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#266
File: src/main/java/konkuk/thip/feed/domain/value/ContentList.java:16-21
Timestamp: 2025-08-20T08:03:04.788Z
Learning: ContentList 클래스의 maxSize 필드는 현재 사용되지 않지만 의도적인 설계입니다. 현재는 Feed 생성 시 이미지 업로드 전 유효성 검증을 위해 static validateImageCount 메서드를 사용하며, maxSize는 향후 이미지 업로드 방식(presigned URL 등) 변경 시 사용할 예정입니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (6)
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (6)

367-373: 주석과 구현 불일치: ISBN 검색에서 ‘내 피드 제외’ 조건이 누락되어 있습니다

메서드 주석은 “다른 사용자 공개 피드”를 의도하지만, 현재 구현은 내 공개 피드도 포함됩니다. 요구사항이 “내 피드 제외”라면 아래와 같이 조건을 복구해 주세요. 아니라면 주석을 수정해 의도를 명확히 해주세요.

 private BooleanExpression feedByBooksFilter(String isbn, Long userId) {
   return feed.status.eq(StatusType.ACTIVE)
           .and(feed.bookJpaEntity.isbn.eq(isbn))
-//                .and(feed.userJpaEntity.userId.ne(userId))
-          .and(feed.isPublic.eq(true));
+          .and(feed.userJpaEntity.userId.ne(userId))
+          .and(feed.isPublic.eq(true));
 }

263-283: Alias NPE 가능성과 contentUrls 널 처리 보강

alias 관계가 optional이거나 데이터 정합성 이슈가 있을 경우 NPE가 날 수 있습니다. 또한 contentList가 null일 가능성에 대비해 방어 코드를 추가하는 편이 안전합니다.

 private FeedQueryDto toDto(FeedJpaEntity f, Integer priority) {
   boolean isPriorityFeed = (priority != null && priority == 1);

-  return FeedQueryDto.builder()
+  var aliasEntity = f.getUserJpaEntity().getAliasForUserJpaEntity();
+  return FeedQueryDto.builder()
           .feedId(f.getPostId())
           .creatorId(f.getUserJpaEntity().getUserId())
           .creatorNickname(f.getUserJpaEntity().getNickname())
-          .creatorProfileImageUrl(f.getUserJpaEntity().getAliasForUserJpaEntity().getImageUrl())      // TODO : DB에 String alias 만 저장하면 수정해야함
-          .alias(f.getUserJpaEntity().getAliasForUserJpaEntity().getValue())
+          .creatorProfileImageUrl(aliasEntity != null ? aliasEntity.getImageUrl() : null)      // TODO : DB에 String alias 만 저장하면 수정해야함
+          .alias(aliasEntity != null ? aliasEntity.getValue() : null)
           .createdAt(f.getCreatedAt())
           .isbn(f.getBookJpaEntity().getIsbn())
           .bookTitle(f.getBookJpaEntity().getTitle())
           .bookAuthor(f.getBookJpaEntity().getAuthorName())
           .contentBody(f.getContent())
-          .contentUrls(f.getContentList().toArray(String[]::new))
+          .contentUrls(f.getContentList() != null ? f.getContentList().toArray(String[]::new) : new String[0])
           .likeCount(f.getLikeCount())
           .commentCount(f.getCommentCount())
           .isPublic(f.getIsPublic())
           .isPriorityFeed(isPriorityFeed)
           .build();
 }

346-365: QueryProjection(contentList)과 수동 매핑(contentUrls)의 타입 일관성 확인 필요

toQueryDto/toSavedFeedQueryDto에서는 feed.contentList(List/값객체)를 그대로 투영하고, toDto에서는 String[]로 변환합니다. FeedQueryDto의 생성자(@QueryProjection) 파라미터 타입과 빌더 세터 타입이 일치하는지 확인해 주세요. 현재처럼 이질적인 타입이 공존하면 직렬화/문서화와 프론트 소비 측면에서 혼란이 생길 수 있습니다.

권장:

  • 도메인/DTO에서 content를 일관되게 List(또는 ContentList 값객체)로 유지하고, 컨트롤러 계층에서만 필요 시 배열로 변환.
  • 또는 배열을 표준으로 삼을 경우, QueryProjection에서도 feed.contentList → 배열 변환을 적용하여 타입을 맞추기.

Also applies to: 420-439


114-148: Keyset 페이징 안정성: 동률(createdAt/priority)에서 중복/누락 가능성. 3차 정렬 키(postId) 추가 권장

현재 ORDER BY는 priority DESC, createdAt DESC만 사용합니다. 동일 priority/createdAt 레코드가 많은 경우 페이지 경계에서 중복/누락이 발생할 수 있습니다. 안정적인 정렬을 위해 postId DESC를 3차 키로 추가하는 것을 권장합니다. 커서에도 3차 키를 반영하면 가장 견고합니다.

최소 변경(정렬 안정성 향상):

-        .orderBy(priority.desc(), feed.createdAt.desc())
+        .orderBy(priority.desc(), feed.createdAt.desc(), feed.postId.desc())

권장(완전한 keyset): lastPostId 커서를 추가하고 비교식을 3키로 확장

- private List<Tuple> fetchFeedIdsAndPriorityByFollowingPriority(Long userId, Integer lastPriority, LocalDateTime lastCreatedAt, int size) {
+ private List<Tuple> fetchFeedIdsAndPriorityByFollowingPriority(Long userId, Integer lastPriority, LocalDateTime lastCreatedAt, Long lastPostId, int size) {
   NumberExpression<Integer> priority = new CaseBuilder()
     ...
-  BooleanExpression cursorCondition = (lastPriority != null && lastCreatedAt != null)
-      ? priority.lt(lastPriority)
-        .or(priority.eq(lastPriority)
-              .and(feed.createdAt.lt(lastCreatedAt)))
-      : Expressions.TRUE;
+  BooleanExpression cursorCondition =
+      (lastPriority != null && lastCreatedAt != null && lastPostId != null)
+        ? priority.lt(lastPriority)
+            .or(priority.eq(lastPriority)
+                .and(
+                    feed.createdAt.lt(lastCreatedAt)
+                    .or(feed.createdAt.eq(lastCreatedAt).and(feed.postId.lt(lastPostId)))
+                ))
+        : Expressions.TRUE;
 
-  return jpaQueryFactory
+  return jpaQueryFactory
       .select(feed.postId, priority)
       ...
-      .orderBy(priority.desc(), feed.createdAt.desc())
+      .orderBy(priority.desc(), feed.createdAt.desc(), feed.postId.desc())
       .limit(size + 1)
       .fetch();
 }

추가로, following을 LEFT JOIN하는 현재 방식은 중복 가능성이 있습니다(UNIQUE 제약이 없을 경우). EXISTS 서브쿼리로 바꾸면 더 단정해집니다(선택).

.when(
  JPAExpressions.selectOne().from(following)
    .where(following.userJpaEntity.userId.eq(userId)
      .and(following.followingUserJpaEntity.userId.eq(feed.userJpaEntity.userId)))
    .exists()
  .and(feed.isPublic.isTrue())
).then(1)

79-86: 순서 보존 시 null 엔티티 안전성 보강 및 priorityMap 접근 NPE 방지

entityMap.get(id)가 null을 반환할 가능성(동시성으로 삭제되었거나 상태가 바뀐 경우)에 대비해 필터링이 필요합니다. 또한 priorityMap.get(e.getPostId())는 null일 수 있으므로 getOrDefault가 더 안전합니다.

-    List<FeedJpaEntity> ordered = feedIds.stream()
-            .map(entityMap::get)
-            .toList();
+    List<FeedJpaEntity> ordered = feedIds.stream()
+            .map(entityMap::get)
+            .filter(java.util.Objects::nonNull)
+            .toList();

-    return ordered.stream()
-            .map(e -> toDto(e, priorityMap.get(e.getPostId())))
-            .toList();
+    return ordered.stream()
+            .map(e -> toDto(e, priorityMap.getOrDefault(e.getPostId(), null)))
+            .toList();

같은 패턴을 사용하는 곳에도 동일 적용 권장:

  • Line 101-109 (findLatestFeedsByCreatedAt)
  • Line 191-199 (findMyFeedsByCreatedAt)
  • Line 211-218 (findSpecificUserFeedsByCreatedAt)

247-261: 파라미터 명 혼동: userId → feedOwnerId로 일관성 향상

메서드 이름은 SpecificUser지만 파라미터 명은 userId라 혼동 여지가 있습니다. 가독성을 위해 feedOwnerId로 변경을 제안합니다(호출부도 같이 정리).

-private List<Long> fetchSpecificUserFeedIdsByCreatedAt(Long userId, LocalDateTime lastCreatedAt, int size) {
+private List<Long> fetchSpecificUserFeedIdsByCreatedAt(Long feedOwnerId, LocalDateTime lastCreatedAt, int size) {
   return jpaQueryFactory
       .select(feed.postId)
       .from(feed)
       .where(
           feed.status.eq(StatusType.ACTIVE),
-          feed.userJpaEntity.userId.eq(userId),
+          feed.userJpaEntity.userId.eq(feedOwnerId),
           feed.isPublic.eq(Boolean.TRUE),
           lastCreatedAt != null ? feed.createdAt.lt(lastCreatedAt) : Expressions.TRUE
       )
       .orderBy(feed.createdAt.desc())
       .limit(size + 1)
       .fetch();
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f4f7cd6 and e1d7439.

📒 Files selected for processing (2)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (1)

59-87: size + 1 반환 정책 확인

여러 메서드에서 limit(size + 1)로 조회한 결과를 그대로 반환합니다. 상위 계층이 “hasNext” 판단을 위해 size+1 목록을 기대한다면 OK입니다. 만약 API 응답 규격상 결과 리스트는 정확히 size개여야 한다면, 리포지토리 계층에서 슬라이싱하거나 서비스 계층에서 슬라이싱이 보장되어야 합니다. 현재 컨트랙트를 사용하는 서비스/컨트롤러의 기대치를 한번 확인해 주세요.

Also applies to: 96-109, 183-199, 202-218, 306-315, 335-344, 405-415

@seongjunnoh seongjunnoh merged commit cd9149b into develop Aug 20, 2025
4 checks passed
@buzz0331 buzz0331 deleted the refactor/#251-delete-content-table branch August 22, 2025 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-297] [refactor] Content 테이블 삭제 후 Feed에 종속

2 participants