Skip to content

Conversation

@minwoo1999
Copy link
Member

@minwoo1999 minwoo1999 commented Aug 27, 2025

📘 작업 유형

📙 작업 내역

  • ReadingRecordTagRepository 인터페이스에 findByReadingRecordIdIn 메서드 추가
  • JpaReadingRecordTagRepositoryfindByReadingRecordIdIn 메서드 추가
  • ReadingRecordTagRepositoryImpl 구현체에 findByReadingRecordIdIn 메서드 구현
  • ReadingRecordServicegetReadingRecords 로직 개선
    • 조회된 readingRecord 들의 ID를 기반으로 태그를 일괄 조회하도록 변경
    • 성능 최적화를 위해 readingRecordTags를 미리 그룹핑 후 매핑하는 방식으로 수정

🧪 테스트 내역

  • 다수의 readingRecord 조회 시 태그가 정상적으로 매핑되는지 확인
  • 태그가 없는 경우 Page.empty(pageable) 정상 반환 확인
  • 기존 기능 동작에 영향 없음 확인

🎨 스크린샷 또는 시연 영상 (선택)

해당 없음

✅ PR 체크리스트

  • 커밋 메시지가 명확합니다
  • PR 제목이 컨벤션에 맞습니다
  • 관련 이슈 번호를 작성했습니다
  • 기능이 정상적으로 작동합니다
  • 불필요한 코드를 제거했습니다

💬 추가 설명 or 리뷰 포인트

  • 다중 readingRecordId 기반으로 태그를 조회할 수 있는 기능이 추가되어 향후 API 성능 최적화에 활용될 수 있습니다.

Summary by CodeRabbit

  • 성능 개선
    • 독서 기록 목록과 태그 로딩이 더 빨라져 목록 화면 응답 속도가 향상되었습니다.
    • 여러 기록을 한 번에 불러와 대량 데이터 처리 시 일관된 성능을 제공합니다.
  • 리팩터
    • 데이터 조회 흐름을 일괄화해 중복 호출을 줄이고 효율을 높였습니다.

@coderabbitai
Copy link

coderabbitai bot commented Aug 27, 2025

Walkthrough

읽기 기록 조회를 per-record VO 생성에서 배치 로딩으로 변경하여 읽기기록, 태그, 유저북을 일괄 조회하고 그룹핑해 ReadingRecordInfoVO를 생성하도록 흐름을 수정했습니다. ReadingRecordTag의 다건 조회 메서드도 도메인/인프라 계층에 추가되었습니다.

Changes

Cohort / File(s) Change Summary
ReadingRecord enrichment flow
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
페이징 조회 후 읽기기록 ID 수집 → 태그 다건 조회 및 그룹핑 → 단일 UserBook 로드 → 사전 로딩 데이터로 각 ReadingRecord를 ReadingRecordInfoVO로 변환. 기존 per-record VO 빌드 경로 제거 및 배치 방식 도입.
Batch tag retrieval (repo interface, JPA, impl)
domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt, infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt, infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt
리포지토리 계층에 findByReadingRecordIdIn(readingRecordIds: List<UUID>) 메서드 추가(JPA 인터페이스 및 구현체 포함). 빈 입력 시 빈 리스트 반환 처리 및 엔티티→도메인 매핑 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant S as ReadingRecordDomainService
  participant RR as ReadingRecordRepository
  participant RRT as ReadingRecordTagRepository
  participant UR as UserBookRepository
  participant T as TagRepository

  C->>S: findReadingRecordsByDynamicCondition(pageable, filters)
  S->>RR: findReadingRecordsByDynamicCondition(...)
  RR-->>S: Page<ReadingRecord>
  alt empty page
    S-->>C: Page.empty(pageable)
  else has content
    S->>RRT: findByReadingRecordIdIn(recordIds)
    RRT-->>S: List<ReadingRecordTag>
    S->>T: findByIdIn(tagIds)
    T-->>S: List<Tag>
    S->>UR: findById(userBookId)
    UR-->>S: UserBook?
    S-->>C: Page<ReadingRecordInfoVO>
  end

  rect rgba(200,230,255,0.25)
  note over S: 변경: N+1 회피를 위한 배치 로딩 및 그룹핑
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
성능 개선 및 쿼리 최적화로 N+1 제거 [#54]
불필요한 코드 제거 [#54] 일부 도메인 메서드에 미사용 가능 변수(readingRecordIds 등) 존재 여부가 불확실함.

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 BOOK-175-refactor/#54

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 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.

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.

@github-actions github-actions bot requested a review from move-hoon August 27, 2025 14:50
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: 5

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

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 62258d2 and 24eec00.

📒 Files selected for processing (4)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: CI - Pull Request
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt

[error] 113-113: Kotlin compilation error: Unresolved reference 'readingRecordTags' and 'it' on line 113.


[error] 117-117: Kotlin compilation error: Unresolved reference 'readingRecordTags' and 'it' on line 117.


[error] 120-120: Kotlin compilation error: Not enough information to infer type variable T on line 120.


[error] 121-121: Kotlin compilation error: Unresolved reference 'it' on line 121.


[error] 125-125: Kotlin compilation error: Unresolved reference 'it' on line 125.

🔇 Additional comments (1)
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt (1)

105-111: 빈 페이지 처리 로직은 적절합니다.

readingRecords.isEmpty()Page.empty(pageable) 반환은 N+1 최적화 플로우와 잘 맞습니다. 그대로 가시죠.

interface ReadingRecordTagRepository {
fun saveAll(readingRecordTags: List<ReadingRecordTag>): List<ReadingRecordTag>
fun findByReadingRecordId(readingRecordId: UUID): List<ReadingRecordTag>
fun findByReadingRecordIdIn(readingRecordIds: List<UUID>): List<ReadingRecordTag>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

배치 조회 메서드 추가는 방향 OK. 입력/결과 계약을 명시해 주세요.

빈 리스트 입력 시 반환 값(빈 리스트 권장), 중복 ID 처리, 결과 정렬 보장 여부를 KDoc에 명확히 남겨주세요. 서비스/인프라에서 이미 안전 가드를 둘 예정이라도 계약을 확실히 해두면 유지보수 비용이 줄어듭니다.

🤖 Prompt for AI Agents
In
domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt
around line 8, the new batch query method lacks a contract in KDoc; add a
concise KDoc above the method that states: (1) empty input returns an empty
list, (2) duplicate IDs in the input are allowed but do not cause duplicate
entities in the result (results correspond to matching entities once per stored
entity), (3) callers should not rely on any ordering unless explicitly requested
— if deterministic ordering is desired specify it (e.g., sorted by
readingRecordId then tag id) and implement accordingly, and (4) whether nulls
are allowed (reject nulls in the list). Keep the KDoc short and precise so
service/infrastructure callers know the expected behavior.


interface JpaReadingRecordTagRepository : JpaRepository<ReadingRecordTagEntity, UUID>, JpaReadingRecordTagQuerydslRepository {
fun findByReadingRecordId(readingRecordId: UUID): List<ReadingRecordTagEntity>
fun findByReadingRecordIdIn(readingRecordIds: List<UUID>): List<ReadingRecordTagEntity>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

빈 컬렉션 IN 처리 주의.

Spring Data JPA에서 ...In(emptyList())는 DB/벤더에 따라 예외가 날 수 있습니다. 호출부 또는 구현체에서 빈 리스트 조기 반환 가드를 두는 것을 전제로 합의해 주세요(아래 Impl 코멘트 참고).

🤖 Prompt for AI Agents
infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt
around line 9: the repository method findByReadingRecordIdIn(readingRecordIds:
List<UUID>) may throw on an empty list depending on the JPA provider; change
callers or the repo implementation to guard against empty input by returning an
empty list early instead of calling the generated query. Add a null/empty check
at call sites to return emptyList() when readingRecordIds.isEmpty(), or provide
a custom repository impl method that checks for empty collection and returns
emptyList() before invoking the JPA query.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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

♻️ Duplicate comments (1)
infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt (1)

23-28: 빈 리스트 가드 추가: 잘 처리되었습니다.

IN () 회피 및 불필요한 호출 방지에 유효합니다. 지난 코멘트 반영 확인했습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

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 24eec00 and 194dd96.

📒 Files selected for processing (2)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt (1 hunks)
⏰ 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). (1)
  • GitHub Check: build-validation
🔇 Additional comments (2)
infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt (1)

30-34: 서명 포매팅 변경만 존재: 문제 없습니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt (1)

105-108: 빈 페이지 fast-return: 적절합니다.

불필요한 후속 조회를 막아 성능 이점이 있습니다.

Comment on lines +113 to +116
val readingRecordTags = readingRecordTagRepository.findByReadingRecordIdIn(readingRecordIds)
val tagIds = readingRecordTags.map { it.tagId.value }
val tagsById = tagRepository.findByIds(tagIds).associateBy { it.id.value }

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

빈 태그일 때 DB 호출 방지 + 중복 tagIds 제거

태그가 없으면 findByIds를 건너뛰고, 중복 tagId를 제거해 바인딩 수를 줄이세요.

-        val tagIds = readingRecordTags.map { it.tagId.value }
-        val tagsById = tagRepository.findByIds(tagIds).associateBy { it.id.value }
+        val tagIds = readingRecordTags.map { it.tagId.value }.distinct()
+        val tagsById: Map<UUID, Tag> =
+            if (tagIds.isEmpty()) emptyMap()
+            else tagRepository.findByIds(tagIds).associateBy { it.id.value }
📝 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
val readingRecordTags = readingRecordTagRepository.findByReadingRecordIdIn(readingRecordIds)
val tagIds = readingRecordTags.map { it.tagId.value }
val tagsById = tagRepository.findByIds(tagIds).associateBy { it.id.value }
val readingRecordTags = readingRecordTagRepository.findByReadingRecordIdIn(readingRecordIds)
val tagIds = readingRecordTags.map { it.tagId.value }.distinct()
val tagsById: Map<UUID, Tag> =
if (tagIds.isEmpty()) emptyMap()
else tagRepository.findByIds(tagIds).associateBy { it.id.value }
🤖 Prompt for AI Agents
In
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
around lines 113 to 116, avoid calling tagRepository.findByIds when there are no
tag IDs and reduce duplicate DB bindings by deduplicating tagIds before the
query; specifically, build tagIds as a distinct collection (e.g.,
toSet()/distinct()) and if that collection is empty return or set tagsById to an
emptyMap(), otherwise call findByIds with the deduped list and associateBy id as
before.

Comment on lines +117 to +121
val tagsByReadingRecordId = readingRecordTags
.groupBy { it.readingRecordId.value }
.mapValues { (_, tags) ->
tags.mapNotNull { tagsById[it.tagId.value] }
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

가독성 니트픽: 람다 변수명 충돌 방지

tagsById와 동일 이름 사용을 피하면 읽기성이 좋아집니다.

-        val tagsByReadingRecordId = readingRecordTags
-            .groupBy { it.readingRecordId.value }
-            .mapValues { (_, tags) ->
-                tags.mapNotNull { tagsById[it.tagId.value] }
-            }
+        val tagsByReadingRecordId = readingRecordTags
+            .groupBy { it.readingRecordId.value }
+            .mapValues { (_, tagLinks) ->
+                tagLinks.mapNotNull { tagsById[it.tagId.value] }
+            }
📝 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
val tagsByReadingRecordId = readingRecordTags
.groupBy { it.readingRecordId.value }
.mapValues { (_, tags) ->
tags.mapNotNull { tagsById[it.tagId.value] }
}
val tagsByReadingRecordId = readingRecordTags
.groupBy { it.readingRecordId.value }
.mapValues { (_, tagLinks) ->
tagLinks.mapNotNull { tagsById[it.tagId.value] }
}
🤖 Prompt for AI Agents
In
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
around lines 117 to 121, avoid reusing the name "tags" in the mapValues lambda
which reduces readability and risks confusion with tagsById; rename the lambda
variable (for example to "rrTags" or "readingRecordTags") and update the inner
mapping to use that new name (e.g., rrTags.mapNotNull { tagsById[it.tagId.value]
}) so the code clearly distinguishes the grouped list from the tagsById lookup.

Comment on lines +125 to +134
return readingRecordPage.map { readingRecord ->
ReadingRecordInfoVO.newInstance(
readingRecord = readingRecord,
emotionTags = tagsByReadingRecordId[readingRecord.id.value]?.map { it.name } ?: emptyList(),
bookTitle = userBook?.title,
bookPublisher = userBook?.publisher,
bookCoverImageUrl = userBook?.coverImageUrl,
author = userBook?.author
)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

제네릭 추론 보조(필요 시만): 람다 파라미터 타입 명시

환경에 따라 드물게 Page.map 제네릭 추론 경고가 남을 수 있습니다. 발생 시 아래처럼 파라미터 타입을 명시하세요.

-        return readingRecordPage.map { readingRecord ->
+        return readingRecordPage.map { readingRecord: ReadingRecord ->
             ReadingRecordInfoVO.newInstance(
                 readingRecord = readingRecord,
                 emotionTags = tagsByReadingRecordId[readingRecord.id.value]?.map { it.name } ?: emptyList(),
                 bookTitle = userBook?.title,
                 bookPublisher = userBook?.publisher,
                 bookCoverImageUrl = userBook?.coverImageUrl,
                 author = userBook?.author
             )
         }
📝 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
return readingRecordPage.map { readingRecord ->
ReadingRecordInfoVO.newInstance(
readingRecord = readingRecord,
emotionTags = tagsByReadingRecordId[readingRecord.id.value]?.map { it.name } ?: emptyList(),
bookTitle = userBook?.title,
bookPublisher = userBook?.publisher,
bookCoverImageUrl = userBook?.coverImageUrl,
author = userBook?.author
)
}
return readingRecordPage.map { readingRecord: ReadingRecord ->
ReadingRecordInfoVO.newInstance(
readingRecord = readingRecord,
emotionTags = tagsByReadingRecordId[readingRecord.id.value]?.map { it.name } ?: emptyList(),
bookTitle = userBook?.title,
bookPublisher = userBook?.publisher,
bookCoverImageUrl = userBook?.coverImageUrl,
author = userBook?.author
)
}
🤖 Prompt for AI Agents
In
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
around lines 125 to 134, the Page.map lambda relies on implicit generic
inference which can emit warnings in some environments; explicitly declare the
lambda parameter type for readingRecord (for example: readingRecord:
ReadingRecord) in the map call to help the compiler infer generics correctly —
update the map invocation to use a typed parameter and keep the inner
construction unchanged.

Comment on lines +23 to +28
override fun findByReadingRecordIdIn(readingRecordIds: List<UUID>): List<ReadingRecordTag> {
if (readingRecordIds.isEmpty()) return emptyList()
return jpaReadingRecordTagRepository
.findByReadingRecordIdIn(readingRecordIds)
.map { it.toDomain() }
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

대규모 IN 파라미터 대비: 중복 제거 + 청크 처리 제안

대량 ID 입력 시 DB 바인드 한도/플랜 비대화 대비를 권장합니다. 안전하게 distinct()chunked()로 나눠 조회하세요.

-    override fun findByReadingRecordIdIn(readingRecordIds: List<UUID>): List<ReadingRecordTag> {
-        if (readingRecordIds.isEmpty()) return emptyList()
-        return jpaReadingRecordTagRepository
-            .findByReadingRecordIdIn(readingRecordIds)
-            .map { it.toDomain() }
-    }
+    override fun findByReadingRecordIdIn(readingRecordIds: List<UUID>): List<ReadingRecordTag> {
+        if (readingRecordIds.isEmpty()) return emptyList()
+        return readingRecordIds
+            .distinct()
+            .chunked(1_000)
+            .flatMap { chunk ->
+                jpaReadingRecordTagRepository.findByReadingRecordIdIn(chunk)
+            }
+            .map { it.toDomain() }
+    }
📝 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
override fun findByReadingRecordIdIn(readingRecordIds: List<UUID>): List<ReadingRecordTag> {
if (readingRecordIds.isEmpty()) return emptyList()
return jpaReadingRecordTagRepository
.findByReadingRecordIdIn(readingRecordIds)
.map { it.toDomain() }
}
override fun findByReadingRecordIdIn(readingRecordIds: List<UUID>): List<ReadingRecordTag> {
if (readingRecordIds.isEmpty()) return emptyList()
return readingRecordIds
.distinct()
.chunked(1_000)
.flatMap { chunk ->
jpaReadingRecordTagRepository.findByReadingRecordIdIn(chunk)
}
.map { it.toDomain() }
}

Copy link
Member

@move-hoon move-hoon left a comment

Choose a reason for hiding this comment

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

LGTM~~

너무 늦어서 죄송합니다!!

@minwoo1999 minwoo1999 merged commit b603502 into develop Sep 8, 2025
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-175/refactor] 쿼리 최적화를 실시합니다.

3 participants