Skip to content

Fix/94/search#95

Merged
icarus0616 merged 2 commits into
devfrom
fix/94/search
Jan 30, 2026
Merged

Fix/94/search#95
icarus0616 merged 2 commits into
devfrom
fix/94/search

Conversation

@millkk04
Copy link
Copy Markdown
Collaborator

@millkk04 millkk04 commented Jan 30, 2026

Summary by CodeRabbit

  • New Features

    • Added Redis-backed caching for improved performance and reduced latency.
    • Implemented home screen API endpoint (GET /api/v1/home) returning real-time rankings and categorized bestsellers.
    • Enabled asynchronous metadata enrichment for book data.
  • Bug Fixes

    • Fixed build artifact naming to correctly generate booklog.jar.
    • Enhanced authentication handling in search endpoints.
  • Chores

    • Added Redis and caching dependencies.
    • Configured async and cache management.
    • Updated application configuration for Redis connectivity.
  • Documentation

    • Added comprehensive guides for deployment, Redis setup, caching strategy, and API specifications.

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

@millkk04 millkk04 linked an issue Jan 30, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 30, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive Redis-based caching infrastructure for book metadata, implements a new home screen API endpoint returning real-time rankings and bestsellers, adds asynchronous metadata enrichment from Kakao API, and refactors search controller authentication. Build configuration updated with cache and Redis dependencies. Extensive documentation added covering deployment, caching strategy, and Redis installation.

Changes

Cohort / File(s) Summary
Build & Infrastructure
build.gradle, application.yaml
Added Spring Boot cache and Redis starter dependencies. Disabled test task and plain JAR task; set bootJar archiveFileName to booklog.jar. Configured Redis connection settings, cache type, TTL (6 hours), and statistics collection in application.yaml.
Caching Configuration
global/config/RedisCacheConfig.java, global/config/AsyncConfig.java
RedisCacheConfig implements Redis-backed CacheManager with fallback to in-memory ConcurrentMapCacheManager on Redis unavailability; includes per-cache TTLs (homeBooks: 6h, bookMetadata: 7d) and CacheErrorHandler for graceful degradation. AsyncConfig provides ThreadPoolTaskExecutor (core: 5, max: 10, queue: 100) for async Kakao API calls.
Home Screen API Implementation
domain/home/controller/HomeController.java, domain/home/service/HomeService.java, domain/home/service/HomeServiceImpl.java
HomeController routes GET /api/v1/home to HomeServiceImpl. HomeServiceImpl assembles real-time rankings (TOP 20) and mood/writing-style/immersion bestsellers from PM-provided title-to-ID mapping, batches BookMetadataService queries, and applies per-section builders with fallback handling. Caching applied to getHomeData().
Home Screen DTOs
domain/home/dto/BookSummary.java, domain/home/dto/HomeResponse.java, domain/home/dto/RealTimeRankingSection.java, domain/home/dto/TaggedBooksSection.java
Java records modeling home response structure: BookSummary (bookId, title, author, publisher, coverImageUrl, ranking), RealTimeRankingSection (sectionTitle, rankings), TaggedBooksSection (tagName, books), and composite HomeResponse containing all sections.
Book Metadata Service
domain/home/service/BookMetadataService.java
New service handling book metadata retrieval and enrichment. Implements DB-first lookup with Redis caching; triggers asynchronous enrichment via Kakao API if thumbnail/publisher missing. Bulk operation getBookSummaries deduplicates, fetches, creates missing entries, and returns partial summaries. Helper methods for ISBN parsing and metadata checks.
Repository Enhancements
domain/library/books/repository/BooksRepository.java
Added findByTitle(String) and findAllByTitleIn(List) with JPQL fetch-join query to eagerly load Books with BookAuthors and Authors for batch home screen queries.
Authentication Updates
domain/search/controller/SearchController.java
Replaced Long userId parameters with Spring Security's @AuthenticationPrincipal CustomUserDetails in saveSearchKeyword() and getRecentSearches() methods; updated internal calls to extract userId from userDetails.
Documentation
docs/BOOK_METADATA_CACHING_STRATEGY.md, docs/CACHING_IMPLEMENTATION_SUMMARY.md, docs/FINAL_DEPLOYMENT_GUIDE.md, docs/HOME_API_IMPLEMENTATION_SUMMARY.md, docs/HOME_API_SPECIFICATION.md, docs/REDIS_FAULT_TOLERANCE.md, docs/REDIS_INSTALLATION_GUIDE.md, docs/UPDATE_NOTES_20260127.md
Comprehensive documentation covering Redis caching strategy and TTLs, end-to-end implementation details with code snippets, deployment procedures, API specification with sample responses, Redis installation for Windows (Memurai/Docker/MSI), fault-tolerance design, and update notes reflecting PM-provided dataset completeness without TOP 3 limitation.
Test Removal
src/test/java/com/example/booklog/domain/booklog/service/BooklogPostServiceImplTest.java
Deleted entire BooklogPostServiceImplTest.java unit test file (128 lines), removing JUnit 5 + Mockito tests for BooklogPostServiceImpl.create_success flow.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant HomeController
    participant HomeServiceImpl
    participant BookMetadataService
    participant BooksRepository
    participant RedisCache as Redis/Cache
    participant KakaoAPI as Kakao API
    participant Database

    Client->>HomeController: GET /api/v1/home
    HomeController->>HomeServiceImpl: getHomeData()
    
    alt Cache Hit
        HomeServiceImpl->>RedisCache: lookup homeBooks
        RedisCache-->>HomeServiceImpl: return cached HomeResponse
        HomeServiceImpl-->>HomeController: HomeResponse
    else Cache Miss
        HomeServiceImpl->>HomeServiceImpl: collectAllBookInfos()<br/>(from PM mapping)
        HomeServiceImpl->>BookMetadataService: getBookSummaries(List<BookInfo>)
        
        BookMetadataService->>BooksRepository: findAllByTitleIn(titles)
        BooksRepository->>Database: query Books with<br/>fetch-join Authors
        Database-->>BooksRepository: Books list
        BooksRepository-->>BookMetadataService: Books list
        
        loop For each book needing enrichment
            BookMetadataService->>BookMetadataService: enrichMetadataAsync()<br/>(`@Async`)
            BookMetadataService->>KakaoAPI: fetch metadata (title)
            KakaoAPI-->>BookMetadataService: thumbnail, ISBN, publisher
            BookMetadataService->>Database: update Books entity
            Database-->>BookMetadataService: ack
        end
        
        BookMetadataService-->>HomeServiceImpl: List<BookSummary>
        HomeServiceImpl->>HomeServiceImpl: buildRealTimeRanking()<br/>buildMoodBestsellers()<br/>buildWritingStyleBestsellers()<br/>buildImmersionBestsellers()
        HomeServiceImpl->>RedisCache: cache HomeResponse<br/>(1 hour TTL)
        HomeServiceImpl-->>HomeController: HomeResponse
    end
    
    HomeController-->>Client: 200 OK + HomeResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • jaehyeon4406
  • ktg3891

Poem

🐰 Hops with glee, a cache so fine!
Redis springs to life, books align.
Async whispers fetch the tome,
Home screen glows—welcome home! 🏠✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix/94/search' is vague and does not clearly describe the main changes in this changeset, which include Redis caching setup, home API implementation, documentation, and search controller authentication updates. Consider a more descriptive title that reflects the primary focus, such as 'Add Redis caching and home API with metadata enrichment' or 'Implement home screen API with Redis-backed metadata caching'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/94/search

Tip

🧪 Unit Test Generation v2 is now available!

We have significantly improved our unit test generation capabilities.

To enable: Add this to your .coderabbit.yaml configuration:

reviews:
  finishing_touches:
    unit_tests:
      enabled: true

Try it out by using the @coderabbitai generate unit tests command on your code files or under ✨ Finishing Touches on the walkthrough!

Have feedback? Share your thoughts on our Discord thread!


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.

@millkk04 millkk04 requested a review from icarus0616 January 30, 2026 12:36
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: 18

🤖 Fix all issues with AI agents
In `@booklog/build.gradle`:
- Around line 74-77: The build.gradle currently disables the test task
unconditionally via tasks.named('test') { enabled = false }, which prevents
local test runs; change this to respect a skip flag instead: remove the
unconditional enabled = false and use a conditional check on a Gradle project
property (e.g., project.hasProperty('skipTests') or
!project.hasProperty('runTests')) inside the tasks.named('test') block so tests
run by default and only skip when the -PskipTests flag is passed; ensure
useJUnitPlatform() remains configured in the same tasks.named('test') block.

In `@booklog/docs/BOOK_METADATA_CACHING_STRATEGY.md`:
- Around line 102-106: The table rows (e.g., entries for `homeBooks` /
`home:all` and `bookMetadata` / `book:{bookId}`) use pipe characters without
surrounding spaces which triggers MD060; update every markdown table in the
document so each column separator has a single space on both sides of the pipe
(e.g., " | " between columns) and ensure header separator rows match that
spacing style; apply the same fix consistently to all other tables in
BOOK_METADATA_CACHING_STRATEGY.md.
- Around line 11-29: This MD040 lint error is from unlabeled fenced code blocks
in BOOK_METADATA_CACHING_STRATEGY.md; update each triple-backtick fence (e.g.,
the block that starts with ``` before the flow diagram) to include an explicit
language identifier like text (```text) or java/bash where appropriate so
markdownlint passes and readability improves.

In `@booklog/docs/CACHING_IMPLEMENTATION_SUMMARY.md`:
- Around line 52-115: Update the Markdown by adding language identifiers to all
fenced code blocks (e.g., ```text for plain text blocks, ```bash or ```java
where snippets show commands or code) so markdownlint stops flagging them, and
normalize the performance table’s pipe spacing so each column separator has a
single space on both sides (fix the header and all rows), including the code
blocks under the "시나리오" sections and the performance table titled "📊 성능 개선 효과".

In `@booklog/docs/FINAL_DEPLOYMENT_GUIDE.md`:
- Around line 5-7: Fix the markdownlint warnings by labeling unlabeled code
fences (e.g., change the fence containing "BUILD SUCCESSFUL in 4s" to ```text
... ```), convert bare URLs like "https://www.memurai.com/get-memurai" to
angle-bracket form "<https://www.memurai.com/get-memurai>", and normalize table
separator rows to include spaces around cells (e.g., change
"|---------|----------|-------------|" to "| -------- | --------- |
------------- |"); apply the same fixes to the other occurrences flagged (lines
referenced in the review).

In `@booklog/docs/HOME_API_IMPLEMENTATION_SUMMARY.md`:
- Around line 19-25: The fenced code block listing DTO names (BookSummary,
RealTimeRankingSection, TaggedBooksSection, HomeResponse) lacks a language
specifier; update the triple-backtick fence to include a language token (e.g.,
```text) so markdownlint MD040 is satisfied and the block is treated as plain
text.

In `@booklog/docs/HOME_API_SPECIFICATION.md`:
- Around line 826-836: The spec currently shows `@Cacheable`(value = "homeData",
key = "'home'") and recommends a 1-hour TTL, but the runtime cache is configured
as homeBooks with a 6-hour TTL; reconcile by updating the spec to reference the
actual cache name and TTL used in the codebase (e.g., change the `@Cacheable`
value to "homeBooks" in examples or explicitly state that homeBooks is used) and
set the documented TTL to 6 hours so the documentation matches the cache
configuration (or alternately change the cache config to 1 hour if you prefer
the doc behavior).

