Skip to content

[refactor] #72 N+1 문제 해결을 위한 매칭 후보자 탐색 쿼리 최적화#113

Merged
2hyunjinn merged 8 commits intomainfrom
refactor/#72
May 7, 2025
Merged

[refactor] #72 N+1 문제 해결을 위한 매칭 후보자 탐색 쿼리 최적화#113
2hyunjinn merged 8 commits intomainfrom
refactor/#72

Conversation

@2hyunjinn
Copy link
Copy Markdown
Member

@2hyunjinn 2hyunjinn commented May 6, 2025

📌 PR 제목

[refactor] #72 N+1 문제 해결을 위한 매칭 후보자 탐색 쿼리 최적화

📌 PR 내용

매칭 요청 시 사용되는 후보자 탐색 쿼리에서 다음과 같은 구조적/성능적 문제를 해결했습니다:

  • 기존 SIZE() 기반 중첩 서브쿼리를 GROUP BY + ORDER BY COUNT() 방식으로 변경하여 해석성과 확장성 향상
  • Participant → User 연관 관계에서 발생하던 N+1 문제를 JOIN FETCH로 해결
  • 쿼리 수 고정화, 중복 쿼리 제거, 유지보수성 개선

🛠 작업 내용

✅ 1. 후보자 탐색 쿼리 리팩터링

항목 변경 전 변경 후
매칭 수 계산 SIZE() 중첩 + 서브쿼리 GROUP BY COUNT() 정렬
정렬 기준 MIN(SIZE()) 일치 대상만 추출 전체 대상 정렬 후 상위 1명 추출
user 접근 Lazy (N+1 발생) JOIN FETCH p.user 사용
실행 쿼리 수 8개 + 중복 발생 가능성 있음 고정 8개
SQL 실행 시간 약 26.0ms 약 33.5ms
유지보수성 매우 낮음 정렬 기준 변경만으로 확장 가능

✅ 2. 서비스 로직/테스트 코드 대응

  • 기존 Optional 반환 → List로 변경
  • PageRequest.of(0, 1) 적용으로 반환은 1명이지만, 테스트에서는 후보 리스트 길이 확인 가능
  • 후보가 여러 명일 때 정확히 1명만 선택하는지 확인하는 테스트 추가
  • 없는 매칭 ID 조회 시 예외 처리 테스트 케이스 보완
  • 매칭 대상이 null이거나 참가자가 없는 경우의 경계값 테스트도 반영

✅ 기대 효과

  • user.gender 접근 시 Lazy Loading 제거로 인한 N+1 문제 해결
  • COUNT() 기반 정렬 → 정렬 기준 추가/변경이 쉬움
  • 테스트 및 예외 케이스 완비로 안정성 확보

🔍 관련 이슈

Closes #72

📸 스크린샷 (Optional)

N/A

📚 레퍼런스 (Optional)

🐌 JPA에서 Lazy 로딩, 정말 느린 걸까?

Summary by CodeRabbit

  • Bug Fixes
    • Improved performance and reliability when selecting participants with the fewest matchings for candidate selection.
    • Added error handling for missing or invalid matching targets.
  • New Features
    • Added pagination support when retrieving matching candidates.
  • Tests
    • Updated test class visibility for improved encapsulation.
    • Removed unused imports from test files.
    • Added tests for error cases and candidate selection behavior.

@2hyunjinn 2hyunjinn added the refactor This doesn't seem right label May 6, 2025
@2hyunjinn 2hyunjinn self-assigned this May 6, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented May 6, 2025

"""

Walkthrough

The changes optimize the participant matching query by introducing a JOIN FETCH to eagerly load associated user entities and replacing a subquery-based minimum count filter with an ORDER BY clause for candidate selection. The repository method was renamed and updated to return a paginated list. The service layer was adjusted to handle the new method signature and pagination. Several test classes had their visibility reduced from public to package-private, and test methods were updated to reflect the repository changes, including a new test for multiple candidates.

Changes

File(s) Change Summary
src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java Renamed method to findMatchingCandidates, changed return type to List<Participant>, added Pageable parameter, updated query to use JOIN FETCH and ORDER BY COUNT() instead of subquery.
src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java Updated calls to repository to use new method with pagination, renamed variables for clarity, unwrapped and rewrapped Optionals accordingly, added null checks with exceptions.
src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java Changed class visibility to package-private, updated mocks and assertions to use list-based repository method, added test for multiple candidates, added tests for exception cases in getMatchingDetail.
src/test/java/org/festimate/team/domain/festival/entity/FestivalTest.java Changed class visibility to package-private, removed unused imports.
src/test/java/org/festimate/team/domain/festival/service/impl/FestivalServiceImplTest.java Changed class visibility to package-private.

Sequence Diagram(s)

sequenceDiagram
    participant MatchingService
    participant MatchingRepository
    participant Database

    MatchingService->>MatchingRepository: findMatchingCandidates(..., PageRequest.of(0,1))
    MatchingRepository->>Database: Query with JOIN FETCH user, LEFT JOIN Matching, GROUP BY, ORDER BY COUNT()
    Database-->>MatchingRepository: List<Participant> with User eagerly loaded
    MatchingRepository-->>MatchingService: List<Participant>
    MatchingService->>MatchingService: Stream list, findFirst candidate
    MatchingService->>MatchingService: saveMatching(Optional.ofNullable(candidate))
Loading

Assessment against linked issues

Objective Addressed Explanation
Add JOIN FETCH to load User in findMatchingCandidate ( #72 )
Replace subquery with ORDER BY COUNT() for candidate selection ( #72 )
Reduce query count and improve JDBC execution time ( #72 )

Poem

In the warren of code, a query once slow,
Now leaps with a JOIN, eager to go!
No more N+1s, just swift bunny hops,
With ORDER BY guiding to matching hilltops.
Test classes now hide, as shy rabbits do—
This patch is a treat for performance, woohoo!
🐇✨
"""


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ad98d6a and 0f645a5.

