Skip to content

Conversation

@doma17
Copy link
Contributor

@doma17 doma17 commented Sep 6, 2025

#️⃣ 연관된 이슈

ex) #이슈번호, #이슈번호

#254 #242

📝 작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)

기존 '카테고리별 삭제 되지 않은 모든 게시물 조회'에서 mongoTemplate으로 커서 기반 페이징 로직 구현했습니다.

  • 추후 Post 리팩토링 PR 합병 시 추가 필요합니다.

테스트 환경

'에브리 타임'과 같이 많은 게시물을 가지고 페이징을 해야하는 경우를 상정해서 테스트를 진행했습니다.

  • 100만개 테스트 데이터 세트

  • 로컬 m2 air 16gb 환경

  • 10개 스레드 - Inifite

  • 평균 latency : 3509ms -> 85ms 로 향상

  • 초당 Throughput : 2.8/sec -> 115/sec 으로 향상

mongosh 복합 인덱스 추가 필요

db.posts.createIndex(
  { postStatus: 1, postCategory: 1, created_at: -1, _id: -1 },
  { partialFilterExpression: { deleted_at: null, postStatus: "ACTIVE" } }
)

스크린샷 (선택)

일반 limit, skip 기반 페이징 - page 0

일반페이징성능2 2025-09-06 15 33 43 일반페이징성능2 2025-09-06 15 33 39 일반페이징성능2 2025-09-06 15 33 36

cursor 기반 페이징 - page 0

커서페이징성능2 2025-09-06 15 36 15 커서페이징성능2 2025-09-06 15 36 07 커서페이징성능2 2025-09-06 15 36 01

cursor 기반 페이징 - page 1000

  • 기존 limit, skip 기반 페이징보다 페이지 depth가 깊어도 안정적인 페이지 반환을 하는 것을 알 수 있습니다.
스크린샷 2025-09-06 15 45 11 스크린샷 2025-09-06 15 45 07 스크린샷 2025-09-06 15 45 02

💬 리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

기존 '카테고리별 삭제 되지 않은 모든 게시물 조회'에서 mongoTemplate으로 커서 기반 페이징 로직 구현
@doma17 doma17 self-assigned this Sep 6, 2025
@doma17 doma17 added the ✨ Feature 기능 개발 label Sep 6, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 6, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

카테고리별 게시글 조회에 커서 기반 페이지네이션을 도입. 컨트롤러에 신규 GET /posts/category/cursor 엔드포인트 추가, 서비스·리포지토리·DTO 신설. 기존 컨트롤러 생성자 시그니처가 PostCursorService 주입으로 변경됨.

Changes

Cohort / File(s) Summary
Controller
src/main/java/.../post/controller/PostController.java
커서 기반 카테고리 조회 엔드포인트 추가(GET /posts/category/cursor). PostCursorService 주입 필드 및 생성자 변경. CursorPageResponse 반환 타입 적용.
Service
src/main/java/.../post/service/PostCursorService.java
블록 사용자 필터링 및 커서(ObjectId) 검증/변환을 포함한 카테고리별 커서 페이징 로직 추가. CursorPageResponse<PostDetailResponseDTO> 생성.
Repository
src/main/java/.../post/repository/PostReadRepository.java
MongoTemplate 기반 커서 조회 메서드 추가: 삭제 아님, ACTIVE 상태, 카테고리 prefix 매칭, 차단 사용자 제외, _id 내림차순 정렬, limit+1 로 다음 페이지 판단.
DTO
src/main/java/.../post/dto/response/CursorPageResponse.java
커서 페이지 응답용 제네릭 DTO(items, nextCursor, hasNext) 추가. Lombok @Getter, @Builder.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Controller as PostController
  participant CursorSvc as PostCursorService
  participant BlockSvc as BlockService
  participant Repo as PostReadRepository
  participant PostSvc as PostService

  Client->>Controller: GET /posts/category/cursor?category&cursor&limit
  Controller->>CursorSvc: getAllPostsByCursorIdOnly(category, cursor, limit)

  CursorSvc->>BlockSvc: getBlockedUsers()
  BlockSvc-->>CursorSvc: List<ObjectId>

  CursorSvc->>CursorSvc: 검증/파싱(cursor → ObjectId?)
  CursorSvc->>Repo: findByCategoryWithIdCursor(cat, blocked, lastId, limit)
  Repo-->>CursorSvc: List<PostEntity>(limit+1까지)

  CursorSvc->>CursorSvc: hasNext 계산 및 trim, nextCursor 산출
  CursorSvc->>PostSvc: getPostListResponseDtos(batch)
  PostSvc-->>CursorSvc: List<PostDetailResponseDTO>

  CursorSvc-->>Controller: CursorPageResponse<PostDetailResponseDTO>
  Controller-->>Client: 200 OK SingleResponse
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

새벽 커서 톡톡, 페이지가 열리고,
당근 깃발 펄럭~ 카테고리 길 위로.
차단은 비켜라, 길은 곧고 선명해,
아이디 별똥별 따라 다음으로 힐끗.
토끼 발굽 리듬에, nextCursor 콩콩!

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/post-db-cursor

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@doma17
Copy link
Contributor Author