In `@booklog/docs/REDIS_FAULT_TOLERANCE.md`:
- Around line 15-27: The markdown has unlabeled fenced code blocks and
improperly padded table separator rows causing MD040/MD060; update each affected
fenced block (e.g., the flow diagram blocks shown around the snippet and at
ranges 35-51, 59-64, 145-149, 175-179, 203-213, 226-234) by adding a language
tag such as "text" (e.g., ```text) and ensure any Markdown table separator rows
include spaces around pipes and dashes for padding; apply these changes across
all listed ranges so linting warnings are resolved.

In `@booklog/docs/REDIS_INSTALLATION_GUIDE.md`:
- Line 53: Replace the bare URL "https://github.com/tporadowski/redis/releases"
with a Markdown link for consistency (e.g. [tporadowski/redis
Releases](https://github.com/tporadowski/redis/releases)) so the URL is
displayed as a clickable link in REDIS_INSTALLATION_GUIDE.md; locate the plain
URL string and convert it to the bracketed link form.
- Line 24: Replace the bare URL
"https://www.docker.com/products/docker-desktop/" with a markdown link for
consistency; e.g., change the line containing that URL to use link syntax such
as [Docker Desktop](https://www.docker.com/products/docker-desktop/) so the URL
is rendered as a proper markdown link.
- Line 6: Replace the bare URL "https://www.memurai.com/get-memurai" with a
Markdown link by enclosing the URL in square brackets for link text and
parentheses for the URL (for example: [Memurai -
Get](https://www.memurai.com/get-memurai)); update the line containing that
exact URL so it becomes a proper Markdown link.
- Around line 99-102: The fenced code block showing Redis/Lettuce logs should
include a language identifier for proper rendering; locate the block containing
the lines "INFO ... - Lettuce connection initialized" and "INFO ... - Created
new connection to localhost:6379" and change the opening fence from ``` to
```text (or another appropriate language like ```log) so the block becomes
```text and closing fence remains ```; ensure there are no extra backticks or
spacing changes around the block.

In `@booklog/docs/UPDATE_NOTES_20260127.md`:
- Around line 93-96: The markdown code fence around the build output in
UPDATE_NOTES_20260127.md is missing a language tag (MD040); update the opening
triple-backtick fence for the block containing "BUILD SUCCESSFUL in 15s" and "5
actionable tasks: 5 executed" to use a language hint (e.g., change ``` to
```text) so the block is explicitly marked as plain text.

In
`@booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java`:
- Around line 43-64: The methods getBookSummary(...) and getBookSummaries(...)
are annotated with `@Transactional`(readOnly = true) but call
booksRepository.save(...) (e.g., save(newBook) in getBookSummary and save(...)
in getBookSummaries), which can suppress writes; remove readOnly = true from the
`@Transactional` annotation on both methods (leave `@Transactional` active or
replace with `@Transactional` without readOnly) so the write operations
(booksRepository.save(...), enrichMetadataAsync triggers that expect
persistence) execute correctly and transaction semantics are preserved.
- Around line 91-95: booksRepository.findAllByTitleIn can return multiple Books
with the same title and Collectors.toMap(Books::getTitle, b -> b) will throw
IllegalStateException on duplicate keys; modify the mapping that builds bookMap
(the stream collecting into a Map) to handle duplicate titles by providing a
merge function (or by grouping and selecting one record) so duplicates are
resolved deterministically (e.g., keep first/last or choose by id/date) instead
of letting toMap fail; update the code around Books,
booksRepository.findAllByTitleIn and bookMap to use Collectors.toMap with a
(existing, incoming) merge rule or use Collectors.groupingBy followed by
pick-one to ensure no exception is thrown.
- Around line 43-58: The current getBookSummary caches incomplete BookSummary
results (null author/publisher/thumbnail) so enrichMetadataAsync updates DB but
the cache remains stale; change approach by either preventing caching of
incomplete metadata in getBookSummary (e.g., extend the `@Cacheable` condition to
skip when core fields are missing) or—preferred—ensure enrichMetadataAsync
evicts/refreshes the cache after a successful DB update by adding a cache
eviction (e.g., `@CacheEvict`(value="bookMetadata", key="'book:' + `#bookId`") or
programmatic eviction) in the enrichMetadataAsync method immediately after
updating the Books entity so subsequent calls to getBookSummary use fresh data
from createBookSummary.
- Around line 145-147: The Kakao API call in BookMetadataService currently uses
kakaoBookClient.search(title, 1, 1).block() without a timeout which can hang
async threads; change this to use a bounded wait (e.g., call
block(Duration.ofSeconds(...)) or configure a request timeout on the WebClient
in KakaoBookClient) and add handling for timeout-related exceptions (e.g., catch
TimeoutException/RuntimeException and log/translate to your domain exception).
Locate the call site in BookMetadataService (the kakaoBookClient.search(...)
invocation) and either add a Duration parameter to block(...) or adjust the
KakaoBookClient WebClient timeout settings, and ensure downstream code handles
the timeout case gracefully.

In
`@booklog/src/main/java/com/example/booklog/domain/library/books/repository/BooksRepository.java`:
- Around line 19-30: The repository exposes findByTitle(String) but Books.title
has no uniqueness constraint and findAllByTitleIn(List<String>) lacks an
empty-list guard; either enforce title uniqueness in the Books entity and DB
migration (add `@Column`(unique=true) or equivalent) if title must be unique, or
change the repository API to reflect possible multiples by replacing
Optional<Books> findByTitle(String) with List<Books> findByTitle(String) (and
update callers). Also add an empty-list guard for the bulk lookup by adding a
safe wrapper on the repository (e.g., a default method in BooksRepository like
findAllByTitleInSafe(List<String> titles) that returns an empty list if titles
is null/empty and delegates to findAllByTitleIn otherwise) or validate and
short-circuit in callers such as HomeServiceImpl.
🧹 Nitpick comments (4)
booklog/src/main/java/com/example/booklog/global/config/AsyncConfig.java (1)

20-27: Consider graceful shutdown + explicit rejection policy for the async pool.

Without shutdown settings, queued enrichment tasks can be dropped on shutdown, and the default rejection policy can throw when the queue is full. A small config tweak improves resiliency.

♻️ Suggested config
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

 import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;

@@
     public Executor getAsyncExecutor() {
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
         executor.setCorePoolSize(5);
         executor.setMaxPoolSize(10);
         executor.setQueueCapacity(100);
         executor.setThreadNamePrefix("BookMetadata-Async-");
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAwaitTerminationSeconds(30);
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
         executor.initialize();
         return executor;
     }
booklog/src/main/java/com/example/booklog/global/config/RedisCacheConfig.java (1)

75-79: In-memory fallback doesn’t expire entries; consider a TTL-capable cache.

ConcurrentMapCacheManager doesn’t enforce TTLs, so if Redis is unavailable at startup, cached data can persist indefinitely. If freshness matters, consider a TTL-capable in-memory cache (e.g., Caffeine) to preserve the intended expiration behavior.

♻️ Example (requires Caffeine dependency)
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.springframework.cache.caffeine.CaffeineCache;
+import org.springframework.cache.support.SimpleCacheManager;
+import java.util.List;

@@
     `@Bean`
     public CacheManager inMemoryCacheManager() {
         log.info("📦 인메모리 캐시 활성화 (ConcurrentMap) - Redis 미사용");
-        return new ConcurrentMapCacheManager("homeBooks", "bookMetadata");
+        CaffeineCache homeBooks = new CaffeineCache(
+                "homeBooks",
+                Caffeine.newBuilder().expireAfterWrite(Duration.ofHours(6)).build()
+        );
+        CaffeineCache bookMetadata = new CaffeineCache(
+                "bookMetadata",
+                Caffeine.newBuilder().expireAfterWrite(Duration.ofDays(7)).build()
+        );
+        SimpleCacheManager manager = new SimpleCacheManager();
+        manager.setCaches(List.of(homeBooks, bookMetadata));
+        return manager;
     }
booklog/src/main/resources/application.yaml (1)

50-55: Align cache TTL configuration between YAML and RedisCacheConfig.

spring.cache.redis.time-to-live is 6 hours here, but RedisCacheConfig sets a 24-hour default and per-cache TTLs. If the custom CacheManager is used, this YAML value is likely ignored—consider removing it or aligning values to avoid confusion.

✅ Simplify to avoid conflicting defaults
   cache:
     type: redis
     redis:
-      time-to-live: 21600000 # 기본 TTL 6시간 (ms)
       # Redis 연결 실패 시 캐시 비활성화 (애플리케이션은 정상 작동)
       enable-statistics: true
booklog/src/main/java/com/example/booklog/domain/home/service/HomeServiceImpl.java (1)

27-72: Avoid hard-coded fallback IDs; derive from BOOK_ID_MAPPING.
BOOK_ID_MAPPING already defines the source of truth, but buildRealTimeRanking re-hardcodes IDs, which can drift. Consider a helper that derives IDs from the mapping.

♻️ Possible refactor
+    private BookSummary getOrFallback(Map<String, BookSummary> bookMap, String title, int ranking) {
+        Long bookId = BOOK_ID_MAPPING.getOrDefault(title, 0L);
+        return bookMap.getOrDefault(title, createFallback(bookId, title, ranking));
+    }
...
-            bookMap.getOrDefault("트렌드 코리아 2026", createFallback(1L, "트렌드 코리아 2026", 1)),
+            getOrFallback(bookMap, "트렌드 코리아 2026", 1),

Also applies to: 139-161

Comment thread booklog/build.gradle
Comment on lines 74 to 77
tasks.named('test') {
useJUnitPlatform()
enabled = false // 테스트 비활성화 -> 에러가 너무 많아서 잠시 비활성화
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "build.gradle" -type f | head -20

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 97


🏁 Script executed:

wc -l ./booklog/build.gradle

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 100


🏁 Script executed:

cat -n ./booklog/build.gradle

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 3554


🏁 Script executed:

# Verify if the suggested conditional approach is valid Gradle syntax
# by checking Gradle documentation patterns and examples
web_search "Gradle conditional disable test task project property -P"

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 125


🌐 Web query:

Gradle how to conditionally disable test task using project property skipTests

💡 Result:

Use a project property and make the Test tasks conditional.

Groovy DSL (build.gradle)

tasks.withType(Test).configureEach {
    onlyIf { !project.hasProperty("skipTests") }
}

Run with:

./gradlew build -PskipTests

This skips tests when skipTests is present. [1]

If you want to disable the task (instead of conditional execution):

tasks.withType(Test).configureEach {
    enabled = !project.hasProperty("skipTests")
}

(Every task has an enabled flag.) [2]

Kotlin DSL (build.gradle.kts)

tasks.withType<Test>().configureEach {
    onlyIf { !project.hasProperty("skipTests") }
}

or:

tasks.withType<Test>().configureEach {
    enabled = !project.hasProperty("skipTests")
}

Alternative (one-off from CLI)

./gradlew build -x test

This excludes the test task for that invocation. [1]

Sources: Gradle docs on skipping tests via onlyIf / -x [1] and task enabled flag [2].

[1] https://docs.gradle.org/current/userguide/java_testing.html
[2] https://docs.gradle.org/current/userguide/controlling_task_execution.html


🏁 Script executed:

find . -name "*.yml" -o -name "*.yaml" | grep -E "(github|gitlab|ci|workflow)" | head -10

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 109


🏁 Script executed:

cat -n ./.github/workflows/dev_deploy.yml

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 2503


Don't disable tests globally in the build script.

Line 76 unconditionally disables tests, which prevents developers from running tests locally by default and can let regressions ship. While your CI uses -x test to skip tests during deployment, the root issue is the blanket disable in the build.gradle configuration. Prefer a conditional skip flag (e.g., -PskipTests) or remove the disable entirely and use CI-only flags.

Suggested fix (conditional skip)
 tasks.named('test') {
     useJUnitPlatform()
-    enabled = false // 테스트 비활성화 -> 에러가 너무 많아서 잠시 비활성화
+    enabled = !project.hasProperty("skipTests")
 }

This allows ./gradlew build to run tests by default, and ./gradlew build -PskipTests to skip them when needed.

🤖 Prompt for AI Agents
In `@booklog/build.gradle` around lines 74 - 77, The build.gradle currently
disables the test task unconditionally via tasks.named('test') { enabled = false
}, which prevents local test runs; change this to respect a skip flag instead:
remove the unconditional enabled = false and use a conditional check on a Gradle
project property (e.g., project.hasProperty('skipTests') or
!project.hasProperty('runTests')) inside the tasks.named('test') block so tests
run by default and only skip when the -PskipTests flag is passed; ensure
useJUnitPlatform() remains configured in the same tasks.named('test') block.

Comment on lines +11 to +29
```
[사용자 요청]
[HomeController] → GET /api/v1/home
[HomeServiceImpl] → @Cacheable("homeBooks")
[BookMetadataService] → DB 우선 조회 + @Cacheable("bookMetadata")
┌────────────────┐
│ 1. DB 조회 │ → Books 엔티티 (author/publisher/thumbnail 포함)
└────────────────┘
├─ 있음 → 즉시 응답 (지연 없음)
│ └─ 메타데이터 미보강? → 비동기로 카카오 API 호출 (@Async)
└─ 없음 → 빈 Books 생성 후 즉시 응답
└─ 비동기로 카카오 API 호출 + DB 업데이트
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks (MD040).

markdownlint reports missing languages on multiple fences; please label them (e.g., text, java, bash) for lint compliance and readability.

✅ Example fix (apply similarly to other fenced blocks)
-```
+```text
 [사용자 요청]
     ↓
 [HomeController] → GET /api/v1/home
@@
-```
+```
📝 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
```
[사용자 요청]
[HomeController] → GET /api/v1/home
[HomeServiceImpl]@Cacheable("homeBooks")
[BookMetadataService] → DB 우선 조회 + @Cacheable("bookMetadata")
┌────────────────┐
│ 1. DB 조회 │ → Books 엔티티 (author/publisher/thumbnail 포함)
└────────────────┘
├─ 있음 → 즉시 응답 (지연 없음)
│ └─ 메타데이터 미보강? → 비동기로 카카오 API 호출 (@Async)
└─ 없음 → 빈 Books 생성 후 즉시 응답
└─ 비동기로 카카오 API 호출 + DB 업데이트
```
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 11-11: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@booklog/docs/BOOK_METADATA_CACHING_STRATEGY.md` around lines 11 - 29, This
MD040 lint error is from unlabeled fenced code blocks in
BOOK_METADATA_CACHING_STRATEGY.md; update each triple-backtick fence (e.g., the
block that starts with ``` before the flow diagram) to include an explicit
language identifier like text (```text) or java/bash where appropriate so
markdownlint passes and readability improves.

Comment on lines +102 to +106
| 캐시 이름 | Key | TTL | 용도 |
|----------|-----|-----|------|
| `homeBooks` | `home:all` | 6시간 | 홈 화면 전체 응답 |
| `bookMetadata` | `book:{bookId}` | 7일 | 개별 도서 메타데이터 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix table spacing style to satisfy MD060.

The table pipes lack surrounding spaces, which triggers MD060. Apply the same spacing fix to other tables in the doc.

✅ Example fix
-| 캐시 이름 | Key | TTL | 용도 |
-|----------|-----|-----|------|
-| `homeBooks` | `home:all` | 6시간 | 홈 화면 전체 응답 |
-| `bookMetadata` | `book:{bookId}` | 7일 | 개별 도서 메타데이터 |
+| 캐시 이름 | Key | TTL | 용도 |
+| ---------- | ----- | ----- | ------ |
+| `homeBooks` | `home:all` | 6시간 | 홈 화면 전체 응답 |
+| `bookMetadata` | `book:{bookId}` | 7일 | 개별 도서 메타데이터 |
📝 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
| 캐시 이름 | Key | TTL | 용도 |
|----------|-----|-----|------|
| `homeBooks` | `home:all` | 6시간 | 홈 화면 전체 응답 |
| `bookMetadata` | `book:{bookId}` | 7일 | 개별 도서 메타데이터 |
| 캐시 이름 | Key | TTL | 용도 |
| ---------- | ----- | ----- | ------ |
| `homeBooks` | `home:all` | 6시간 | 홈 화면 전체 응답 |
| `bookMetadata` | `book:{bookId}` | 7일 | 개별 도서 메타데이터 |
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 103-103: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 103-103: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 103-103: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 103-103: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 103-103: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 103-103: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 103-103: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 103-103: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)

🤖 Prompt for AI Agents
In `@booklog/docs/BOOK_METADATA_CACHING_STRATEGY.md` around lines 102 - 106, The
table rows (e.g., entries for `homeBooks` / `home:all` and `bookMetadata` /
`book:{bookId}`) use pipe characters without surrounding spaces which triggers
MD060; update every markdown table in the document so each column separator has
a single space on both sides of the pipe (e.g., " | " between columns) and
ensure header separator rows match that spacing style; apply the same fix
consistently to all other tables in BOOK_METADATA_CACHING_STRATEGY.md.

Comment on lines +52 to +115
### 시나리오 1: 최초 진입 (DB 없음)
```
사용자 요청 (t=0)
DB 조회 → 없음 (50ms)
빈 Books 생성 + 응답 (200ms) ✅ 사용자에게 즉시 반환
[별도 스레드] 카카오 API 호출 (500ms)
DB 업데이트 완료 (t=700ms)
```
**사용자 체감**: 200ms (빠름)

### 시나리오 2: 2번째 진입 (Redis 캐시)
```
사용자 요청 (t=0)
Redis 조회 → 히트! (10ms) ✅
```
**사용자 체감**: 10ms (매우 빠름)

### 시나리오 3: 캐시 만료 (DB 조회)
```
사용자 요청 (t=0)
Redis 조회 → 미스 (5ms)
DB 조회 → 히트 (100ms) ✅
Redis 캐시 갱신
```
**사용자 체감**: 100ms (빠름)