📒 Files selected for processing (1)
  • src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java (8 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test
🔇 Additional comments (10)
src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java (10)

24-24: Import additions align with pagination implementation

These imports support the pagination changes introduced to address the N+1 problem. The PageRequest is used for pagination parameters, and Collections is used for empty list returns in mocked repository responses.

Also applies to: 29-29


126-129: Method signature update correctly implements query optimization

The repository method has been properly updated from findMatchingCandidate to findMatchingCandidates, returning a list instead of an optional and using pagination with PageRequest.of(0, 1). This change supports the JOIN FETCH optimization mentioned in the PR description to solve the N+1 problem.


150-163: Second priority matching test correctly updated

The repository mocking now properly returns an empty list for the first priority and a list with one element for the second priority. This update correctly tests the prioritization logic with the new repository method signature.


183-191: Third priority matching test correctly reflects repository changes

The test has been properly updated to mock multiple repository calls, returning empty lists for the first and second priorities, and a list with one candidate for the third priority. The updated method calls maintain the test's original intent while adapting to the optimized query structure.


211-217: Already matched candidate test properly updated

All repository method calls were correctly updated to return empty lists, maintaining the test's purpose of verifying behavior when no candidates are available due to previous matches.


235-237: Empty result test appropriately modified

The test now properly uses explicit parameters for the repository method call and handles the list return type, maintaining the original test's intent of verifying behavior when no candidates are found.


258-262: Invalid festival test updated to handle null checks

The test now correctly sets a non-null targetParticipant and COMPLETED status, which aligns with the updated null checks in the service implementation. This ensures the test fails for the correct reason (festival mismatch) rather than null checks.


276-302: New test for pending matching scenario

This new test correctly verifies that querying a pending matching (with null targetParticipant) throws a TARGET_NOT_FOUND exception. This test is important for validating the new null checks added to the service implementation.


304-331: New test for non-existent matching scenario

This test properly verifies that attempting to get details for a non-existent matching ID throws the appropriate TARGET_NOT_FOUND exception. It helps ensure the service handles missing data correctly.


333-356: New test for multiple candidates scenario

This test correctly verifies that when multiple candidates are returned by the repository, only the first candidate is selected. This test is important for validating the pagination behavior where only the first page with limit 1 is requested, confirming that the service layer properly handles the repository's new return type.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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.
    • Generate unit testing code for this file.
    • 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. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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

Documentation and Community

  • 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
Copy Markdown

github-actions bot commented May 6, 2025

Test Results

44 tests   44 ✅  1s ⏱️
10 suites   0 💤
10 files     0 ❌

Results for commit 0f645a5.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java (1)

16-18: Consider adding DISTINCT to avoid accidental duplicates after the JOIN FETCH.

p.user is probably a @ManyToOne, so in theory a duplicate row cannot be produced.
However, if the mapping is ever switched to @OneToMany / @ManyToMany in the future (or further JOIN FETCHes are appended), the current query would suddenly start returning duplicates and break the single-row expectation of the method.
A defensive change keeps the query stable:

-SELECT p FROM Participant p
-JOIN FETCH p.user
+SELECT DISTINCT p FROM Participant p
+JOIN FETCH p.user
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a5b2e32 and e5c8fac.

📒 Files selected for processing (5)
  • src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java (1 hunks)
  • src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java (1 hunks)
  • src/test/java/org/festimate/team/domain/festival/entity/FestivalTest.java (1 hunks)
  • src/test/java/org/festimate/team/domain/festival/service/impl/FestivalServiceImplTest.java (1 hunks)
  • src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java (1 hunks)
🔇 Additional comments (5)
src/test/java/org/festimate/team/domain/festival/service/impl/FestivalServiceImplTest.java (1)

21-21: Improved visibility modifier for test class.

Changing the visibility from public to package-private follows best practices for test classes, as they are typically only used within their package.

src/test/java/org/festimate/team/domain/festival/entity/FestivalTest.java (1)

14-14: Improved visibility modifier for test class.

Changing the visibility from public to package-private follows best practices for test classes, as they are typically only used within their package.

src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java (1)

33-33: Improved visibility modifier for test class.

Changing the visibility from public to package-private follows best practices for test classes, as they are typically only used within their package.

src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java (1)

50-53: Improved variable naming and Optional handling.

The renaming from targetParticipantOptional to targetOptional makes the code more concise, and the explicit unwrapping with orElse(null) followed by rewrapping with Optional.ofNullable() makes the flow clearer.

This change aligns with the PR objective of optimizing the matching candidate search logic by making the code more explicit about when and how optional values are accessed.

src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java (1)

22-27: Previous matches are filtered only in one direction – is that intentional?

The sub-query excludes candidates when the caller was applicant and the candidate was target.
If a completed matching exists in the opposite direction (caller was the target), the candidate is still returned.
If “matched once means never match again” is the rule, this condition should be symmetric:

-AND p.participantId NOT IN (
-    SELECT m.targetParticipant.participantId
-    FROM Matching m
-    WHERE m.applicantParticipant.participantId = :participantId
-      AND m.status = 'COMPLETED'
-)
+AND p.participantId NOT IN (
+    SELECT CASE
+             WHEN m.applicantParticipant.participantId = :participantId
+               THEN m.targetParticipant.participantId
+             ELSE m.applicantParticipant.participantId
+           END
+    FROM Matching m
+    WHERE (m.applicantParticipant.participantId = :participantId
+           OR m.targetParticipant.participantId = :participantId)
+      AND m.status = 'COMPLETED'
+)

Please confirm the business rule to avoid repeat pairings in the opposite direction.

Copy link
Copy Markdown

@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 (2)
src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java (2)

17-31: Consider adding a comment explaining the query optimization.

The query optimization is well-implemented, but it would be beneficial to add a comment explaining the approach and its performance benefits for future maintainers.

 @Query("""
+                // Optimized query to solve N+1 problem by eagerly loading users
+                // and ordering participants by their matching count in ascending order
                 SELECT p FROM Participant p
                 JOIN FETCH p.user
                 LEFT JOIN Matching m1
                        ON (m1.applicantParticipant = p OR m1.targetParticipant = p)
                 WHERE p.festival.festivalId = :festivalId
                   AND p.typeResult = :typeResult
                   AND p.user.gender != :gender
                   AND p.participantId != :participantId
                   AND p.participantId NOT IN (
                       SELECT m.targetParticipant.participantId
                       FROM Matching m
                       WHERE m.applicantParticipant.participantId = :participantId
                         AND m.status = 'COMPLETED'
                   )
                 GROUP BY p
                 ORDER BY COUNT(m1) ASC
             """)

41-41: Consider consistent spacing.

There's an extra blank line that could be removed to maintain consistent spacing between repository methods.

 );

-

 @Query("""
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e5c8fac and a4bd031.

📒 Files selected for processing (3)
  • src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java (1 hunks)
  • src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java (3 hunks)
  • src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java
  • src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java
🔇 Additional comments (5)
src/main/java/org/festimate/team/domain/matching/repository/MatchingRepository.java (5)

7-7: Appropriate addition of Pageable import.

The addition of Pageable supports the new pagination capability in the repository method, which is consistent with the optimizations mentioned in the PR objectives.


17-19: Great optimization with JOIN FETCH and improved join strategy!

The JOIN FETCH p.user correctly addresses the N+1 query problem by eagerly loading the User entities associated with Participants. The LEFT JOIN with the OR condition is an efficient approach for counting relationships where the participant appears on either side.


25-26: Clean subquery formulation.

The subquery is well-structured and correctly identifies participants that have already been matched with the current participant.


30-31: Effective replacement of subquery with GROUP BY and ORDER BY.

This change replaces the previous subquery-based minimum count filter with a more efficient approach using GROUP BY and ORDER BY COUNT, which should perform better, especially with larger datasets.


33-39: Repository method signature properly updated for pagination.

The method has been appropriately renamed from findMatchingCandidate to findMatchingCandidates and now returns a List<Participant> with pagination support. This addresses the potential NonUniqueResultException mentioned in previous reviews while maintaining the performance benefits.

2hyunjinn added 4 commits May 7, 2025 19:43
# Conflicts:
#	src/main/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImpl.java
#	src/test/java/org/festimate/team/domain/matching/service/impl/MatchingServiceImplTest.java
@2hyunjinn 2hyunjinn merged commit e707930 into main May 7, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor This doesn't seem right

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[refactor] N+1 문제 해결을 위한 매칭 후보자 탐색 쿼리 최적화

1 participant