doma17 commented Sep 6, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 6, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@doma17 doma17 changed the title perf: post 커서 기반 페이징 로직 추가 perf: post 커서 기반 페이징 Sep 6, 2025
Copy link
Contributor

@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: 4

🧹 Nitpick comments (5)
src/main/java/inu/codin/codin/domain/post/dto/response/CursorPageResponse.java (1)

8-14: 마지막 페이지에서 nextCursor를 숨겨 응답 단순화

nextCursor가 null일 때 직렬화 제외하면 클라이언트 처리 비용이 줄어듭니다. 아래처럼 NON_NULL 적용을 권장합니다.

 package inu.codin.codin.domain.post.dto.response;

+import com.fasterxml.jackson.annotation.JsonInclude;
 import lombok.Builder;
 import lombok.Getter;

 import java.util.List;

 @Getter
 @Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
 public class CursorPageResponse<T> {
     private final List<T> items;
     private final String nextCursor; // 다음 페이지 커서 값
     private final boolean hasNext;
 }
src/main/java/inu/codin/codin/domain/post/controller/PostController.java (1)

20-21: 검증 애노테이션 import 정리 제안

위 변경을 적용한다면 아래 import가 필요합니다.

 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Pattern;
src/main/java/inu/codin/codin/domain/post/repository/PostReadRepository.java (3)

39-46: 주석과 실제 조건이 불일치합니다.

  • Line 39: “차단 유저가 없을 때” → 조건은 blockedUsers가 존재할 때입니다.
  • Line 44: “첫번째 페이지 조회” → cursorId가 있을 때는 첫 페이지가 아닙니다.
-        // 차단 유저가 없을 때
+        // 차단 유저가 있을 때
 ...
-        // 첫번째 페이지 조회
+        // 커서가 있을 때(후속 페이지)

48-53: 인덱스 설계 조정 제안(쿼리와 정렬에 정합성 맞추기)

현재 쿼리는 _id 기준 내림차순 정렬만 사용합니다. 인덱스도 이에 맞추는 편이 효율적입니다.

  • 제안 인덱스(부분 인덱스 포함, 필드명은 실제 스키마에 맞게 조정 필요):
// deleted_at 필드가 null 또는 미존재이고, postStatus가 ACTIVE인 문서만 대상
db.posts.createIndex(
  { postStatus: 1, postCategory: 1, _id: -1 },
  { partialFilterExpression: { $and: [
      { postStatus: { $eq: "ACTIVE" } },
      { $or: [ { deleted_at: null }, { deleted_at: { $exists: false } } ] }
  ]}}
)
  • 만약 created_at 정렬을 실제로 사용할 계획이라면, 서비스/레포지토리 정렬과 범위 조건을 created_at + _id로 바꾸고, 인덱스도 { postStatus:1, postCategory:1, created_at:-1, _id:-1 }로 일치시키세요.

21-53: Criteria 조합 간소화(선택)

andOperator 중첩 대신 Query.addCriteria로 단계별 추가가 읽기 쉽습니다. 기능 동일합니다.

-        Criteria base = new Criteria().andOperator(
-                new Criteria().orOperator(
-                        Criteria.where("deletedAt").is(null),
-                        Criteria.where("deletedAt").exists(false)
-                ),
-                Criteria.where("postStatus").is("ACTIVE"),
-                Criteria.where("postCategory").is(postCategory)
-        );
+        Criteria base = new Criteria();
+        base.orOperator(
+                Criteria.where("deletedAt").is(null),
+                Criteria.where("deletedAt").exists(false)
+        );
+        base.and("postStatus").is("ACTIVE");
+        base.and("postCategory").is(postCategory);
 ...
-        Query query = new Query(base)
+        Query query = new Query().addCriteria(base)
                 .with(Sort.by(Sort.Direction.DESC, "_id"))
                 .limit(limit + 1);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f736ade and 92bb81a.

📒 Files selected for processing (4)
  • src/main/java/inu/codin/codin/domain/post/controller/PostController.java (3 hunks)
  • src/main/java/inu/codin/codin/domain/post/dto/response/CursorPageResponse.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/post/repository/PostReadRepository.java (1 hunks)
  • src/main/java/inu/codin/codin/domain/post/service/PostCursorService.java (1 hunks)
🔇 Additional comments (2)
src/main/java/inu/codin/codin/domain/post/dto/response/CursorPageResponse.java (1)

8-14: DTO 구성 깔끔합니다.

불변 필드 + Lombok 빌더 선택 적절합니다.

src/main/java/inu/codin/codin/domain/post/service/PostCursorService.java (1)

36-40: IllegalArgumentException 핸들러 확인 완료: 400 응답 반환
GlobalExceptionHandler.java에 @ExceptionHandler(IllegalArgumentException.class)가 정의되어 HttpStatus.BAD_REQUEST를 반환하도록 설정되어 있어 예외 매핑이 정상입니다.

- 다음 페이지가 없을 때 커서 null
- deletedAt -> deleted_at 쿼리 조정
- regex에서 정확한 postCategory로 분류
@gisu1102
Copy link
Contributor

기존 PR 빠르게 병합이후 위 PR 내용 병합진행하겠습니다.
처음보는 내용인데 성능이 극단적으로 향상되네요 감사합니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants