Skip to content

[BE][feat]: 알림 페이징 조회 쿼리 정렬 최적화#1055

Draft
2Jin1031 wants to merge 1 commit intodevelop-befrom
feat/1054
Draft

[BE][feat]: 알림 페이징 조회 쿼리 정렬 최적화#1055
2Jin1031 wants to merge 1 commit intodevelop-befrom
feat/1054

Conversation

@2Jin1031
Copy link
Contributor

@2Jin1031 2Jin1031 commented Dec 14, 2025

🔗 관련 이슈

📝 작업 내용

주요 변경사항

📸 스크린샷 (Optional)

Summary by CodeRabbit

릴리스 노트

  • 버그 수정
    • 알림 목록의 정렬이 개선되어 동일한 시간에 생성된 알림들이 더욱 일관성 있게 표시됩니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@2Jin1031 2Jin1031 closed this Dec 14, 2025
@github-actions github-actions bot changed the title feat: 알림 조회 쿼리 ORDER BY 방식에 생성일시 컬럼 추가 [BE][feat]: 알림 페이징 조회 쿼리 정렬 최적화 Dec 14, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 14, 2025

Important

Review skipped

Draft detected.

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.

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'tools'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • 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

Walkthrough

NotificationRepository의 JPQL 쿼리 2개가 정렬 조건을 수정했습니다. 기존 id DESC 단일 정렬에서 createdAt DESCid DESC 복합 정렬로 변경되었습니다. 메서드 시그니처와 반환 타입은 유지되며, 동일한 패턴의 수정이 두 쿼리에 적용되었습니다.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • 변경 범위가 제한적 (단일 파일, 2개 쿼리)
  • 동일한 패턴의 반복적 수정으로 일관성 높음

추가 검토 영역:

  • 데이터베이스 인덱스 전략과의 정렬 순서 매칭 여부 확인 필요
  • 쿼리 성능 개선 효과가 실제로 검증되었는지 검토 권장

상세 검토 의견

🔍 긍정적 측면

쿼리 정렬 최적화 아이디어는 적절합니다. 특히 페이징 조회 시나리오에서 createdAt을 우선 정렬하면, 같은 타임스탬프를 가진 알림들이 일관되게 정렬되어 페이징 안정성이 향상됩니다.

⚠️ 검토가 필요한 부분

1. 데이터베이스 인덱스 전략의 정렬 순서

  • 현재 쿼리 정렬이 createdAt DESC, id DESC인데, DB 인덱스가 이 순서대로 구성되어 있는지 확인이 필수입니다
  • 만약 인덱스가 (id DESC, createdAt DESC) 또는 단일 컬럼으로만 구성되어 있다면, 이 쿼리는 인덱스를 제대로 활용하지 못해 오히려 성능이 악화될 수 있습니다
  • EXPLAIN PLAN을 통해 쿼리 실행 계획이 개선되었는지 검증 권장

2. 성능 영향도 분석 부재

  • 추가 정렬 컬럼으로 인한 정렬 비용 증가가 인덱스 활용으로 상쇄되는지 명확하지 않습니다
  • 특히 대량의 알림 데이터 환경에서는 영향이 클 수 있습니다

💡 개선 방향

인덱스 재구성 고려:

옵션 A: 복합 인덱스 생성 (권장)

CREATE INDEX idx_notification_created_at_id ON notification(created_at DESC, id DESC);
  • 장점: 쿼리 성능 최적화, 정렬 비용 최소화
  • 단점: 인덱스 유지 비용, 저장소 사용량 증가

옵션 B: 기존 쿼리 유지

ORDER BY id DESC
  • 장점: 인덱스 구조 변경 불필요, 단순함
  • 단점: 같은 생성 시간의 알림들이 id순으로 섞일 수 있음 (사용자 경험 측면에서 부자연스러움)

현재 변경은 UX 관점에서는 의미 있으나, 인덱스 전략 및 성능 영향도 검증 문서가 함께 제시되면 더 설득력 있는 변경이 될 것 같습니다.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명은 관련 이슈 번호를 명시했으나, 작업 내용과 주요 변경사항 섹션이 비워져 있어 변경의 의도와 구체적 내용이 부족합니다. 작업 내용 섹션에 쿼리 정렬 변경의 목적을 작성하고, 주요 변경사항에 createdAt DESC 추가와 영향 범위를 구체적으로 기술해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues check ❓ Inconclusive 코드 변경사항(JPQL 쿼리에 createdAt DESC 추가)은 이슈 제목의 정렬 최적화 목표와 일치하지만, 이슈에 상세 요구사항이 없어 완전한 검증이 어렵습니다. 이슈 #1054의 상세 작업 내용이 구체화되거나, 이 변경이 페이징 정렬 성능 문제를 어떻게 해결하는지 명시해주세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 변경 사항의 핵심을 정확히 반영하고 있으며, 알림 조회 쿼리의 정렬 최적화라는 주요 변경점을 명확하게 전달합니다.
Out of Scope Changes check ✅ Passed NotificationRepository의 두 JPQL 쿼리 정렬 변경은 이슈의 정렬 최적화 목표와 직접적으로 관련되어 있으며, 범위를 벗어난 변경은 없습니다.

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.

@github-actions github-actions bot added the BE 백엔드 관련 이슈입니다. label Dec 14, 2025
@2Jin1031 2Jin1031 reopened this Dec 14, 2025
@2Jin1031 2Jin1031 marked this pull request as draft December 14, 2025 07:04
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: 2

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5720e9 and 725e297.

📒 Files selected for processing (1)
  • backend/src/main/java/backend/mulkkam/notification/repository/NotificationRepository.java (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
backend/**

⚙️ CodeRabbit configuration file

backend/**: - 1. 팀 및 공식 컨벤션, 가독성, 예외처리, 테스트/확장/유지보수성, 모듈화, API/DB/보안 설계 기준을 기반으로 리뷰해주세요.

    1. 최신 트렌드, 불필요한 로직, 클린코드, 리팩토링, 서비스/도메인 설계, 공통 예외 처리, 확장성도 함께 확인해주세요.
    1. 각 피드백은 문제점·대안·장단점을 짧고 논리적으로, 예시 코드가 있다면 간결히 포함해 주세요.
    1. 팀 내 스타일 통일성도 확인해주세요.
    1. 미작성한 테스트 코드 케이스가 있다면, 어떤 테스트가 필요한지 제안해주세요. (예: 컨트롤러는 인수 테스트, 나머지는 단위 테스트)
    1. 리뷰 남겨주는 부분은 해당 라인 범위의 코멘트에 작성해주세요.

Files:

  • backend/src/main/java/backend/mulkkam/notification/repository/NotificationRepository.java
🧠 Learnings (1)
📚 Learning: 2025-09-28T07:47:02.970Z
Learnt from: 2Jin1031
Repo: woowacourse-teams/2025-mul-kkam PR: 873
File: backend/src/main/java/backend/mulkkam/notification/repository/NotificationRepository.java:0-0
Timestamp: 2025-09-28T07:47:02.970Z
Learning: 알림 조회에서 커서 페이지네이션 구현 시 `n.id <= :lastId` 조건을 사용할 때는 중복 노출을 방지하기 위해 커서 찾는 방식의 순서를 적절히 조정해야 함.

Applied to files:

  • backend/src/main/java/backend/mulkkam/notification/repository/NotificationRepository.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). (2)
  • GitHub Check: test
  • GitHub Check: test

WHERE n.createdAt >= :limitStartDateTime
AND n.member.id = :memberId
ORDER BY n.id DESC
ORDER BY n.createdAt DESC, n.id DESC
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n backend/src/main/java/backend/mulkkam/notification/repository/NotificationRepository.java

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 3575


🏁 Script executed:

find . -path "*/notification/domain/Notification.java" -type f | head -1 | xargs cat -n

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 3186


🏁 Script executed:

find . -path "*/notification/domain/*.java" -type f | xargs ls -la

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 876


🏁 Script executed:

find . -path "*/migration*" -o -path "*/schema*" -o -path "*sql*" | grep -E "\.(sql|java)$" | head -20

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 1878


🏁 Script executed:

find . -path "*/notification*Test.java" -type f

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 1190


🏁 Script executed:

grep -n "notification" ./backend/src/main/resources/db/migration/common/V1__init.sql -i -A 20 | head -50

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 57


🏁 Script executed:

cat -n ./backend/src/main/resources/db/migration/common/V20250825_2349001__modify_notification.sql

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 263


🏁 Script executed:

cat -n ./backend/src/main/resources/db/migration/common/V20250826_004900__modify_notification.sql

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 218


🏁 Script executed:

grep -n "CREATE INDEX\|notification" ./backend/src/main/resources/db/migration/common/V1__init.sql -i | grep -A5 -B5 notification

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 57


🏁 Script executed:

grep -rn "CREATE INDEX" ./backend/src/main/resources/db/migration/ | grep notification

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 57


🏁 Script executed:

cat -n ./backend/src/test/java/backend/mulkkam/notification/service/NotificationServiceIntegrationTest.java | head -100

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 5278


🏁 Script executed:

grep -n "cursor\|Cursor\|findByCursor" ./backend/src/test/java/backend/mulkkam/notification/service/NotificationServiceIntegrationTest.java -i

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 446


🏁 Script executed:

wc -l ./backend/src/main/resources/db/migration/common/V1__init.sql

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 139


🏁 Script executed:

sed -n '1,100p' ./backend/src/main/resources/db/migration/common/V1__init.sql

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 919


🏁 Script executed:

grep -n "notification" ./backend/src/main/resources/db/migration/common/V1__init.sql -i

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 57


🏁 Script executed:

rg -l "CREATE TABLE notification" ./backend/src/main/resources/db/migration/ -t sql

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 148


🏁 Script executed:

sed -n '140,170p' ./backend/src/test/java/backend/mulkkam/notification/service/NotificationServiceIntegrationTest.java

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 1685


🏁 Script executed:

cat -n ./backend/src/main/resources/db/migration/common/V3__add_notification.sql

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 451


🏁 Script executed:

sed -n '100,250p' ./backend/src/test/java/backend/mulkkam/notification/service/NotificationServiceIntegrationTest.java

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 7333


🏁 Script executed:

grep -n "findByCursorRows\|readNotificationsAfter" ./backend/src/main/java/backend/mulkkam/notification/service/NotificationService.java -A 15 | head -60

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 1628


🏁 Script executed:

grep -n "class ReadNotificationRow\|record ReadNotificationRow" ./backend/src/main/java/backend/mulkkam/notification/dto/ReadNotificationRow.java -A 10

Repository: woowacourse-teams/2025-mul-kkam

Length of output: 357


커서 기반 페이지네이션의 정렬 순서와 커서 조건이 일치하지 않아 데이터 손실이 발생할 수 있습니다.

findByCursorRows 메서드에서 심각한 문제가 있습니다. 52번 줄의 WHERE 조건은 n.id < :lastId만으로 커서를 판단하지만, 55번 줄의 ORDER BY는 n.createdAt DESC, n.id DESC로 정렬합니다. 이 둘이 일치하지 않으면 커서 페이지네이션에서 데이터가 손실됩니다.

발생 시나리오:

  • 알림 A: id=100, createdAt=2025-01-05 10:00
  • 알림 B: id=5, createdAt=2025-01-05 11:00

정렬 순서: B(11:00, id=5) → A(10:00, id=100)

첫 페이지가 B에서 끝나면 nextCursor=5로 설정되고, 다음 페이지 쿼리는:

WHERE n.id < 5 AND n.createdAt >= '2025-01-05' ...

이 조건에서 A(id=100)는 id < 5를 만족하지 않아 제외되므로 데이터 손실이 발생합니다.

해결 방안:
Keyset 페이지네이션으로 변경하여 커서 조건을 정렬 기준과 일치시켜야 합니다:

WHERE (n.createdAt, n.id) < (:lastCreatedAt, :lastId)
  AND n.createdAt >= :limitStartDateTime
  AND n.member.id = :memberId
ORDER BY n.createdAt DESC, n.id DESC

또는 service 계층에서 이전 페이지의 (createdAt, id) 튜플을 함께 전달받아 정렬된 결과 내에서 올바른 위치부터 조회해야 합니다.

추가로:

  • 32번 줄의 findLatestRows 변경은 초기 페이지 로드이므로 문제없습니다.
  • 쿼리 성능을 위해 (member_id, created_at DESC, id DESC) 복합 인덱스 추가를 권장합니다.
🤖 Prompt for AI Agents
backend/src/main/java/backend/mulkkam/notification/repository/NotificationRepository.java
around line 32: the cursor-based pagination WHERE clause currently uses only
n.id < :lastId while ORDER BY is n.createdAt DESC, n.id DESC, which can drop
rows; change the cursor logic to compare the tuple (n.createdAt, n.id) against
the last cursor values (lastCreatedAt, lastId) — i.e., add a condition
equivalent to (n.createdAt, n.id) < (:lastCreatedAt, :lastId) together with the
existing member and date filters, and update the method signature to accept
lastCreatedAt (or accept a cursor object containing both createdAt and id) so
the query ordering and cursor predicate match; also consider adding a composite
index on (member_id, created_at DESC, id DESC) for performance.

Comment on lines 52 to 56
WHERE n.id < :lastId
AND n.createdAt >= :limitStartDateTime
AND n.member.id = :memberId
ORDER BY n.id DESC
ORDER BY n.createdAt DESC, n.id DESC
""")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

[Critical] 커서 조건과 정렬 순서 불일치로 인한 데이터 누락 발생 가능

현재 커서 페이지네이션 로직에 심각한 결함이 있습니다. ORDER BY(createdAt DESC, id DESC) 복합 정렬인데, 커서 조건은 id < :lastId만 사용하고 있어 데이터가 누락됩니다.

문제 시나리오:

알림 A: id=100, createdAt=2025-12-14 10:00:00
알림 B: id=50,  createdAt=2025-12-14 11:00:00

정렬 결과: B(50) → A(100)  // createdAt이 더 최근인 B가 먼저

1페이지: B를 반환, lastId=50
2페이지: WHERE id < 50 조건으로 인해 A(id=100) 제외됨 ❌

왜 문제인가:

  • 복합 정렬 시 커서는 모든 정렬 컬럼을 고려해야 합니다
  • id만으로는 createdAt 역순 정렬의 연속성을 보장할 수 없습니다
  • 특히 id가 순차 증가하는 경우 시간 역순과 id 역순이 반대 방향이므로 반드시 누락 발생

해결 방안:

튜플 비교를 사용한 올바른 커서 조건으로 변경해야 합니다:

@Query("""
    SELECT new backend.mulkkam.notification.dto.ReadNotificationRow(
        n.id,
        n.createdAt,
        n.content,
        n.notificationType,
        n.isRead,
        s.recommendedTargetAmount,
        s.applyTargetAmount
    )
    FROM Notification n
    LEFT JOIN SuggestionNotification s ON s.id = n.id
    WHERE (n.createdAt < :lastCreatedAt 
           OR (n.createdAt = :lastCreatedAt AND n.id < :lastId))
      AND n.createdAt >= :limitStartDateTime
      AND n.member.id = :memberId
    ORDER BY n.createdAt DESC, n.id DESC
""")
List<ReadNotificationRow> findByCursorRows(
        Long memberId,
        Long lastId,
        LocalDateTime lastCreatedAt,  // 파라미터 추가 필요
        LocalDateTime limitStartDateTime,
        Pageable pageable
);

장점:

  • 정렬 순서와 커서 조건이 일치하여 데이터 누락/중복 없음
  • createdAt이 같은 경우에만 id로 2차 정렬 보장

단점:

  • 메서드 시그니처 변경으로 호출부 수정 필요
  • 서비스 레이어에서 lastCreatedAt도 함께 전달해야 함

Based on learnings, 커서 페이지네이션에서는 정렬 순서에 맞춰 커서 조건을 정확히 구성해야 중복 노출이나 누락을 방지할 수 있습니다.

호출부 수정과 통합 테스트 코드가 필요하시면 추가로 제안해드릴까요?

🤖 Prompt for AI Agents
backend/src/main/java/backend/mulkkam/notification/repository/NotificationRepository.java
lines 52-56: The cursor WHERE clause uses only id (< :lastId) while ORDER BY is
(createdAt DESC, id DESC), causing missed rows; change the WHERE to compare the
tuple (createdAt, id) by adding lastCreatedAt and using (n.createdAt <
:lastCreatedAt OR (n.createdAt = :lastCreatedAt AND n.id < :lastId)) in addition
to the existing limitStartDateTime and memberId filters, update the @Query to
accept the new lastCreatedAt parameter, change the repository method signature
to include LocalDateTime lastCreatedAt, and update all callers/service layer to
pass lastCreatedAt (plus add/adjust integration tests) so cursor conditions
match the ORDER BY exactly.

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

Labels

BE 백엔드 관련 이슈입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant