Conversation
Walkthrough이번 변경에서는 전체 피드 조회 API의 도입 및 관련 비즈니스 로직, 매퍼, 포트, 어댑터, 테스트 코드가 대규모로 추가되었습니다. 피드 조회는 기본, 팔로잉 우선, 개인화 전략별로 분리되어 서비스화되었으며, 커서 기반 페이징 및 피드 저장/좋아요 상태도 함께 반환하도록 구현되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant FeedQueryController
participant FeedShowAllUseCase (Service)
participant FeedQueryPort
participant SavedQueryPort
participant PostLikeQueryPort
Client->>FeedQueryController: GET /feeds?cursor=...
FeedQueryController->>FeedShowAllUseCase: showAllFeeds(userId, cursor)
FeedShowAllUseCase->>FeedQueryPort: findLatestFeedsByCreatedAt/findFeedsByFollowingPriority(...)
FeedShowAllUseCase->>SavedQueryPort: findSavedFeedIdsByUserIdAndFeedIds(...)
FeedShowAllUseCase->>PostLikeQueryPort: findPostIdsLikedByUser(...)
FeedShowAllUseCase->>FeedQueryMapper: toFeedShowAllResponse(...)
FeedShowAllUseCase-->>FeedQueryController: FeedShowAllResponse
FeedQueryController-->>Client: BaseResponse<FeedShowAllResponse>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Suggested labels
Suggested reviewers
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (7)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (6)
⏰ 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)
✨ Finishing Touches
🧪 Generate unit tests
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: 1
🧹 Nitpick comments (3)
src/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.java (1)
31-31: 페이지 크기를 설정 가능하게 만드는 것을 고려해보세요현재
PAGE_SIZE가 하드코딩되어 있습니다. 운영 환경에서 성능 튜닝이나 요구사항 변경에 대응하기 위해 설정 파일로 외부화하는 것을 권장합니다.-private static final int PAGE_SIZE = 10; +@Value("${feed.page.size:10}") +private int pageSize;src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java (1)
30-30: 페이지 크기를 설정 가능하게 만드는 것을 고려해보세요.현재
PAGE_SIZE가 하드코딩되어 있습니다. 향후 유연성을 위해 설정 파일에서 관리하는 것을 고려해보세요.-private static final int PAGE_SIZE = 10; +@Value("${feed.page.size:10}") +private int pageSize;그리고 사용하는 곳에서:
-CursorBasedList<FeedQueryDto> result = feedQueryPort.findFeedsByFollowingPriority(userId, cursorVal, PAGE_SIZE); +CursorBasedList<FeedQueryDto> result = feedQueryPort.findFeedsByFollowingPriority(userId, cursorVal, pageSize);src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java (1)
44-85: 테스트 코드 중복을 줄이는 리팩토링을 고려해보세요.
FollowingPriorityFeedShowAllApiTest와 대부분의 설정 코드가 중복됩니다. 공통 부모 클래스를 만들어 중복을 제거할 수 있습니다.예시:
@AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") public abstract class BaseFeedShowAllApiTest { @Autowired protected MockMvc mockMvc; // 모든 repository 필드들... @AfterEach void tearDown() { // 공통 정리 로직 } // 공통 헬퍼 메소드들 }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
build.gradle(1 hunks)src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java(2 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedShowAllResponse.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java(3 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java(2 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(2 hunks)src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/in/FeedShowAllUseCase.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/in/dto/DummyQuery.java(0 hunks)src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/PersonalizedFeedShowAllService.java(1 hunks)src/test/java/konkuk/thip/common/util/TestEntityFactory.java(2 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java(1 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java(1 hunks)
💤 Files with no reviewable changes (1)
- src/main/java/konkuk/thip/feed/application/port/in/dto/DummyQuery.java
🧰 Additional context used
🧠 Learnings (4)
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (1)
Learnt from: seongjunnoh
PR: #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/mapper/FeedQueryMapper.java (1)
Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: #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/out/FeedQueryPort.java (1)
Learnt from: seongjunnoh
PR: #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 (5)
src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
DateUtil(12-62)
src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
DateUtil(12-62)
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(27-271)
src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(27-271)
src/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
DateUtil(12-62)
🪛 GitHub Actions: CI with Gradle
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java
[error] 366-366: Test failure: java.lang.AssertionError at line 366 in FollowingPriorityFeedShowAllApiTest.java during API integration test for feed retrieval with cursor parameter.
🔇 Additional comments (31)
build.gradle (1)
72-73: 의존성 추가가 적절합니다.테스트 가시성을 위한 Guava 의존성이 올바르게 추가되었습니다.
compileOnly스코프 사용이 적절하며,@VisibleForTesting어노테이션 사용 목적이 명확합니다.src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java (2)
8-8: 적절한 import 추가입니다.테스트 가시성 어노테이션을 위한 Guava import가 올바르게 추가되었습니다.
43-46: 테스트를 위한 setter 메서드가 적절합니다.
@VisibleForTesting어노테이션과protected접근 제한자를 사용하여 테스트 목적임을 명확히 하고, 캡슐화를 최대한 유지했습니다. 통합 테스트에서 타임스탬프 제어가 필요한 상황에 적합한 구현입니다.src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java (3)
4-4: 적절한 import 추가입니다.테스트 가시성 어노테이션을 위한 import가 올바르게 추가되었습니다.
15-15: 필요한 타입 import가 추가되었습니다.
LocalDateTimeimport가 새로운 setter 메서드를 위해 적절히 추가되었습니다.
55-58: 테스트용 setter 메서드가 올바르게 구현되었습니다.부모 클래스의 protected 메서드에 위임하는 패턴이 적절하며,
@VisibleForTesting어노테이션으로 테스트 목적임을 명확히 표시했습니다. 통합 테스트에서 피드 생성 시간 제어를 위한 합리적인 구현입니다.src/main/java/konkuk/thip/feed/application/port/in/FeedShowAllUseCase.java (2)
5-8: Use Case 인터페이스 설계가 명확합니다.메서드 시그니처가 명확하고 커서 기반 페이지네이션을 적절히 지원합니다. 사용자 ID와 커서 매개변수는 피드 조회 기능에 적합합니다.
3-3: 아키텍처 레이어 분리 검토가 필요합니다.애플리케이션 레이어(
application.port.in)에서 웹 어댑터 레이어(adapter.in.web.response)의 응답 타입을 직접 참조하고 있습니다. 클린 아키텍처 원칙에 따르면 애플리케이션 레이어는 어댑터 레이어에 의존하지 않아야 합니다.
FeedShowAllResponse를 애플리케이션 레이어의 DTO로 이동하거나, 별도의 응답 모델을 애플리케이션 레이어에 정의하는 것을 고려해보세요.⛔ Skipped due to learnings
Learnt from: buzz0331 PR: THIP-TextHip/THIP-Server#78 File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3 Timestamp: 2025-07-14T18:22:56.538Z Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.src/main/java/konkuk/thip/feed/adapter/in/web/FeedQueryController.java (3)
3-6: 필요한 import들이 올바르게 추가되었습니다.새로운 피드 조회 기능을 위한 모든 필수 import가 적절히 추가되었습니다.
16-16: 의존성 주입이 올바르게 구현되었습니다.Use Case 인터페이스를 통한 의존성 주입이 적절하며, 다양한 구현체 전략을 지원할 수 있는 구조입니다.
18-23: REST API 엔드포인트가 잘 설계되었습니다.
- RESTful 규칙을 따르는
/feeds경로 사용@UserId어노테이션을 통한 사용자 인증 처리- 선택적 커서 매개변수로 페이지네이션 지원
- 표준화된
BaseResponse래퍼 사용커서 기반 페이지네이션을 적절히 지원하는 깔끔한 API 설계입니다.
src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java (2)
10-14: MapStruct 설정이 적절합니다
unmappedTargetPolicy = ReportingPolicy.IGNORE를 명시적으로 설정하여 매핑되지 않은 필드를 무시하도록 한 것이 좋습니다. 또한DateUtil클래스를 imports에 포함하여 expression에서 사용할 수 있도록 한 것도 올바른 접근입니다.
17-21: 날짜 포맷팅 로직이 올바릅니다
DateUtil.formatBeforeTime()을 사용하여createdAt을 상대적 시간 문자열로 변환하는 것이 적절합니다. 사용자에게 "N분 전", "N시간 전" 형태로 표시되어 UX 관점에서 유용합니다.src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java (1)
13-15: 커서 기반 페이지네이션 인터페이스 설계가 우수합니다두 메서드 모두 일관된 시그니처를 가지고 있으며,
LocalDateTime을 커서 값으로 사용하는 것이 시간 기반 정렬에 적합합니다.CursorBasedList<FeedQueryDto>반환 타입을 통해 데이터와 페이지네이션 정보를 함께 제공하는 것도 좋은 설계입니다.메서드명이 명확하여 각각의 역할을 쉽게 이해할 수 있습니다:
findFeedsByFollowingPriority: 팔로잉 우선순위 기반 조회findLatestFeedsByCreatedAt: 최신순 기반 조회src/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.java (1)
35-54: 피드 조회 로직이 잘 구현되었습니다커서 기반 페이지네이션 로직이 올바르게 구현되어 있습니다:
- 커서 파싱에서 null과 빈 문자열을 적절히 처리
@Transactional(readOnly = true)어노테이션으로 읽기 전용 트랜잭션 설정- DTO → Response 매핑이 깔끔하게 구현됨
- 페이지네이션 메타데이터(nextCursor, isLast)를 정확히 제공
DateUtil.parseDateTime()에서 파싱 실패 시 적절한 예외(InvalidStateException)를 던지므로 에러 핸들링도 잘 되어 있습니다.src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java (1)
5-21: 종합적이고 잘 설계된 DTO입니다
FeedQueryDto레코드가 피드 조회에 필요한 모든 정보를 포괄적으로 담고 있습니다:
- 사용자 정보:
creatorId,creatorNickname,creatorProfileImageUrl,alias- 컨텐츠 정보:
contentBody,contentUrls, 타임스탬프- 도서 정보:
isbn,bookTitle,bookAuthor- 상호작용 정보:
likeCount,commentCount,isSaved,isLiked필드명이 명확하고 타입 선택이 적절합니다. 불변 객체인 Java record를 사용한 것도 데이터 전송 객체로서 올바른 선택입니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (1)
12-14: 인터페이스 설계가 잘 구현되었습니다.CQRS QueryPort 패턴을 올바르게 따르고 있으며, 메서드명이 명확하고 커서 기반 페이지네이션을 위한 파라미터가 적절히 설계되었습니다.
src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java (2)
19-22: 전략 패턴 구현이 적절합니다.
@ConditionalOnProperty를 사용한 전략 패턴 구현이 깔끔하고, YAML 설정을 통한 런타임 전략 선택이 가능하도록 잘 설계되었습니다.
38-38: 커서 파싱 로직이 안전하게 구현되었습니다.
DateUtil.parseDateTime을 사용하여 커서를 파싱하는 로직이 null 체크와 빈 문자열 체크를 포함하여 안전하게 구현되었습니다.DateUtil.parseDateTime에서 파싱 실패 시 적절한 예외를 던지는 것도 확인했습니다.src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (2)
27-32: 어댑터 구현이 올바르게 되었습니다.
FeedQueryPort인터페이스를 적절히 구현하고 있으며,CursorBasedList.of()팩토리 메서드를 사용하여 커서 기반 리스트로 변환하는 로직이 깔끔합니다.
34-39: 일관된 구현 패턴을 따르고 있습니다.두 메서드 모두 동일한 패턴으로 구현되어 있어 일관성이 좋고,
createdAt.toString()을 커서 키로 사용하는 것이 적절합니다.src/test/java/konkuk/thip/common/util/TestEntityFactory.java (2)
75-83: 테스트 유틸리티 확장이 적절합니다.기존
createUser메서드를 오버로드하여 닉네임을 지정할 수 있도록 한 것이 테스트의 유연성을 높입니다. 기존 패턴을 잘 따르고 있습니다.
235-263: 피드 생성 팩토리 메서드가 잘 설계되었습니다.
likeCount,commentCount,imageUrls파라미터를 추가하여 더 다양한 테스트 시나리오를 지원할 수 있도록 확장되었습니다.ContentJpaEntity매핑 로직도 적절합니다.src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedShowAllResponse.java (2)
5-9: 응답 DTO 설계가 잘 되었습니다.커서 기반 페이지네이션에 필요한
nextCursor와isLast필드를 포함하여 적절하게 설계되었습니다. Java Record 사용으로 불변성과 간결성을 확보했습니다.
10-26: 중첩된 Feed 레코드가 포괄적으로 설계되었습니다.피드의 모든 필수 정보(생성자 정보, 도서 정보, 콘텐츠, 참여 지표 등)를 포함하고 있으며,
contentUrls를 배열로 처리한 것이 JSON 직렬화에 적합합니다.src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (2)
75-85: 테스트 데이터 정리 순서가 적절합니다.참조 무결성을 고려하여 종속 엔티티부터 삭제하는 순서가 올바르게 구현되었습니다.
359-374: 커서 파싱 및 쿼리 조건 정상 확인
DateUtil.parseDateTime은ISO_LOCAL_DATE_TIME포맷으로LocalDateTime.toString()과 완벽히 일치하며,
FeedQueryRepositoryImpl의feed.createdAt.lt(cursorVal)조건도 의도한 대로 이전(older) 데이터를 조회합니다.따라서 테스트 실패 원인은 페이징 로직 자체가 아니라 다음 중 하나일 가능성이 높습니다:
- 테스트 데이터(f10, f11, f12)의
createdAt설정 순서 및 값- 쿼리 결과 정렬 순서(
priority.desc(), feed.createdAt.desc())CursorBasedList를FeedShowAllResponse로 변환하면서 계산되는nextCursor/isLast로직위 항목들을 확인해 보시기 바랍니다.
src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java (1)
199-212: 기본 정렬 전략이 올바르게 테스트되고 있습니다.팔로잉 우선순위 없이 순수하게 최신순으로 정렬되는 것을 확인하는 테스트가 적절히 구현되었습니다.
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (3)
35-42: Q클래스를 필드로 선언한 것이 좋습니다.메소드마다 Q클래스를 재생성하지 않아 성능이 향상되며, Q클래스는 불변이므로 스레드 안전성도 보장됩니다.
131-131: 커서 조건이 의도한 대로 작동하는지 확인하세요.
feed.createdAt.lt(cursorVal)조건은 커서 시간보다 이전(과거) 데이터를 가져옵니다. 일반적인 커서 기반 페이지네이션에서 이것이 맞는 방향인지 확인이 필요합니다.만약 최신 데이터부터 과거로 스크롤하는 것이 의도라면 문제없습니다.
160-170: 효율적인 fetch join 전략입니다.N+1 문제를 방지하기 위한 fetch join과 OneToMany 관계에서 발생할 수 있는 중복 제거를 위한 distinct 사용이 적절합니다.
| @Override | ||
| public FeedShowAllResponse showAllFeeds(Long userId, String cursor) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Stub 구현체에서 null 반환으로 인한 잠재적 위험성
현재 구현체는 null을 반환하여 이 전략이 실수로 활성화될 경우 NullPointerException이 발생할 수 있습니다.
더 안전한 구현을 위해 다음과 같이 개선하는 것을 권장합니다:
@Override
public FeedShowAllResponse showAllFeeds(Long userId, String cursor) {
- return null;
+ throw new UnsupportedOperationException("개인화된 피드 기능은 아직 구현되지 않았습니다.");
}📝 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.
| @Override | |
| public FeedShowAllResponse showAllFeeds(Long userId, String cursor) { | |
| return null; | |
| } | |
| @Override | |
| public FeedShowAllResponse showAllFeeds(Long userId, String cursor) { | |
| throw new UnsupportedOperationException("개인화된 피드 기능은 아직 구현되지 않았습니다."); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/feed/application/service/PersonalizedFeedShowAllService.java
at lines 19 to 22, the method showAllFeeds currently returns null, which risks
causing NullPointerExceptions if this stub implementation is accidentally used.
Replace the null return with a safe default response, such as returning an empty
FeedShowAllResponse or a properly initialized instance that represents no feeds,
to prevent runtime errors and improve robustness.
- 현재 반환되는 nextCursor 값은 실제 DB 에 저장되어 있던 현재 페이지의 마지막 레코드의 createdAt을 toString() 으로 변환한 값이다 - 이때 DB 에 저장된 createdAt은 DATETIME(6) 이므로 마이크로 초 단위까지 저장한다 - 하지만 기존 테스트 코드에서는 java의 LocalDateTime을 toString() 으로 변환하여 cursor 로 사용해 요청을 보냈었는데, java의 LocalDateTime 은 나노초 단위까지 포함하므로 github action 에서 테스트가 실패하는 이슈가 있었다 - 이를 해결하기 위해 native query 를 사용하여 이전 페이지의 마지막 레코드인 f10의 createdAt값을 조죄하여 cursor 로 사용하였다
| // DB에 저장된 f10의 createdAt 값을 native query 로 조회 | ||
| LocalDateTime nextCursorVal = jdbcTemplate.queryForObject( | ||
| "SELECT created_at FROM posts WHERE post_id = ?", | ||
| (rs, rowNum) -> rs.getTimestamp("created_at").toLocalDateTime(), f10.getPostId() | ||
| ); | ||
| String nextCursor = nextCursorVal.toString(); | ||
|
|
||
| //when //then | ||
| mockMvc.perform(get("/feeds") | ||
| .requestAttr("userId", me.getUserId()) | ||
| .param("cursor", t10.toString())) // 이전에 f10 까지 조회 -> f10의 createdAt이 커서 | ||
| .param("cursor", nextCursor)) // 이전에 f10 까지 조회 -> f10의 createdAt이 커서 |
There was a problem hiding this comment.
github action 에서는 테스트가 실패한 원인을 분석하여 간단히 정리하였습니다.
페이징 처리 관련 테스트 코드 작성하실 때 참고하시면 좋을 것 같습니다!
- 현재 반환되는 nextCursor 값은 실제 DB 에 저장되어 있던 현재 페이지의 마지막 레코드의 createdAt을 toString() 으로 변환한 값이다
- 이때 DB 에 저장된 createdAt은 DATETIME(6) 이므로 마이크로 초 단위까지 저장한다
- test 용 yml 에 h2 DB를 mysql 모드로 사용하도록 명시되어 있는데, h2 공식 문서에 따르면 default 는 소수점 아래 6자리 == 마이크로초 단위까지 저장한다고 나와있는것 같습니다
- 참고 : https://www.h2database.com/html/datatypes.html?highlight=datetime&search=date&utm_source=chatgpt.com#firstFound
- 하지만 기존 테스트 코드에서는 java의 LocalDateTime을 toString() 으로 변환하여 cursor 로 사용해 요청을 보냈었는데, java의 LocalDateTime 은 나노초 단위까지 포함하므로 github action 에서 테스트가 실패하는 이슈가 있었다
- 이를 해결하기 위해 native query 를 사용하여 이전 페이지의 마지막 레코드인 f10의 createdAt값을 조죄하여 cursor 로 사용하였다
그런데 의문인 점은 로컬에서도 마찬가지로 test 용 yml을 사용하도록 명시하여 mysql 모드의 h2 DB를 사용하였는데, 왜 로컬에서는 테스트가 통과하고, ci 에서는 실패하였는지 모르겠습니다
There was a problem hiding this comment.
CI와 로컬에서 테스트 결과가 다르게 나오는 이유는 이전에 @DataJpaTest 이슈 때와 비슷한 원인일 것 같습니다.
그때에도 정확하게 문제 원인을 찾진 못했는데 CI환경에서는 로컬 환경에서 사용하는 같은 yml을 사용하더라도, 실행되는 OS, JVM, 라이브러리 버전 환경 차이 때문이라고 결론 내렸었습니다. 그 중 가장 유력한 후보는 github actions는 Linux 기반, 로컬은 Windows 또는 MacOs라는 차이 또는 사용하는 JVM 버전 차이라고 생각합니다.
그 차이로 인해 JDBC가 DB에서 DATETIME(6) 값을 LocalDateTime으로 변환할 때 정밀도가 다르게 처리될 수 있을 것 같습니다.
로컬 환경에서는 DB의 DATETIME(6) 값이 마이크로초(6자리)까지만 잘리고 LocalDateTime도 ...805923000 형태로 6자리까지만 유지되는데,
CI 환경에서는 같은 값이 LocalDateTime으로 변환될 때 나노초(9자리)까지 채워져 들어가면서 커서 비교에서 미묘한 오차가 발생하는 것으로 보입니다.
즉, DB에 저장될 때부터 나노초가 들어가는 것은 아니고, DB → Java로 가져올 때 변환 과정에서 OS/JVM 환경 차이로 나노초 자리값이 다르게 세팅되는 것으로 추정됩니다.
실제로 로컬에서 LocalDateTime을 출력해보면 아래처럼 6자리까지만 찍히는 것을 확인할 수 있었습니다.
2025-07-26T16:16:57.805923
정리하자면, 로컬 환경에서는 DB 값과 LocalDateTime의 정밀도가 맞아서 문제가 없지만,
CI 환경에서는 변환 과정의 정밀도 차이로 인해 커서 비교에 오차가 생기는 것 같습니다!
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (1)
232-286: 많은 수의 테스트 데이터 생성 코드가 중복됩니다.12개의 피드를 생성하고 각각의 타임스탬프를 업데이트하는 코드가 반복적입니다. 테스트 가독성과 유지보수성을 개선할 수 있습니다.
다음과 같이 헬퍼 메소드를 추가하여 코드 중복을 줄일 수 있습니다:
private void createFeedsWithTimestamps(List<UserJpaEntity> users, BookJpaEntity book, LocalDateTime baseTime, int count) { List<FeedJpaEntity> feeds = new ArrayList<>(); for (int i = 0; i < count; i++) { UserJpaEntity user = users.get(i % users.size()); FeedJpaEntity feed = feedJpaRepository.save( TestEntityFactory.createFeed(user, book, true, 10, 5, List.of("contentUrl1", "contentUrl2"))); feeds.add(feed); } feedJpaRepository.flush(); for (int i = 0; i < feeds.size(); i++) { jdbcTemplate.update( "UPDATE posts SET created_at = ? WHERE post_id = ?", Timestamp.valueOf(baseTime.minusMinutes((i + 1) * 5)), feeds.get(i).getPostId()); } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(27-271)
⏰ 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 (8)
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (8)
37-43: 테스트 설정이 적절합니다.
feed.show.strategy=following_priority프로퍼티를 통해 팔로잉 우선순위 전략을 명시적으로 설정하고, 적절한 Spring Boot 테스트 어노테이션들을 사용하고 있습니다.
117-125: Native SQL을 사용한 타임스탬프 조작이 적절합니다.JPA의 audit 기능을 우회하여 정확한 시간 순서를 설정하는 방법이 올바릅니다. 테스트의 예측 가능성을 보장합니다.
360-364: 커서 값 조회 로직이 명확합니다.Native query를 사용하여 실제 DB에 저장된 타임스탬프 값을 조회하는 방식이 정확합니다. 커서 기반 페이지네이션 테스트에 적합합니다.
128-152: 테스트 어설션이 포괄적이고 정확합니다.피드 정렬 순서, 각 피드의 상세 정보(좋아요 수, 댓글 수, 저장/좋아요 상태 등)를 모두 검증하고 있어 테스트의 신뢰성이 높습니다.
202-216: 복잡한 정렬 로직 검증이 우수합니다.팔로잉 우선순위와 공개/비공개 필터링, 시간순 정렬이 모두 올바르게 작동하는지 체계적으로 검증하고 있습니다. 주석으로 예상 결과를 명확히 설명한 점도 좋습니다.
288-310: 페이지네이션 첫 페이지 테스트가 철저합니다.
nextCursor와isLast플래그, 그리고 정확히 10개의 피드가 반환되는지 확인하고 있어 페이지네이션 로직을 완전히 검증합니다.
366-381: 커서 기반 페이지네이션의 마지막 페이지 처리가 정확합니다.커서를 사용해 다음 페이지를 요청했을 때
nextCursor가 null이고isLast가 true가 되는 케이스를 올바르게 테스트하고 있습니다.
75-85: 삭제 순서 검증 완료: 현재 순서가 올바르게 설정되어 있습니다.
자식 엔티티(postLike → savedFeed → content → feed → following)에서 부모 엔티티(user → alias → book) 순으로 삭제가 이루어지므로 외래키 제약조건을 위반하지 않습니다.
- 마찬가지의 이유로 실패한 테스트 코드 수정
buzz0331
left a comment
There was a problem hiding this comment.
수고하셨습니다~!! 로직이 복잡하다보니 리뷰가 좀 있네여 확인해주시면 감사하겠습니다 🤩
| public record Feed( | ||
| Long feedId, | ||
| Long creatorId, | ||
| String creatorNickname, | ||
| String creatorProfileImageUrl, | ||
| String alias, | ||
| String postDate, | ||
| String isbn, | ||
| String bookTitle, | ||
| String bookAuthor, | ||
| String contentBody, | ||
| String[] contentUrls, | ||
| int likeCount, | ||
| int commentCount, | ||
| boolean isSaved, | ||
| boolean isLiked | ||
| ) { } |
There was a problem hiding this comment.
p3: 도메인과의 차이를 두기위해 FeedDto로 네이밍을 수정하는 것 어떨까요?
| public interface FeedQueryMapper { | ||
|
|
||
| @Mapping( | ||
| target = "postDate", | ||
| expression = "java(DateUtil.formatBeforeTime(dto.createdAt()))" | ||
| ) | ||
| FeedShowAllResponse.Feed toFeedShowAllResponse(FeedQueryDto dto); |
There was a problem hiding this comment.
저는 MapStruct 매퍼의 네이밍을 도메인 + Dto + Mapper로 두긴했는데 도메인 + Query + Mapper도 괜찮아 보이네요! 둘 중 하나로 통일할까요?
@hd0rable 결정 오네가이시마스
There was a problem hiding this comment.
하잇 저도 둘다 좋은 방식이지만 통일한다면 조회의 의미를 나타내는 도메인 + Query + Mapper로 하는건 어떨까??라는생각이
There was a problem hiding this comment.
하잇 저도 둘다 좋은 방식이지만 통일한다면 조회의 의미를 나타내는 도메인 + Query + Mapper로 하는건 어떨까??라는생각이
제가 정확히 이런생각으로 네이밍했긴 합니다 하하
| List<Long> feedIds = fetchFeedIdsLatest(userId, cursorVal, size); | ||
| if (feedIds.isEmpty()) { | ||
| return List.of(); // early return | ||
| } |
There was a problem hiding this comment.
오호 FETCH JOIN + 페이징 처리의 문제가 발생하지 않을까 우려되었는데 페이징을 먼저 하고 들어갔군요! LGTM 💯
| /** | ||
| * 엔티티 목록 -> FeedQueryDto 목록 변환 | ||
| */ | ||
| private List<FeedQueryDto> mapToDtoList(List<FeedJpaEntity> entities, Set<Long> savedSet, Set<Long> likedSet) { | ||
| return entities.stream() | ||
| .map(e -> { | ||
| String[] urls = e.getContentList().stream() | ||
| .map(ContentJpaEntity::getContentUrl) | ||
| .toArray(String[]::new); | ||
| return new FeedQueryDto( | ||
| e.getPostId(), | ||
| e.getUserJpaEntity().getUserId(), | ||
| e.getUserJpaEntity().getNickname(), | ||
| e.getUserJpaEntity().getImageUrl(), | ||
| e.getUserJpaEntity().getAliasForUserJpaEntity().getValue(), | ||
| e.getCreatedAt(), | ||
| e.getBookJpaEntity().getIsbn(), | ||
| e.getBookJpaEntity().getTitle(), | ||
| e.getBookJpaEntity().getAuthorName(), | ||
| e.getContent(), | ||
| urls, | ||
| e.getLikeCount(), | ||
| e.getCommentCount(), | ||
| savedSet.contains(e.getPostId()), | ||
| likedSet.contains(e.getPostId()) | ||
| ); | ||
| }) | ||
| .toList(); | ||
| } |
There was a problem hiding this comment.
p2: @QueryProjection을 통해 Q타입을 직접 정의하고 사용하는 것 어떨까요?
현재 성준님의 Querydsl 로직은 연관된 엔티티들을 fetch join으로 모두 영속화한 뒤 Stream API를 통해 DTO로 매핑하는 방식을 사용하고 계신데, 이 경우 모든 엔티티를 메모리에 올리고 DTO를 생성하는 과정에서 메모리 사용량이 증가하고 매핑 비용이 추가로 발생할 것 같습니다.
반대로 @QueryProjection을 사용하면 쿼리 단계에서 필요한 컬럼만 SELECT하여 바로 DTO로 매핑할 수 있기 때문에, 엔티티를 생성하지 않고 필요한 데이터만 메모리에 적재하게 되어 메모리 사용량과 오버헤드가 줄어들 것 같습니다.
다만, @QueryProjection을 사용할 경우에는 fetch join이 아닌 일반 join으로 데이터를 가져오기 때문에 엔티티가 영속화되지 않게 될 것 같습니다. 따라서 이 방식을 도입하신다면 현재 FeedQueryDto에 포함된 isLiked, isSaved와 같은 사용자 의존 필드는 제외하고, 서비스 레이어에서 별도의 포트를 통해 Set을 조회한 뒤 응답 DTO에 매핑하는 방식으로 처리하는 것이 적절할 것 같습니다!
There was a problem hiding this comment.
오호 @QueryProjection 을 통해 dto의 Q클래스 사용은 고려하지 못했네요!
음 현준님 코멘트처럼 isLiked, isSaved 를 어떻게 처리할지 고민해보겠습니다!
There was a problem hiding this comment.
@buzz0331 현준님 생각을 좀 해보다가 FeedQueryDto에 @QueryProjection을 도입하지 않고, QueryDSL에서 기존 방식대로
- 조건에 맞는 feedIds 리스트 조회
- 해당 id 값을 통해 dto를 구성하는데 필요한 엔티티 조회
- dto 로 매핑
하는 로직으로 유지하는 것이 낫다는 결론을 내렸습니다
이유는
@QueryProjection을 사용하여 sql 쿼리가 Q클래스 dto를 반환하도록 하면 하나의 sql 쿼리로 페이징 + 정렬 + 필터링이 적용된 피드 조회를 구현할 수 있지만, 현재 contents가 feed와 1:n 이므로 조회 결과에 중복된 데이터들이 존재할 수 있습니다.
따라서 페이지네이션이 틀어진다고 생각했고, 이를 막기위해 group by를 도입하는 방법도 있지만 이럴거면 쿼리 2번이 낫지 않나? 라는 생각이 들었습니다
따라서 페이징 처리 등을 통해 어떤 데이터를 조회할 지를 ID만 가볍게 조회함으로써 필터링하고, 이에 대응하는 데이터를 한번 더 조회하는 방식을 채택했습니다!
dto를 구성하는 데이터들을 페이징 + 정렬 + 필터링 로직을 포함해서 한번의 sql로 조회하는 것보다 유지보수성 + 가독성 또한 더 좋지 않나 라고 생각합니다! (성능은 모르겠지만, 조회 연산 2번은 괜찮지 않을까요ㅎ)
| @Override | ||
| public List<FeedQueryDto> findFeedsByFollowingPriority(Long userId, LocalDateTime cursorVal, int size) { | ||
| // 1) 게시글 ID만 우선순위 + 페이징으로 조회 | ||
| List<Long> feedIds = fetchFeedIdsByFollowingPriority(userId, cursorVal, size); | ||
| if (feedIds.isEmpty()) { | ||
| return List.of(); // early return | ||
| } | ||
|
|
||
| // 2) 상세 엔티티를 ID 순으로 조회 후 정렬 | ||
| List<FeedJpaEntity> entities = fetchFeedEntitiesByIds(feedIds); | ||
| Map<Long, FeedJpaEntity> entityMap = entities.stream() | ||
| .collect(Collectors.toMap(FeedJpaEntity::getPostId, e -> e)); | ||
| List<FeedJpaEntity> ordered = feedIds.stream() | ||
| .map(entityMap::get) | ||
| .toList(); | ||
|
|
||
| // 3) 플래그 조회 | ||
| Set<Long> savedSet = fetchSavedSet(userId, feedIds); | ||
| Set<Long> likedSet = fetchLikedSet(userId, feedIds); | ||
|
|
||
| // 4) DTO 변환 | ||
| return mapToDtoList(ordered, savedSet, likedSet); | ||
| } | ||
|
|
||
| @Override | ||
| public List<FeedQueryDto> findLatestFeedsByCreatedAt(Long userId, LocalDateTime cursorVal, int size) { | ||
| // 1) 게시글 ID만 최신순 페이징으로 조회 | ||
| List<Long> feedIds = fetchFeedIdsLatest(userId, cursorVal, size); | ||
| if (feedIds.isEmpty()) { | ||
| return List.of(); // early return | ||
| } | ||
|
|
||
| // 2) 상세 엔티티 조회 및 정렬 | ||
| List<FeedJpaEntity> entities = fetchFeedEntitiesByIds(feedIds); | ||
| Map<Long, FeedJpaEntity> entityMap = entities.stream() | ||
| .collect(Collectors.toMap(FeedJpaEntity::getPostId, e -> e)); | ||
| List<FeedJpaEntity> ordered = feedIds.stream() | ||
| .map(entityMap::get) | ||
| .toList(); | ||
|
|
||
| // 3) 플래그 조회 | ||
| Set<Long> savedSet = fetchSavedSet(userId, feedIds); | ||
| Set<Long> likedSet = fetchLikedSet(userId, feedIds); | ||
|
|
||
| // 4) DTO 변환 | ||
| return mapToDtoList(ordered, savedSet, likedSet); | ||
| } |
There was a problem hiding this comment.
p3: 두 메서드의 전체적인 코드 구조가 유사해서, 하나의 메서드로 합친 뒤 조건이 다른 부분을 파라미터로 받아 분기 처리하는 방식으로 개선해도 좋을 것 같습니다.
다만, 현재 전략 패턴을 도입하면서 쿼리 자체를 완전히 분리하려는 의도 같기도해서..
만약 전략별로 쿼리를 명확하게 분리하는 의도가 있으시다면, 지금과 같은 방식도 괜찮을 것 같습니다!!
There was a problem hiding this comment.
저도 처음에는 현준님 생각처럼 겹치는 코드들을 private 로 분리할까 싶다가, 각 메서드의 플로우가 현재는 비슷하지만, 추후에는 바뀔수도 있지 않나 라는 생각이 들어서 dto를 매핑하는 과정만을 private로 분리하고 두 메서드가 공유하도록 구현하였습니다!
다만 dto의 Q 클래스를 도입하면서 코드 수정이 발생할 것 같은데, 이때 리펙토링을 좀 더 고민해보곘습니다!
| @ConditionalOnProperty( | ||
| name = "feed.show.strategy", | ||
| havingValue = "basic", | ||
| matchIfMissing = true // 프로퍼티가 없거나 basic 이면 이 구현체 사용 | ||
| ) |
There was a problem hiding this comment.
오호 이런 어노테이션은 처음 알았네요 yml 파일만 바꿔주면 되니 운영 환경에서 유용할 것 같아요!
| private List<Long> fetchFeedIdsByFollowingPriority(Long userId, LocalDateTime cursorVal, int size) { | ||
| // 내가 작성한 모든 글 + 내가 팔로우하는 다른 유저가 작성한 공개글을 우선적으로 최신순 조회 | ||
| // 이후 내가 팔로우하지 않는 다른 유저가 작성한 공개글을 최신순 조회 | ||
| NumberExpression<Integer> priority = new CaseBuilder() | ||
| .when(feed.userJpaEntity.userId.eq(userId)).then(1) | ||
| .when( | ||
| following.userJpaEntity.userId.eq(userId) | ||
| .and(following.followingUserJpaEntity.userId.eq(feed.userJpaEntity.userId)) | ||
| .and(feed.isPublic.eq(true)) | ||
| ).then(1) | ||
| .otherwise(0); | ||
|
|
||
| return jpaQueryFactory | ||
| .select(feed.postId) | ||
| .distinct() | ||
| .from(feed) | ||
| .leftJoin(following) | ||
| .on(following.userJpaEntity.userId.eq(userId) | ||
| .and(following.followingUserJpaEntity.userId.eq(feed.userJpaEntity.userId))) | ||
| .where( | ||
| // ACTIVE 인 feed & (내가 작성한 글 or 다른 유저가 작성한 공개글) | ||
| feed.status.eq(StatusType.ACTIVE), | ||
| feed.userJpaEntity.userId.eq(userId).or(feed.isPublic.eq(true)), | ||
| cursorVal != null ? feed.createdAt.lt(cursorVal) : Expressions.TRUE | ||
| ) | ||
| .orderBy(priority.desc(), feed.createdAt.desc()) | ||
| .limit(size + 1) | ||
| .fetch(); | ||
| } |
There was a problem hiding this comment.
현재 로직에서 createdAt만 커서로 사용하는 경우에는 문제가 발생할 수 있을 것 같습니다.
예를 들어 (priority, createdAt) 값이 다음과 같다고 가정해보겠습니다.
(1, 10:00)
(1, 09:30)
(1, 09:00)
(0, 11:00)
(0, 10:30)
페이지 크기가 3이라면, 첫 번째 페이지 응답에서는 isLast = false, nextCursor = 09:00으로 내려가게 됩니다.
이후 두 번째 페이지 요청 시 WHERE createdAt < 09:00 조건이 적용되면서 createdAt이 09:00보다 큰 나머지 두 개의 priority=0 데이터가 모두 걸러져 조회되지 않는 문제가 생길 것 같습니다.
따라서 이런 케이스를 방지하려면 단일 createdAt 커서 대신 priority와 createdAt을 함께 사용하는 복합 커서를 도입하는 것이 더 안전할 것 같습니다!
There was a problem hiding this comment.
오오 이걸 놓쳣네요!! 그런데 api 통합테스트에서 걸러내지도 못했네요 허허
애초에 정렬조건이 사실상 2개인 거니 커서가 2개여야하는게 맞네요 하하하하
해당 메서드는 요구사항 변경으로인해 사용되지 않는 메서드이긴 한데, 지금 안고치면 까먹을거같으니 고쳐보겠습니다!
hd0rable
left a comment
There was a problem hiding this comment.
수고하셨습니다~!! 현준띠니가 워낙 예리하게 리뷰 잘 해주셔서 해당 사항들 확인해보시면될것같습니다!! 전 테스트 팩토리 메서드 관련해서 리뷰하나 짤막하게 남겨봤어요 ㅎ
| private StatusType status = StatusType.ACTIVE; | ||
| } No newline at end of file | ||
|
|
||
| @VisibleForTesting |
| public interface FeedQueryMapper { | ||
|
|
||
| @Mapping( | ||
| target = "postDate", | ||
| expression = "java(DateUtil.formatBeforeTime(dto.createdAt()))" | ||
| ) | ||
| FeedShowAllResponse.Feed toFeedShowAllResponse(FeedQueryDto dto); |
There was a problem hiding this comment.
하잇 저도 둘다 좋은 방식이지만 통일한다면 조회의 의미를 나타내는 도메인 + Query + Mapper로 하는건 어떨까??라는생각이
| @ConditionalOnProperty( | ||
| name = "feed.show.strategy", | ||
| havingValue = "basic", | ||
| matchIfMissing = true // 프로퍼티가 없거나 basic 이면 이 구현체 사용 | ||
| ) |
| return feed; | ||
| } | ||
|
|
||
| public static FeedJpaEntity createFeed(UserJpaEntity user, |
There was a problem hiding this comment.
p3: 커스텀한 값을 팩토리에서 한번에 생성하고싶으셔서 만드신 함수 같은데 메서드명에 With를 사용해서 어떤 필드명을 커스텀할수있는지 명시하거나(근데 필드가 좀많으면 메서드명이 가독성이 없어질거같기도하네요) 주석처리로 어떤피드를 만드는 함수라고 작성하면 다른 테스트코드 작성시에도 유용하게 사용할수 있을 것 같습니다!
There was a problem hiding this comment.
메서드 네이밍을 다르게 가져갈까 하다가, 희진님 리뷰 내용처럼 메서드 이름이 너무 길어질 여지가 있어서, 메서드 오버로딩을 적용해보았습니다!
윗 부분에 주석 처리 해두겠습니다!
- 수정된 FeedQueryDto 에 따라 유저가 저장한, 좋아하는 피드들 정보 따로 조회
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (3)
src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java (1)
42-42: 커서 파싱 예외 처리 확인 필요BasicFeedShowAllService와 동일한 이슈로,
DateUtil.parseDateTime(cursor)에서 발생할 수 있는 예외에 대한 처리가 필요합니다.src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (2)
96-124: 커서 페이징에서 복합 정렬 조건 문제를 확인해 주세요.현재
(priority, createdAt)순으로 정렬하지만 커서로는createdAt만 사용하고 있어, 같은createdAt을 가진 서로 다른priority데이터들이 누락될 수 있습니다.예시:
(priority=1, createdAt=10:00),(priority=0, createdAt=10:00)이 있을 때 두 번째 페이지에서createdAt < 10:00조건으로 인해 priority=0인 데이터가 누락될 수 있습니다.과거 리뷰에서 언급된 대로 복합 커서 또는 다른 해결책을 고려해 보세요.
다음 스크립트로 동일한
createdAt을 가진 서로 다른 우선순위 데이터가 존재하는지 확인할 수 있습니다:#!/bin/bash # 동일한 createdAt을 가진 서로 다른 우선순위의 피드가 있는지 확인 rg -A 5 -B 5 "createdAt.*lt.*cursorVal" --type java
163-186: DTO 매핑 로직에서 @QueryProjection 도입을 고려해 보세요.현재 엔티티를 모두 영속화한 후 Stream으로 DTO 매핑하는 방식은 메모리 사용량이 많을 수 있습니다.
과거 리뷰에서 제안된
@QueryProjection을 사용하면:
- 필요한 컬럼만 SELECT하여 메모리 효율성 향상
- 엔티티 생성 오버헤드 제거
- 다만
isLiked,isSaved같은 사용자별 데이터는 별도 포트에서 조회 필요현재 방식도 동작하지만, 대용량 데이터 처리 시 성능 개선을 위해 고려해 볼 만합니다.
🧹 Nitpick comments (2)
src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java (1)
19-19: contentUrls 필드 타입 개선을 고려해보세요.현재
String[]배열을 사용하고 있는데,List<String>을 사용하는 것이 더 안전하고 현대적인 접근법입니다. 배열은 null 안전성 문제와 불변성 보장에 제한이 있습니다.- String[] contentUrls, + List<String> contentUrls,src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java (1)
38-65: 코드 중복 개선을 고려해보세요.BasicFeedShowAllService와 구조가 거의 동일하여 상당한 코드 중복이 있습니다. 커서 파싱, 저장/좋아요 정보 조회, 매핑 로직 등 공통 부분을 추상 클래스나 공통 유틸리티로 추출하는 것을 고려해보세요.
공통 로직을 추상화한 예시:
public abstract class AbstractFeedShowAllService implements FeedShowAllUseCase { protected abstract CursorBasedList<FeedQueryDto> queryFeeds(Long userId, LocalDateTime cursor, int pageSize); @Transactional(readOnly = true) public final FeedShowAllResponse showAllFeeds(Long userId, String cursor) { LocalDateTime cursorVal = parseCursor(cursor); CursorBasedList<FeedQueryDto> result = queryFeeds(userId, cursorVal, PAGE_SIZE); // 공통 로직... } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedShowAllResponse.java(1 hunks)src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java(2 hunks)src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java(1 hunks)src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java(1 hunks)src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java(1 hunks)src/test/java/konkuk/thip/common/util/TestEntityFactory.java(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedShowAllResponse.java
- src/test/java/konkuk/thip/common/util/TestEntityFactory.java
🧰 Additional context used
🧠 Learnings (4)
src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java (1)
Learnt from: seongjunnoh
PR: #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/saved/application/port/out/SavedQueryPort.java (1)
Learnt from: seongjunnoh
PR: #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/mapper/FeedQueryMapper.java (2)
Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
Learnt from: seongjunnoh
PR: #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/FeedQueryRepositoryImpl.java (4)
Learnt from: buzz0331
PR: #75
File: src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java:50-83
Timestamp: 2025-07-14T14:19:38.796Z
Learning: Vote와 VoteItem 엔티티는 자주 함께 사용되므로, N+1 문제를 방지하기 위해 양방향 매핑과 fetch join을 고려하는 것이 좋습니다. 특히 기록장 조회 API 등에서도 함께 사용될 가능성이 높습니다.
Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.
Learnt from: seongjunnoh
PR: #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로부터 조회하는 메서드를 추가함.
Learnt from: seongjunnoh
PR: #93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:369-388
Timestamp: 2025-07-21T08:18:15.767Z
Learning: THIP 프로젝트의 커서 페이지네이션에서는 클라이언트가 "현재 조회할 페이지의 첫 번째 레코드 정보"를 cursor로 전달하며, 서버는 해당 커서 이상(inclusive)의 데이터를 조회하도록 goe, loe를 사용하여 구현되어 있다.
🧬 Code Graph Analysis (2)
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java (1)
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java (1)
Repository(9-29)
src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
DateUtil(12-62)
⏰ 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 (22)
src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java (2)
3-3: 필요한 import 추가됨새로운 메서드에서 사용할
List타입을 위한 import가 적절히 추가되었습니다.
10-10: QueryPort 컨벤션에 맞는 메서드 추가사용자가 좋아요한 피드 ID들을 효율적으로 조회하는 메서드가 추가되었습니다. 메서드 네이밍과 시그니처가 프로젝트의 CQRS 컨벤션을 잘 따르고 있으며, 피드 목록에서 사용자별 좋아요 상태를 배치로 조회할 수 있어 성능상 이점이 있습니다.
src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java (2)
6-6: 필요한 import 추가됨새로운 메서드에서 사용할
List타입을 위한 import가 적절히 추가되었습니다.
13-13: 일관성 있는 메서드 추가
PostLikeQueryPort와 동일한 패턴으로 저장된 피드 ID 조회 메서드가 추가되었습니다. 메서드 네이밍과 시그니처가 일관성 있게 설계되어 있어 코드의 가독성과 유지보수성이 향상됩니다.src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java (1)
93-96: 올바른 어댑터 패턴 구현새로운 메서드가 올바르게 구현되었습니다. 단순히 repository 계층에 위임하는 방식으로 어댑터 패턴을 적절히 따르고 있으며, 클래스 내 다른 메서드들과 일관성 있는 구조를 유지하고 있습니다.
src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java (1)
19-20: 효율적인 JPQL 쿼리 구현사용자가 저장한 피드 ID들을 배치로 조회하는 JPQL 쿼리가 잘 구현되었습니다. IN 절을 사용하여 주어진 피드 목록에서 사용자가 저장한 항목들만 효율적으로 필터링할 수 있으며, 파라미터 바인딩도 적절히 처리되었습니다.
src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java (2)
7-7: 필요한 import 추가됨새로운 메서드에서 사용할
List타입을 위한 import가 적절히 추가되었습니다.
25-28: 일관성 있는 어댑터 구현
SavedQueryPersistenceAdapter와 동일한 패턴으로 구현된 메서드입니다. Repository 계층에 적절히 위임하고 있으며, 전체 어댑터 클래스들 간의 일관성을 잘 유지하고 있습니다.src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java (1)
5-6: 새로운 import문과 쿼리 메서드 구현이 올바릅니다.JPQL 쿼리가 문법적으로 올바르고, IN 절을 사용하여 효율적으로 다중 포스트 ID 필터링을 구현했습니다. 피드 조회 시 사용자별 좋아요 상태를 배치로 조회하는 목적에 적합한 구현입니다.
Also applies to: 9-9, 17-18
src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java (1)
7-22: DTO 구조가 잘 설계되었습니다.피드 조회에 필요한 모든 정보를 적절히 포함하고 있으며, record를 사용한 불변 객체 설계가 좋습니다. 필드명도 명확하고 타입 선택도 적절합니다.
src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java (2)
12-16: MapStruct 설정이 적절합니다.spring 컴포넌트 모델 사용, DateUtil import, unmappedTargetPolicy 설정이 모두 적절하게 구성되어 있습니다.
19-29: 매핑 로직이 효율적으로 구현되었습니다.
contains()메서드를 사용한 boolean 플래그 설정이 간결하고 효율적입니다DateUtil.formatBeforeTime()을 사용한 시간 포맷팅도 적절합니다- 매개변수를 통해 사용자별 저장/좋아요 정보를 받아 처리하는 방식이 깔끔합니다
src/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.java (3)
21-25: 조건부 빈 등록 설정이 우수합니다.
@ConditionalOnProperty를 사용하여 설정 기반으로 서비스 구현체를 선택할 수 있도록 한 것이 운영 환경에서 매우 유용할 것 같습니다.matchIfMissing = true설정으로 기본값 처리도 적절합니다.
39-65: 서비스 로직이 잘 구조화되어 있습니다.
- 커서 기반 페이징 로직이 명확하게 구현되어 있습니다
- 배치로 저장/좋아요 정보를 조회하는 방식이 효율적입니다
@Transactional(readOnly = true)설정이 적절합니다- 응답 구성 로직도 간결하고 이해하기 쉽습니다
43-43: DateUtil.parseDateTime의 예외 처리 확인됨
DateUtil.parseDateTime메서드 내부에서 잘못된 형식의 문자열을DateTimeParseException으로부터InvalidStateException(ErrorCode.API_INVALID_TYPE)로 래핑 처리하고 있으므로,
BasicFeedShowAllService에서는 별도의 추가 예외 처리 없이도 글로벌 예외 핸들러를 통해 적절히 400 Bad Request 응답이 반환됩니다.src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java (1)
21-24: 조건부 전략 설정이 적절합니다.
following_priority전략에 대한 조건부 빈 등록이 올바르게 구현되어 있습니다. BasicFeedShowAllService와 함께 전략 패턴을 잘 구현했습니다.src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java (6)
3-5: 새로운 import 추가가 적절합니다.QueryDSL의 조건부 로직과 엔티티 매핑을 위한 import들이 잘 구성되어 있습니다.
Also applies to: 7-7, 11-11, 13-16
33-38: Q클래스 필드 선언이 깔끔합니다.QueryDSL Q클래스들을 필드로 미리 선언하여 코드 가독성을 높였습니다.
54-71: 팔로잉 우선순위 피드 조회 로직이 잘 구현되었습니다.2단계 접근법(ID 조회 → 엔티티 조회)으로 FETCH JOIN과 페이징 문제를 해결했습니다. 코드 구조도 명확합니다.
74-91: 최신순 피드 조회 로직이 명확합니다.기본적인 최신순 정렬과 커서 페이징이 적절히 구현되어 있습니다.
129-143: 최신순 ID 조회 로직이 간단하고 명확합니다.단일 정렬 조건(
createdAt)이므로 커서 페이징 문제가 없습니다.
148-158: fetch join 최적화가 잘 적용되었습니다.N+1 문제를 방지하기 위해 필요한 연관 엔티티들을 모두 fetch join으로 처리했습니다. 특히
content,user,alias,book엔티티들의 연관관계를 고려한 설계가 좋습니다.
buzz0331
left a comment
There was a problem hiding this comment.
수고하셨습니다~~ 반환 타입 관련해서 리뷰 하나 남겼습니다!! FollowingPriorityFeedShowAllService쪽 복합커서는 아직 도입이 안된 것 같아서 이후에 반영되면 추가적으로 리뷰해보겠습니다~
| boolean isLast | ||
| ) { | ||
| public record Feed( | ||
| public record FeedDto( |
| List<Long> savedFeedIdsByUser = savedQueryPort.findSavedFeedIdsByUserIdAndFeedIds(userId, feedIds); | ||
| List<Long> likedFeedIdsByUser = postLikeQueryPort.findLikedFeedIdsByUserIdAndFeedIds(userId, feedIds); |
There was a problem hiding this comment.
p3: Set으로 반환하는 것 어떨까요? PostQueryMapper에서 contains를 통해 포함여부를 찾을때 성능적으로 O(n)과 O(1)의 차이가 있을 것 같습니다!!
There was a problem hiding this comment.
오오 Set 생각을 자꾸 못하네요 좋습니다!!
| @Query("SELECT p.postJpaEntity.postId FROM PostLikeJpaEntity p WHERE p.userJpaEntity.userId = :userId AND p.postJpaEntity.postId IN :postIds") | ||
| List<Long> findPostIdsLikedByUserIdAndPostIds(@Param("userId") Long userId, @Param("postIds") List<Long> postIds); |
| @Mapping(target = "isSaved", expression = "java(savedFeedIds.contains(dto.feedId()))") | ||
| @Mapping(target = "isLiked", expression = "java(likedFeedIds.contains(dto.feedId()))") |
- List -> Set 으로 변경
- "우선순위 + 최신순 조회" 버전의 피드 조회시 커서값에 해당하는 priority 관련 값을 cursor로 인코딩해야하므로, dto에 관련 필드값 추가
- "우선순위 + 최신순 조회" 버전 피드 조회의 QueryDSL 코드 수정
- Cursor 객체를 활용해 디코딩, 인코딩 하여 QueryDSL 구현체 메서드 호출하도록 코드 수정
- Cursor 객체 생성하여 adapter 에게 던지도록 코드 수정
- MockMvc.param() 메서드는 내부적으로 request param의 인코딩을 수행하므로, '|' 구분자를 활용해 raw 한 cursor 를 직접 request param 으로 주입해야함
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (1)
3-3: 불필요한 import 제거 필요
com.amazonaws.services.cloudformation.model.StackResourceDriftInformation는 사용되지 않는 import입니다.-import com.amazonaws.services.cloudformation.model.StackResourceDriftInformation;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java(2 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(2 hunks)src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java(1 hunks)src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.java(1 hunks)src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java(1 hunks)src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java(1 hunks)src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java(1 hunks)src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (12)
- src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java
- src/main/java/konkuk/thip/feed/application/service/FollowingPriorityFeedShowAllService.java
- src/main/java/konkuk/thip/post/application/port/out/PostLikeQueryPort.java
- src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeQueryPersistenceAdapter.java
- src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java
- src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java
- src/main/java/konkuk/thip/feed/application/service/BasicFeedShowAllService.java
- src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java
- src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java
- src/main/java/konkuk/thip/feed/application/port/out/dto/FeedQueryDto.java
- src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java
- src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java
🧰 Additional context used
🧠 Learnings (2)
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (1)
Learnt from: seongjunnoh
PR: #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/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (2)
Learnt from: seongjunnoh
PR: #93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:369-388
Timestamp: 2025-07-21T08:18:15.767Z
Learning: THIP 프로젝트의 커서 페이지네이션에서는 클라이언트가 "현재 조회할 페이지의 첫 번째 레코드 정보"를 cursor로 전달하며, 서버는 해당 커서 이상(inclusive)의 데이터를 조회하도록 goe, loe를 사용하여 구현되어 있다.
Learnt from: hd0rable
PR: #57
File: src/test/java/konkuk/thip/room/domain/RoomTest.java:0-0
Timestamp: 2025-07-08T16:30:33.771Z
Learning: Room 도메인에서 startDate는 현재 날짜 이후여야 하는 도메인 규칙이 있어서, 테스트에서 만료된 상태를 시뮬레이션하려면 reflection을 사용해야 한다.
🧬 Code Graph Analysis (1)
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(29-293)
⏰ 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 (11)
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepository.java (4)
3-6: 적절한 import 구성입니다.새로 추가된 메서드들에 필요한 타입들이 올바르게 import되었습니다. FeedQueryDto, LocalDateTime, List 모두 메서드 시그니처에서 사용되는 필수 타입들입니다.
12-12: 팔로잉 우선순위 피드 조회 메서드가 잘 설계되었습니다.복합 커서(우선순위 + 생성시간)를 사용한 커서 기반 페이징을 적절히 구현했습니다. 메서드명이 전략을 명확히 나타내고, 파라미터 구성도 적절합니다.
14-14: 최신순 피드 조회 메서드가 적절히 정의되었습니다.생성시간 기반의 단순한 커서 페이징을 사용하는 메서드로, 메서드명이 기능을 명확히 표현하고 파라미터 구성이 적절합니다.
12-14: 전체적인 설계가 우수합니다.
- CQRS 패턴에 맞게 QueryPort의 역할을 잘 수행합니다
- 두 가지 피드 조회 전략을 지원하여 PR 목표에 부합합니다
- 커서 기반 페이징으로 대용량 데이터 처리에 효율적입니다
- 기존 메서드와의 일관성을 유지하면서 확장성도 고려되었습니다
src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (7)
39-41: LGTM! 테스트 전략 설정이 적절함Spring Boot 테스트에서
feed.show.strategy=following_priority속성을 통해 특정 피드 조회 전략을 테스트하도록 설정한 것이 좋습니다.
89-155: 테스트 데이터 설정과 검증 로직이 정확함팔로잉 우선순위 피드 조회 로직을 잘 테스트하고 있습니다:
- 사용자 본인 글(f1)이 최우선으로 반환
- 팔로잉 사용자 글(f2)이 그 다음으로 반환
- 저장된 피드와 좋아요 상태도 올바르게 검증
Native query를 사용한
created_at타임스탬프 설정도 적절합니다.
157-218: 우선순위와 공개/비공개 필터링 테스트가 포괄적임다음 시나리오를 잘 다루고 있습니다:
- 본인 글과 팔로잉 사용자의 공개 글 우선 반환
- 팔로잉하지 않는 사용자의 공개 글 후순위 반환
- 비공개 글(f3, f5) 제외 확인
테스트 데이터 설정과 검증 로직이 정확합니다.
220-312: 첫 번째 페이지 페이징 테스트가 올바름10개 항목 반환,
nextCursor존재,isLast=false검증이 적절합니다.
12개 피드 중 첫 10개가 올바른 우선순위 순서로 반환되는지 확인하고 있습니다.
314-382: 커서 기반 페이징 테스트의 개선된 구현past review comments에서 언급된 타임스탬프 정밀도 문제를 해결하기 위해 native query로 실제 DB 저장값을 조회하여 커서로 사용하는 방식이 훌륭합니다.
LocalDateTime lastCreatedAt = jdbcTemplate.queryForObject( "SELECT created_at FROM posts WHERE post_id = ?", (rs, rowNum) -> rs.getTimestamp("created_at").toLocalDateTime(), f10.getPostId() );이 접근법은 CI/로컬 환경 간 타임스탬프 정밀도 차이로 인한 테스트 실패를 효과적으로 방지합니다.
retrieved_learnings에서 확인한 대로, THIP 프로젝트의 커서 페이지네이션은 "현재 조회할 페이지의 첫 번째 레코드 정보"를 커서로 사용하며, 해당 커서 이상(inclusive)의 데이터를 조회하는 방식으로 구현되어 있습니다.
366-366: 커서 인코딩 처리에 대한 명확한 주석MockMvc가 문자열을 내부적으로 한번 더 인코딩하므로
Cursor.toEncodedString메서드를 사용하지 않는다는 주석이 매우 유용합니다. 이런 세부사항을 명시해주는 것이 좋습니다.
77-87: tearDown의 삭제 순서 검증 불필요
코드 내 JPA 엔티티에서 @manytoone, @onetomany 등 관계 매핑 어노테이션을 사용하고 있지 않아 외래키 제약이 생성되지 않습니다. 따라서 현재tearDown()메서드의 삭제 순서를 변경하거나 검증할 필요가 없습니다.Likely an incorrect or invalid review comment.
| LocalDateTime lastCreatedAt = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(1); | ||
| int size = cursor.getPageSize(); |
There was a problem hiding this comment.
커서 인코딩도 adapter에서 이루어지니까 지금 하신 것처럼 이쪽에서 Cursor를 꺼내서 전달하는 것도 괜찮을 것 같네여 👍🏻
There was a problem hiding this comment.
제 개인적인 생각으로는 Cursor 객체가 너무 많은 layer에서 사용되는 것이 아니라, service에서 request param으로 받은 cursor의 파싱, 그리고 다음 페이징처리를 위해 다시 String cursor로의 인코딩으로만 사용되는게 유지보수하기에 좋지 않나 라고 생각합니다
하지만 시간 관계상 일단 기존 Cursor 코드는 건들지 않았고, adapter에서 QueryDSL 에게 파싱한 커서값을 던져주는식으로 구현하였습니다!!
추후에 Cursor의 역할과 책임에 대해서 한번 얘기해봐도 좋을것같습니다! (일단 api 개발 중에는 지금 코드 고대로 고고하셔도 될듯합니다)
| @Query("SELECT p.postJpaEntity.postId FROM PostLikeJpaEntity p WHERE p.userJpaEntity.userId = :userId AND p.postJpaEntity.postId IN :postIds") | ||
| List<Long> findPostIdsLikedByUserIdAndPostIds(@Param("userId") Long userId, @Param("postIds") List<Long> postIds); | ||
|
|
||
| @Query(value = "SELECT pl.post_id FROM post_likes pl WHERE pl.user_id = :userId AND pl.post_id IN (:postIds)", nativeQuery = true) | ||
| Set<Long> findPostIdsLikedByUser(@Param("postIds") Set<Long> postIds, | ||
| @Param("userId") Long userId); |
There was a problem hiding this comment.
두개의 쿼리 통합하기로 했던 것 같은데 누락된 것 같습니다!!
| List<Long> feedIds = tuples.stream() | ||
| .map(tuple -> tuple.get(0, Long.class)) | ||
| .toList(); | ||
| Map<Long, Integer> priorityMap = tuples.stream() | ||
| .collect(Collectors.toMap( | ||
| tuple -> tuple.get(0, Long.class), | ||
| tuple -> tuple.get(1, Integer.class) | ||
| )); |
There was a problem hiding this comment.
혹시 이게 어떤 걸 매핑하고 있는 로직이죠..?
There was a problem hiding this comment.
"우선순위 + 최신순 조회" 버전의 피드 조회에서는 "유저 본인이 작성한 피드 + 유저가 팔로잉하고 있는 다른 유저가 작성한 공개 피드" 가 우선적으로 보여야 하는데, QueryDSL 코드에서
- 1차적으로 현재 페이지에 맞는 피드의 id값과 해당 피드의 우선순위(= 0 or 1) 을 sql 쿼리를 통해 조회 -> 이때 조회 결과가 List 입니다
- 1에서 조회한 List을 Map<Long, Integer> prioirtyMap 으로 변환
- 이후 1에서 조회한 id에 따라 DB에서 dto를 구성하는 정보를 조회한 후, dto로 매핑
이런 플로우로 구현하였습니다
기존에는 조회한 결과에 priority 값이 누락되어 있어서 페이징 처리가 제대로 이루어지지 않는 버그가 있었는데, 이를 해결하기 위해 1번에서 현재 페이지에 해당하는 id값만 조회하는게 아니라, Tuple 을 사용하여 id와 priority 값을 한꺼번에 조회하는 식으로 수정했습니다!
There was a problem hiding this comment.
아하 확인했습니다~ 💯 성준띠의 고충이 느껴지네여,,
- 내부 repository 코드는 native query -> jpql을 사용하도록 수정
- 파라미터로 List가 아니라 Set을 받도록 수정
| @Query("SELECT p.postJpaEntity.postId FROM PostLikeJpaEntity p WHERE p.userJpaEntity.userId = :userId AND p.postJpaEntity.postId IN :postIds") | ||
| List<Long> findPostIdsLikedByUserIdAndPostIds(@Param("userId") Long userId, @Param("postIds") List<Long> postIds); | ||
|
|
||
| @Query(value = "SELECT pl.post_id FROM post_likes pl WHERE pl.user_id = :userId AND pl.post_id IN (:postIds)", nativeQuery = true) | ||
| Set<Long> findPostIdsLikedByUser(@Param("postIds") Set<Long> postIds, |



#️⃣ 연관된 이슈
📝 작업 내용
전체 피드를 페이징처리하여 조회하는 api를 개발하였습니다.
<전체 피드 조회 api 플로우>
controller
service
FeedShowAllUseCase 의 구현체는 현재 총 3개 있습니다
유저가 작성한 게시글 및 다른 유저가 작성한 공개 게시글을 [최신순] 으로 정렬하여 반환하는 구현체 입니다
유저가 작성한 게시글 및 유저가 팔로잉하는 다른 유저가 작성한 공게 게시글을 [우선적으로], 최신순으로 정렬합니다
이후, 유저가 팔로잉하지 않는 다른 유저가 작성한 공개 게시글을 최신순으로 정렳하여 반환하는 구현체 입니다
[유저 맞춤 피드 추천 기능] 이 포함된 구현체 입니다. 현재 구현되지는 않았고, 추후에 구현될 예정입니다.
처음에는 피그마의 화면설계서에 작성된 것처럼 2번 방식으로 전체 피드 조회 로직을 구현하였으나, @heeeeyong 님이 "유저가 작성한 게시글과 다른 유저가 작성한 공개 게시글을 단순히 [최신순] 으로 정렬하여 반환" 하도록 요청하셔서, 1번 방식을 추가로 구현하여 현재 main 메서드 run 시에 1번 구현체가 동작합니다.
런타임시에 어떤 구현체가 동작하는 지를 yml 파일의 feed.show.strategy 에 주입된 값에 따라서 선택할 수 있습니다.
영속성 adapter
통합 테스트 코드
📸 스크린샷
💬 리뷰 요구사항
@buzz0331 님이 의견주신 것처럼 조회 로직시에 사용될 하나의 피드를 구성하는 조회용 dto인 FeedQueryDto를 정의하여 QueryDSL 코드가 이를 반환 타입으로 설정하도록 하였습니다.
전체 피드 조회 api 뿐만 아니라, 피드 상세조회 등 피드 관련 조회 api 에서 해당 조회용 dto 를 유용하게 사용할 수 있을 것 같습니다!
해당 부분 참고해서 리뷰해주시면 감사하겠습니다!
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores