[Feat] 매체 목록 API에 offset/limit 페이지네이션 추가#59
Conversation
매체 사진(photoUrl)이 고해상도 원본이라 목록을 한 번에 전부 내려주면 트래픽이 크다. 이미지 자체를 리사이즈하는 대신, 화면에 실제로 보일 만큼만 불러오도록 offset/limit 기반 페이지네이션을 추가했다 (예: 첫 진입 limit=10, 스크롤마다 offset을 누적하며 limit=6으로 추가 로드하는 무한 스크롤 전제). - MediaUnitListResponse에 hasMore 추가 - MediaUnitQueryService: RealtimeGraphService와 동일한 clamp 패턴 (limit 기본 10, 최대 50, 범위 밖이면 자동 clamp). 페이지네이션을 먼저 적용한 뒤 기간 충돌 조회(findConflictingMediaUnitIds)를 페이지 분량의 ID로만 실행하도록 순서를 바꿔 쿼리 규모도 줄였다 - Swagger 문서, campaign-registration-api-spec.md 갱신 검증: 페이지네이션 케이스(기본 10개+hasMore, offset 적용한 다음 페이지, limit 최대치 clamp) 테스트 추가, 전체 295개 테스트 통과. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 42 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough매체 목록 조회 API에 Changes매체 목록 페이지네이션
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant MediaUnitController
participant MediaUnitQueryService
participant MediaUnitListResponse
Client->>MediaUnitController: offset, limit 포함 목록 조회 요청
MediaUnitController->>MediaUnitQueryService: 필터와 페이지네이션 파라미터 전달
MediaUnitQueryService->>MediaUnitQueryService: 파라미터 보정 및 페이지 생성
MediaUnitQueryService->>MediaUnitListResponse: mediaUnits와 hasMore 생성
MediaUnitQueryService-->>MediaUnitController: 페이지 응답 반환
MediaUnitController-->>Client: mediaUnits와 hasMore 응답
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryService.java`:
- Around line 73-101: 현재 메모리 기반 페이징 구현은 변경하지 말고, 향후 활성 매체 수 증가에 대비해 DB 레벨 페이징과
검색 최적화 전환 작업을 백로그에 추가하세요. MediaUnitQueryService의 filtered 및
stream().skip().limit() 흐름을 참고해 Pageable 기반 조회와 Full Text Search 인덱스 검토 항목을
기록하세요.
In
`@src/test/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryServiceTest.java`:
- Around line 97-109: Extend MediaUnitQueryServiceTest with boundary tests for
negative pagination parameters and offsets beyond the available data, using the
existing getMediaUnits and persistence helpers. Verify negative offset and limit
are clamped to valid minimums with the expected first-page result, and verify an
offset beyond the total count returns an empty mediaUnits list with hasMore
false.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1bae3922-c409-47cc-bc5d-b6b8cdc43f48
📒 Files selected for processing (6)
docs/campaign-registration-api-spec.mdsrc/main/java/com/shinhan/klljs/domain/media/controller/MediaUnitController.javasrc/main/java/com/shinhan/klljs/domain/media/controller/MediaUnitControllerDocs.javasrc/main/java/com/shinhan/klljs/domain/media/dto/MediaUnitListResponse.javasrc/main/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryService.javasrc/test/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryServiceTest.java
| return new MediaUnitListResponse(List.of(), false); | ||
| } | ||
|
|
||
| List<Long> mediaUnitIds = filtered.stream().map(MediaUnit::getId).toList(); | ||
| // 매체 사진(photoUrl)이 고해상도 원본이라 한 번에 다 내려주면 트래픽이 크다 - 화면에 보일 | ||
| // 만큼만 페이지로 잘라서 응답한다(무한 스크롤). 매체 수가 많아져도 findConflictingMediaUnitIds가 | ||
| // 이 페이지의 ID만 조회하도록, 페이지네이션을 먼저 적용한 뒤 기간 충돌을 계산한다. | ||
| int offset = clampOffset(offsetParam); | ||
| int limit = clampLimit(limitParam); | ||
| List<MediaUnit> page = filtered.stream().skip(offset).limit(limit).toList(); | ||
| boolean hasMore = offset + page.size() < filtered.size(); | ||
|
|
||
| if (page.isEmpty()) { | ||
| return new MediaUnitListResponse(List.of(), false); | ||
| } | ||
|
|
||
| List<Long> pageMediaUnitIds = page.stream().map(MediaUnit::getId).toList(); | ||
| Set<Long> conflictingIds = new HashSet<>(campaignRepository.findConflictingMediaUnitIds( | ||
| mediaUnitIds, | ||
| pageMediaUnitIds, | ||
| CampaignStatus.REGISTRATION_FAILED, | ||
| executionStartDate, | ||
| executionEndDate | ||
| )); | ||
|
|
||
| List<MediaUnitSummary> summaries = filtered.stream() | ||
| List<MediaUnitSummary> summaries = page.stream() | ||
| .map(media -> MediaUnitSummary.from(media, !conflictingIds.contains(media.getId()))) | ||
| .toList(); | ||
| return new MediaUnitListResponse(summaries); | ||
| return new MediaUnitListResponse(summaries, hasMore); | ||
| } | ||
|
|
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial
메모리 기반 페이징 처리 관련 운영 조언
현재 MVP 특성상 매체 수가 적어 전체 데이터를 메모리에 로드한 뒤 stream().skip().limit()으로 페이징을 처리하고 있습니다. 이 방식은 현재로서는 안전하지만, 향후 활성 매체 수가 급증할 경우 다음과 같은 문제가 발생할 수 있습니다.
- 모든 엔티티를 영속성 컨텍스트 및 메모리에 적재함에 따른 힙 메모리 사용량 증가
- 가비지 컬렉션(GC) 빈도 증가로 인한 응답 지연
당장은 변경할 필요가 없으나, 서비스 규모가 커지면 DB 레벨의 페이징(Spring Data JPA의 Pageable) 및 검색 최적화(Full Text Search 인덱스 등)로 전환하는 것을 백로그에 추가하여 관리하시기를 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryService.java`
around lines 73 - 101, 현재 메모리 기반 페이징 구현은 변경하지 말고, 향후 활성 매체 수 증가에 대비해 DB 레벨 페이징과
검색 최적화 전환 작업을 백로그에 추가하세요. MediaUnitQueryService의 filtered 및
stream().skip().limit() 흐름을 참고해 Pageable 기반 조회와 Full Text Search 인덱스 검토 항목을
기록하세요.
CodeRabbit 지적 검증 결과 clampOffset/clampLimit과 빈 페이지 처리 로직은 이미 올바르게 동작했지만(코드 트레이싱으로 확인), 회귀 방지를 위해 테스트로 고정해둔다. - offset=-5, limit=-3 -> offset 0, limit 1로 clamp되어 첫 항목 하나만 반환 - offset이 전체 개수를 넘으면 빈 목록 + hasMore=false DB 레벨 페이징 전환 제안은 코드 변경 없이 스킵 - MediaUnitQueryService 클래스 주석에 이미 "매체 수가 작은 MVP" 전제가 문서화되어 있고, 트래픽이 커지면 그때 전환한다는 계획도 이미 있다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
배경
매체 목록 조회(
GET /api/v1/media-units)에서 매체 사진(photoUrl)이 고해상도 원본이라, 목록 전체를 한 번에 내려주면 트래픽이 큽니다. 이미지 자체를 리사이즈하는 방식(백엔드가 이미지 파일을 소유하지 않아 SSRF/메모리 이슈 등 리스크가 더 큼) 대신, 화면에 실제로 보일 만큼만 페이지 단위로 불러오는 쪽으로 결정했습니다.변경 내용
offset/limit쿼리 파라미터 추가. 생략 시offset=0,limit=10, 최대limit=50(범위를 벗어나면 자동 clamp -RealtimeGraphService의 기존 clamp 패턴과 동일)MediaUnitListResponse에hasMore추가 - 무한 스크롤 종료 시점 판단용findConflictingMediaUnitIds)를 실행하도록 순서를 바꿔, 이 쿼리도 페이지 분량의 ID만 조회하도록 함께 개선docs/campaign-registration-api-spec.md갱신프론트 사용 예
limit=10offset + limit을 다음offset으로,limit=6으로 반복 호출hasMore: false가 나오면 더 불러올 매체 없음검증
페이지네이션 케이스(기본 10개+
hasMore,offset적용한 다음 페이지,limit최대치 clamp) 테스트 추가. 전체 295개 테스트 통과.🤖 Generated with Claude Code
Summary by CodeRabbit
새로운 기능
offset과limit으로 원하는 구간을 조회할 수 있으며, 기본값은 각각 0과 10입니다.hasMore가 포함되어 추가 데이터 존재 여부를 확인할 수 있습니다.hasMore: false를 반환합니다.문서