---

## 📊 성능 개선 효과

| 항목 | Before (동기) | After (캐싱+비동기) | 개선율 |
|-----|-------------|------------------|-------|
| 최초 응답 시간 | 10초 | 200ms | **50배 개선** |
| 2번째 응답 시간 | 10초 | 10ms | **1000배 개선** |
| 외부 API 호출 | 매번 | 최초 1회만 | **99% 감소** |
| 캐시 히트율 | 0% | 95%+ | - |

---

## 🗂️ 파일 목록

### 신규 생성
```
global/config/
├─ RedisCacheConfig.java # Redis 캐시 설정
└─ AsyncConfig.java # 비동기 처리 설정

domain/home/service/
└─ BookMetadataService.java # 메타데이터 관리 서비스

docs/
└─ BOOK_METADATA_CACHING_STRATEGY.md # 전략 문서
```

### 수정
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced blocks and fix table pipe spacing.

markdownlint flags several fenced blocks without a language and the performance table’s pipe spacing. Please add language tags (e.g., text, bash, java) and normalize the table formatting.

📝 Example fixes (apply similarly to all flagged blocks)
-```
+```text
-|항목|Before (동기)|After (캐싱+비동기)|개선율|
+| 항목 | Before (동기) | After (캐싱+비동기) | 개선율 |

Also applies to: 244-246

🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 67-67: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 75-75: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 91-91: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 91-91: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 91-91: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 91-91: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 91-91: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 91-91: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 91-91: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 91-91: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 102-102: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@booklog/docs/CACHING_IMPLEMENTATION_SUMMARY.md` around lines 52 - 115, Update
the Markdown by adding language identifiers to all fenced code blocks (e.g.,
```text for plain text blocks, ```bash or ```java where snippets show commands
or code) so markdownlint stops flagging them, and normalize the performance
table’s pipe spacing so each column separator has a single space on both sides
(fix the header and all rows), including the code blocks under the "시나리오"
sections and the performance table titled "📊 성능 개선 효과".

Comment on lines +5 to +7
```
BUILD SUCCESSFUL in 4s
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Address markdownlint warnings (code fence language, bare URL, table separator spacing).
These items are flagged by markdownlint (MD040/MD034/MD060). Consider applying the same fixes to all unlabeled fences and separator rows in this file.

📝 Suggested fixes
-```
+```text
 BUILD SUCCESSFUL in 4s

...
-- https://www.memurai.com/get-memurai
+- https://www.memurai.com/get-memurai
...
-|---------|----------|-------------|
+| -------- | --------- | ------------- |

</details>



Also applies to: 44-45, 126-130, 136-138, 207-209, 217-219

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.20.0)</summary>

[warning] 5-5: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @booklog/docs/FINAL_DEPLOYMENT_GUIDE.md around lines 5 - 7, Fix the
markdownlint warnings by labeling unlabeled code fences (e.g., change the fence
containing "BUILD SUCCESSFUL in 4s" to text ... ), convert bare URLs like
"https://www.memurai.com/get-memurai" to angle-bracket form
"https://www.memurai.com/get-memurai", and normalize table separator rows to
include spaces around cells (e.g., change "|---------|----------|-------------|"
to "| -------- | --------- | ------------- |"); apply the same fixes to the
other occurrences flagged (lines referenced in the review).


</details>

<!-- fingerprinting:phantom:medusa:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +43 to +64
@Cacheable(value = "bookMetadata", key = "'book:' + #bookId", unless = "#result == null")
@Transactional(readOnly = true)
public BookSummary getBookSummary(Long bookId, String title, Integer ranking) {
// 1. DB에서 조회
Optional<Books> bookOpt = booksRepository.findByTitle(title);

if (bookOpt.isPresent()) {
Books book = bookOpt.get();

// 2. 메타데이터 보강 필요 시 비동기로 처리
if (needsMetadataEnrichment(book)) {
enrichMetadataAsync(book.getId(), title);
}

// 3. 현재 DB 데이터로 응답 (즉시 반환)
return createBookSummary(bookId, book, ranking);
}

// 4. DB에 없으면 빈 Books 생성 후 비동기 보강
Books newBook = createEmptyBook(title);
Books savedBook = booksRepository.save(newBook);
enrichMetadataAsync(savedBook.getId(), title);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "BookMetadataService.java" -type f

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 163


🏁 Script executed:

cat -n ./booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 9976


Change to read‑write transactions where writes occur.

Both getBookSummary (line 44) and getBookSummaries (line 83) are marked @Transactional(readOnly = true) but call booksRepository.save(...) at lines 63 and 110 respectively. In read-only mode, writes may be suppressed or delayed depending on JPA configuration, causing data loss or inconsistent state.

Remove readOnly = true from both methods since they perform write operations:

Suggested fix
-    `@Transactional`(readOnly = true)
+    `@Transactional`
     public BookSummary getBookSummary(Long bookId, String title, Integer ranking) {
-    `@Transactional`(readOnly = true)
+    `@Transactional`
     public List<BookSummary> getBookSummaries(List<BookInfo> bookInfoList) {
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java`
around lines 43 - 64, The methods getBookSummary(...) and getBookSummaries(...)
are annotated with `@Transactional`(readOnly = true) but call
booksRepository.save(...) (e.g., save(newBook) in getBookSummary and save(...)
in getBookSummaries), which can suppress writes; remove readOnly = true from the
`@Transactional` annotation on both methods (leave `@Transactional` active or
replace with `@Transactional` without readOnly) so the write operations
(booksRepository.save(...), enrichMetadataAsync triggers that expect
persistence) execute correctly and transaction semantics are preserved.

