Skip to content

Conversation

@jigun058
Copy link
Collaborator

@jigun058 jigun058 commented Aug 31, 2025

📌 PR 제목

  • feat: 기능 추가
  • fix: 버그 수정
  • refactor: 리팩토링

✅ 작업 내용

  • getCourse의 userCourse 불러오는 로직을 수정해 성능을 개선하였습니다.

🔍 체크리스트

  • [ ✅] 테스트를 완료했나요?
  • 코드 리뷰를 받았나요?
  • 변경된 API 문서를 반영했나요?

📸 스크린샷 (UI 변경이 있다면)

🔗 관련 이슈

Summary by CodeRabbit

  • 리팩터링
    • 코스 목록에서 각 코스의 최신 학습 상태를 직접 조회하는 방식으로 변경하여 상태 계산 경로를 단순화했습니다.
    • 페이지 단위로 필요한 코스에 대해서만 최신 상태를 가져오도록 최적화해 코스 목록 로딩 성능과 일관성을 개선했습니다.

@jigun058 jigun058 requested a review from soonga00 August 31, 2025 10:35
@jigun058 jigun058 self-assigned this Aug 31, 2025
@jigun058 jigun058 linked an issue Aug 31, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Aug 31, 2025

Walkthrough

사용자별 코스 목록 조회 흐름에서 최신 UserCourse 상태 계산을 스트림/그루핑 방식 대신, 코스 ID 리스트 기반의 전용 레포지토리 쿼리와 서비스 메서드로 대체했습니다. 레포지토리에 최신 레코드 조회용 JPQL이 추가되고, 서비스는 이를 맵으로 변환해 파사드에서 사용합니다.

Changes

Cohort / File(s) Summary
Repository: 최신 UserCourse 조회 쿼리 추가
src/main/java/com/aloc/aloc/course/repository/UserCourseRepository.java
findLatestUserCoursesByUserAndCourseIds(User, List<Long>) 추가. (user, course)별 MAX(createdAt) 기준 최신 UserCourse를 코스 ID 리스트로 필터링해 조회하는 JPQL 도입.
Service: 상태 맵 변환 API 추가
src/main/java/com/aloc/aloc/course/service/UserCourseService.java
getLatestUserCourseStates(User, List<Long>) 추가. 레포지토리 결과를 Map<courseId, UserCourseState>로 변환해 제공.
Facade: 페이지 단위 상태 계산 경량화
src/main/java/com/aloc/aloc/user/service/facade/UserFacade.java
getCoursesByUser에서 페이지의 코스 ID를 수집 후 서비스의 상태 맵을 조회해 기본값 NOT_STARTED로 매핑. 종전의 스트림 기반 최신 항목 계산 제거.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant F as UserFacade
  participant S as UserCourseService
  participant R as UserCourseRepository
  participant DB as Database

  C->>F: GET /api/courses (paged)
  F->>F: 페이지 코스 목록에서 courseIds 수집
  F->>S: getLatestUserCourseStates(user, courseIds)
  S->>R: findLatestUserCoursesByUserAndCourseIds(user, courseIds)
  R->>DB: SELECT latest UserCourse by (user, courseId IN list)
  DB-->>R: List<UserCourse>
  R-->>S: List<UserCourse>
  S->>S: Map<courseId, state>로 변환
  S-->>F: Map<Long, UserCourseState>
  F->>F: 각 코스에 상태 할당 (기본 NOT_STARTED)
  F-->>C: 응답 (코스 + 상태)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Assessment against linked issues