Comment on lines +43 to +58
@Cacheable(value = "bookMetadata", key = "'book:' + #bookId", unless = "#result == null")
@Transactional(readOnly = true)
public BookSummary getBookSummary(Long bookId, String title, Integer ranking) {
// 1. DB에서 조회
Optional<Books> bookOpt = booksRepository.findByTitle(title);

if (bookOpt.isPresent()) {
Books book = bookOpt.get();

// 2. 메타데이터 보강 필요 시 비동기로 처리
if (needsMetadataEnrichment(book)) {
enrichMetadataAsync(book.getId(), title);
}

// 3. 현재 DB 데이터로 응답 (즉시 반환)
return createBookSummary(bookId, book, ranking);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "BookMetadataService.java"

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 163


🏁 Script executed:

cat -n ./booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 9976


The cache will serve stale null metadata until TTL expires; enrichMetadataAsync doesn't evict or refresh the cached entry.

When a BookSummary with null author/publisher/thumbnail is cached, subsequent calls within 6 hours will return that cached stale state even after enrichment completes and the database is updated. The enrichMetadataAsync method updates the Books entity but has no cache invalidation (@CacheEvict or @CachePut).

To fix this, either skip caching incomplete metadata or evict the cache after enrichment. The conditional unless approach is a partial mitigation:

Option 1: Skip caching when core fields are missing
-    `@Cacheable`(value = "bookMetadata", key = "'book:' + `#bookId`", unless = "#result == null")
+    `@Cacheable`(
+            value = "bookMetadata",
+            key = "'book:' + `#bookId`",
+            unless = "#result == null || `#result.author` == null || `#result.publisher` == null || `#result.coverImageUrl` == null"
+    )

Better: Add @CacheEvict(value = "bookMetadata", key = "'book:' + #bookId") in enrichMetadataAsync after successfully updating the database (after line 178) to immediately refresh the cache.

🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java`
around lines 43 - 58, The current getBookSummary caches incomplete BookSummary
results (null author/publisher/thumbnail) so enrichMetadataAsync updates DB but
the cache remains stale; change approach by either preventing caching of
incomplete metadata in getBookSummary (e.g., extend the `@Cacheable` condition to
skip when core fields are missing) or—preferred—ensure enrichMetadataAsync
evicts/refreshes the cache after a successful DB update by adding a cache
eviction (e.g., `@CacheEvict`(value="bookMetadata", key="'book:' + `#bookId`") or
programmatic eviction) in the enrichMetadataAsync method immediately after
updating the Books entity so subsequent calls to getBookSummary use fresh data
from createBookSummary.

Comment on lines +91 to +95
// 2. 일괄 조회 (IN 쿼리 1번)
List<Books> books = booksRepository.findAllByTitleIn(titles);
Map<String, Books> bookMap = books.stream()
.collect(Collectors.toMap(Books::getTitle, b -> b));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "Books.java" -o -name "Book.java" | head -20

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 223


🏁 Script executed:

find . -type f -name "*Entity*.java" | grep -i book | head -20

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 245


🏁 Script executed:

fd -e java | grep -iE "(books|book)" | head -30

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 2726


🏁 Script executed:

cat -n ./booklog/src/main/java/com/example/booklog/domain/library/books/entity/Books.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 5718


🏁 Script executed:

cat -n ./booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java | head -120

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 4917


Guard against duplicate titles when building the lookup map.

Collectors.toMap throws IllegalStateException on duplicate keys. The Books.title column has no unique constraint, so multiple records with the same title can exist. When findAllByTitleIn(titles) returns duplicates, the toMap on line 93–94 will fail with a 500 error. Add a merge function or enforce uniqueness.

🛠️ Example safeguard
-        Map<String, Books> bookMap = books.stream()
-                .collect(Collectors.toMap(Books::getTitle, b -> b));
+        Map<String, Books> bookMap = books.stream()
+                .collect(Collectors.toMap(Books::getTitle, b -> b, (existing, duplicate) -> existing));
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java`
around lines 91 - 95, booksRepository.findAllByTitleIn can return multiple Books
with the same title and Collectors.toMap(Books::getTitle, b -> b) will throw
IllegalStateException on duplicate keys; modify the mapping that builds bookMap
(the stream collecting into a Map) to handle duplicate titles by providing a
merge function (or by grouping and selecting one record) so duplicates are
resolved deterministically (e.g., keep first/last or choose by id/date) instead
of letting toMap fail; update the code around Books,
booksRepository.findAllByTitleIn and bookMap to use Collectors.toMap with a
(existing, incoming) merge rule or use Collectors.groupingBy followed by
pick-one to ensure no exception is thrown.

Comment on lines +145 to +147
// 카카오 API 호출
KakaoBookSearchResponse response = kakaoBookClient.search(title, 1, 1).block();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "BookMetadataService.java" | head -5

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 163


🏁 Script executed:

find . -type f -name "KakaoBookClient.java" | head -5

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 175


🏁 Script executed:

find . -type f -name "*KakaoBookClient*" -o -name "*KakaoBook*" | grep -E "\.(java|kt)$" | head -10

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 273


🏁 Script executed:

cat -n ./booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java | sed -n '140,155p'

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 766


🏁 Script executed:

wc -l ./booklog/src/main/java/com/example/booklog/domain/library/books/service/client/KakaoBookClient.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 178


🏁 Script executed:

cat -n ./booklog/src/main/java/com/example/booklog/domain/library/books/service/client/KakaoBookClient.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 1201


🏁 Script executed:

find . -type f -name "*Config*.java" | xargs grep -l "kakaoWebClient\|WebClient" | head -10

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 153


🏁 Script executed:

rg "kakaoWebClient" -t java -B 3 -A 3

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 2280


🏁 Script executed:

rg "WebClientConfig\|@Bean.*WebClient\|@Bean.*kakao" -t java -B 2 -A 10

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 57


🏁 Script executed:

cat -n ./booklog/src/main/java/com/example/booklog/global/config/WebClientConfig.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 1196


Add a timeout to the Kakao API call to avoid hung async threads.

block() without a timeout can pin async threads during network stalls. The WebClient bean has no timeout configuration, and block() is called without a timeout parameter. Consider block(Duration) or configure WebClient timeouts in KakaoBookClient, and handle timeout exceptions.

⏱️ Example timeout
+import java.time.Duration;
@@
-            KakaoBookSearchResponse response = kakaoBookClient.search(title, 1, 1).block();
+            KakaoBookSearchResponse response =
+                    kakaoBookClient.search(title, 1, 1).block(Duration.ofSeconds(3));
📝 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
// 카카오 API 호출
KakaoBookSearchResponse response = kakaoBookClient.search(title, 1, 1).block();
// 카카오 API 호출
KakaoBookSearchResponse response =
kakaoBookClient.search(title, 1, 1).block(Duration.ofSeconds(3));
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java`
around lines 145 - 147, The Kakao API call in BookMetadataService currently uses
kakaoBookClient.search(title, 1, 1).block() without a timeout which can hang
async threads; change this to use a bounded wait (e.g., call
block(Duration.ofSeconds(...)) or configure a request timeout on the WebClient
in KakaoBookClient) and add handling for timeout-related exceptions (e.g., catch
TimeoutException/RuntimeException and log/translate to your domain exception).
Locate the call site in BookMetadataService (the kakaoBookClient.search(...)
invocation) and either add a Duration parameter to block(...) or adjust the
KakaoBookClient WebClient timeout settings, and ensure downstream code handles
the timeout case gracefully.

Comment on lines +19 to +30
Optional<Books> findByTitle(String title);

/**
* 홈 화면용 title 일괄 조회
* Fetch Join으로 BookAuthors와 Authors를 함께 조회
*/
@Query("SELECT DISTINCT b FROM Books b " +
"LEFT JOIN FETCH b.bookAuthors ba " +
"LEFT JOIN FETCH ba.author a " +
"WHERE b.title IN :titles")
@QueryHints(@QueryHint(name = "hibernate.query.passDistinctThrough", value = "false"))
List<Books> findAllByTitleIn(@Param("titles") List<String> titles);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and read the Books entity to check for uniqueness constraints on title
find . -name "Books.java" -type f | head -5

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 157


🏁 Script executed:

# Also find the full BooksRepository interface
find . -name "BooksRepository.java" -type f | head -5

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 171


🏁 Script executed:

# Search for usage of findAllByTitleIn and findByTitle methods
rg "findAllByTitleIn|findByTitle" --type java -B 2 -A 2

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 2709


🏁 Script executed:

# Read the Books entity to check for title uniqueness constraints
cat -n ./booklog/src/main/java/com/example/booklog/domain/library/books/entity/Books.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 5718


🏁 Script executed:

# Read the relevant section of BookMetadataService to see context of findAllByTitleIn usage
cat -n ./booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java | head -80

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 3081


🏁 Script executed:

# Continue reading BookMetadataService to see the full context of findAllByTitleIn usage
cat -n ./booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java | sed -n '75,150p'

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 3101


🏁 Script executed:

# Search for any guards or validation around empty titles list in BookMetadataService
rg "isEmpty|isNull|isEmpty|titles" ./booklog/src/main/java/com/example/booklog/domain/home/service/BookMetadataService.java -B 2 -A 2

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 1139


🏁 Script executed:

# Search for callers of getBookSummaries to see if they validate input lists
rg "getBookSummaries" --type java -B 3 -A 3

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 1836


🏁 Script executed:

# Check if there are any test cases that might reveal empty list behavior
rg "findAllByTitleIn|getBookSummaries" --type java ./booklog/src/test -B 2 -A 2 2>/dev/null || echo "No test files found with these patterns"

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 114


🏁 Script executed:

# Search for any existing validation in the controller/service layer that calls getBookSummaries
rg "bookInfoList|BookInfo" --type java -B 2 -A 2 | head -40

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 4849


🏁 Script executed:

# Check if title is used as a unique identifier elsewhere in the codebase
rg "findByTitle|getTitle" --type java | grep -i "unique\|constraint\|key" | head -20

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 57


🏁 Script executed:

# Look at the full BooksRepository interface to see all methods and their assumptions
cat -n ./booklog/src/main/java/com/example/booklog/domain/library/books/repository/BooksRepository.java

Repository: Project-BookLog/BookLog-BackEnd

Length of output: 4936


Add empty-list guard for findAllByTitleIn() and reconsider title-uniqueness assumptions.

The Books entity does not enforce a unique constraint on the title column (only a non-unique index for query performance). This means findByTitle(String) can return arbitrary results if multiple books share the same title—the method signature Optional<Books> is misleading. Additionally, findAllByTitleIn() receives no validation for empty lists; while the current caller (HomeServiceImpl) always provides titles, a defensive check is recommended to prevent silent failures if the method is called with an empty list elsewhere.

Consider:

  • Adding a uniqueness constraint on the title column if title should be unique by design, or documenting that this method returns the first matching book.
  • Adding a guard method in BooksRepository (or validating in callers) to safely handle empty lists.
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/library/books/repository/BooksRepository.java`
around lines 19 - 30, The repository exposes findByTitle(String) but Books.title
has no uniqueness constraint and findAllByTitleIn(List<String>) lacks an
empty-list guard; either enforce title uniqueness in the Books entity and DB
migration (add `@Column`(unique=true) or equivalent) if title must be unique, or
change the repository API to reflect possible multiples by replacing
Optional<Books> findByTitle(String) with List<Books> findByTitle(String) (and
update callers). Also add an empty-list guard for the bulk lookup by adding a
safe wrapper on the repository (e.g., a default method in BooksRepository like
findAllByTitleInSafe(List<String> titles) that returns an empty list if titles
is null/empty and delegates to findAllByTitleIn otherwise) or validate and
short-circuit in callers such as HomeServiceImpl.

@icarus0616 icarus0616 self-assigned this Jan 30, 2026
@icarus0616 icarus0616 merged commit 9d67901 into dev Jan 30, 2026
1 check passed
This was referenced Jan 30, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Feb 10, 2026
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.

[FIX] 검색 API 인증 방식을 JWT 방식으로 통일.

2 participants