Objective Addressed Explanation
GET /api/courses 지연시간 감소를 위한 조회/계산 경량화 (#232)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
- -

Possibly related issues

  • (없음)

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 232-refactor-get-apicourses-지연시간-줄이기

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
🪧 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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

Copy link

@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/com/aloc/aloc/user/service/facade/UserFacade.java (1)

281-285: 컨텐츠 없음일 때 불필요한 레포 호출 회피

페이지가 비어 있으면 레포 호출을 생략하고 즉시 빈 페이지(혹은 NOT_STARTED 매핑)로 반환하세요. 서비스에서 빈 리스트 가드를 추가하더라도, 상위에서 호출 자체를 막는 편이 안전하고 가볍습니다.

 Page<Course> courses = courseService.getCoursePageByCourseType(pageable, courseTypeOrNull);
 
-List<Long> courseIds = courses.getContent().stream().map(Course::getId).toList();
+List<Long> courseIds = courses.getContent().stream().map(Course::getId).toList();
+if (courseIds.isEmpty()) {
+  return courses.map(course -> CourseResponseDto.of(course, UserCourseState.NOT_STARTED));
+}
 
 Map<Long, UserCourseState> userCourseStateMap =
     userCourseService.getLatestUserCourseStates(user, courseIds);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6e0a53b and 90d8c6e.

📒 Files selected for processing (3)
  • src/main/java/com/aloc/aloc/course/repository/UserCourseRepository.java (1 hunks)
  • src/main/java/com/aloc/aloc/course/service/UserCourseService.java (2 hunks)
  • src/main/java/com/aloc/aloc/user/service/facade/UserFacade.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/com/aloc/aloc/course/repository/UserCourseRepository.java (2)
src/main/java/com/aloc/aloc/problem/repository/UserCourseProblemRepository.java (1)
  • UserCourseProblemRepository (15-31)
src/main/java/com/aloc/aloc/course/repository/CourseRepository.java (1)
  • CourseRepository (13-27)
src/main/java/com/aloc/aloc/user/service/facade/UserFacade.java (2)
src/main/java/com/aloc/aloc/course/service/CourseService.java (1)
  • getCourses (26-29)
src/main/java/com/aloc/aloc/usercourse/entity/UserCourse.java (1)
  • updateUserCourseState (63-65)
src/main/java/com/aloc/aloc/course/service/UserCourseService.java (1)
src/main/java/com/aloc/aloc/usercourse/entity/UserCourse.java (1)
  • updateUserCourseState (63-65)
🔇 Additional comments (2)
src/main/java/com/aloc/aloc/course/service/UserCourseService.java (1)

17-19: 불필요한 변경 없음 – 임포트 추가 OK

Map/Collectors 임포트 추가는 합당합니다.

src/main/java/com/aloc/aloc/user/service/facade/UserFacade.java (1)

289-289: 기본값 처리 LGTM

getOrDefault(..., NOT_STARTED) 기본값 전략 적절합니다.

Comment on lines +49 to 61
@Query(
"""
SELECT uc FROM UserCourse uc
WHERE uc.user = :user AND uc.course.id IN :courseIds
AND uc.createdAt = (
SELECT MAX(uc2.createdAt)
FROM UserCourse uc2
WHERE uc2.user = :user AND uc2.course.id = uc.course.id
)
""")
List<UserCourse> findLatestUserCoursesByUserAndCourseIds(
@Param("user") User user, @Param("courseIds") List<Long> courseIds);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타이브레이커 없는 상관 서브쿼리 → 중복 행 가능성; toMap 충돌/비결정성 위험

createdAt = MAX(createdAt)만으로는 동률 시 다건 반환 가능성이 있습니다. 서비스의 toMap 병합으로 임시 방어하더라도, 근본적으로 쿼리에서 단일 행이 선택되도록 타이브레이커를 넣어주세요. 일반적으로 단조 증가 PK(id)를 사용하는 것이 간단하고 DB에 독립적입니다.

-@Query(
-    """
-  SELECT uc FROM UserCourse uc
-  WHERE uc.user = :user AND uc.course.id IN :courseIds
-  AND uc.createdAt = (
-    SELECT MAX(uc2.createdAt)
-    FROM UserCourse uc2
-    WHERE uc2.user = :user AND uc2.course.id = uc.course.id
-  )
-"""
-)
+@Query(
+    """
+  SELECT uc FROM UserCourse uc
+  WHERE uc.user = :user
+    AND uc.course.id IN :courseIds
+    AND uc.id = (
+      SELECT MAX(uc2.id)
+      FROM UserCourse uc2
+      WHERE uc2.user = :user
+        AND uc2.course.id = uc.course.id
+    )
+"""
+)
 List<UserCourse> findLatestUserCoursesByUserAndCourseIds(
     @Param("user") User user, @Param("courseIds") List<Long> courseIds);

보완 제안:

  • IN :courseIds는 빈 리스트에서 예외가 나므로, 상위 서비스에서 빈 리스트 조기 리턴(본 리뷰의 Service 코멘트)으로 방지하세요.
  • 성능: (user_id, course_id, id) 또는 (user_id, course_id, created_at) 복합 인덱스를 고려하세요(아래 참조).

인덱스 제안:

  • MySQL/InnoDB 기준
    • ALTER TABLE user_course ADD INDEX idx_user_course_latest (user_id, course_id, id);
📝 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.

Suggested change
@Query(
"""
SELECT uc FROM UserCourse uc
WHERE uc.user = :user AND uc.course.id IN :courseIds
AND uc.createdAt = (
SELECT MAX(uc2.createdAt)
FROM UserCourse uc2
WHERE uc2.user = :user AND uc2.course.id = uc.course.id
)
""")
List<UserCourse> findLatestUserCoursesByUserAndCourseIds(
@Param("user") User user, @Param("courseIds") List<Long> courseIds);
}
@Query(
"""
SELECT uc FROM UserCourse uc
WHERE uc.user = :user
AND uc.course.id IN :courseIds
AND uc.id = (
SELECT MAX(uc2.id)
FROM UserCourse uc2
WHERE uc2.user = :user
AND uc2.course.id = uc.course.id
)
"""
)
List<UserCourse> findLatestUserCoursesByUserAndCourseIds(
@Param("user") User user, @Param("courseIds") List<Long> courseIds);
}
🤖 Prompt for AI Agents
In src/main/java/com/aloc/aloc/course/repository/UserCourseRepository.java
around lines 49 to 61, the subquery only compares createdAt which can return
multiple rows on ties leading to nondeterministic duplicates downstream; change
the selection to guarantee a single row by using a tiebreaker on the
monotonically increasing PK (id) — e.g., replace the createdAt = (SELECT
MAX(...)) condition with id = (SELECT MAX(uc2.id) ... WHERE uc2.user = :user AND
uc2.course.id = uc.course.id) or add an additional id = subcondition so ties on
createdAt resolve to the latest id; also ensure the calling service
short-circuits when courseIds is empty to avoid IN () issues, and add a
composite index like (user_id, course_id, id) at the DB level for performance.

Comment on lines +133 to +139
public Map<Long, UserCourseState> getLatestUserCourseStates(User user, List<Long> courseIds) {
List<UserCourse> latestUserCourses =
userCourseRepository.findLatestUserCoursesByUserAndCourseIds(user, courseIds);

return latestUserCourses.stream()
.collect(Collectors.toMap(uc -> uc.getCourse().getId(), UserCourse::getUserCourseState));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

빈 목록 대비 및 중복 키 안전성 보강 필요 (빈 IN 예외, toMap 충돌 가능성)

  • courseIds가 빈 리스트일 때 JPA IN (:courseIds)는 런타임 예외를 유발할 수 있습니다. 서비스 레벨에서 조기 리턴하세요.
  • 저장 시각 동률 등으로 레포지토리 쿼리가 동일 코스에 대해 2건을 반환하면 Collectors.toMap이 중복 키로 터질 수 있습니다(병합 함수 없음). 방어적으로 병합 함수를 추가하거나(임시 방편), 레포지토리 쿼리에서 타이브레이커를 보장하세요(아래 레포 코멘트 참조).

다음과 같이 조치 제안드립니다.

 @Service
 @AllArgsConstructor
 public class UserCourseService {
@@
-  public Map<Long, UserCourseState> getLatestUserCourseStates(User user, List<Long> courseIds) {
+  @Transactional(readOnly = true)
+  public Map<Long, UserCourseState> getLatestUserCourseStates(User user, List<Long> courseIds) {
+    if (courseIds == null || courseIds.isEmpty()) {
+      return java.util.Collections.emptyMap();
+    }
     List<UserCourse> latestUserCourses =
         userCourseRepository.findLatestUserCoursesByUserAndCourseIds(user, courseIds);
 
-    return latestUserCourses.stream()
-        .collect(Collectors.toMap(uc -> uc.getCourse().getId(), UserCourse::getUserCourseState));
+    return latestUserCourses.stream()
+        .collect(
+            Collectors.toMap(
+                uc -> uc.getCourse().getId(),
+                UserCourse::getUserCourseState,
+                (left, right) -> left // 레포 타이브레이커 적용 전 임시 병합
+            ));
   }

부가 제안(선택): 레포 쿼리를 코스 ID와 상태만 프로젝션으로 가져오면(예: select uc.course.id, uc.userCourseState) 엔티티 하이드레이션을 줄일 수 있습니다.

🤖 Prompt for AI Agents
In src/main/java/com/aloc/aloc/course/service/UserCourseService.java around
lines 133-139, handle the case where courseIds is empty by returning an empty
map early to avoid JPA IN(:courseIds) runtime errors, and make the stream
collection defensive against duplicate course IDs by providing a merge function
to Collectors.toMap (e.g., pick the latest/first value or resolve
deterministically) or by deduplicating/ordering latestUserCourses before
collecting; optionally consider changing the repository to return only courseId
and state projection to avoid hydrating full entities.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: GET /api/courses 지연시간 줄이기

2 participants