Skip to content

SCRUM 236 : 최근 검색어 기능 리팩토링#100

Open
Seungkiii wants to merge 2 commits into
developfrom
refactor/SCRUM-236
Open

SCRUM 236 : 최근 검색어 기능 리팩토링#100
Seungkiii wants to merge 2 commits into
developfrom
refactor/SCRUM-236

Conversation

@Seungkiii

@Seungkiii Seungkiii commented Aug 10, 2025

Copy link
Copy Markdown
Member

💡 개요

최근 검색어 조회 및 검색 시 검색어 캐시 저장 기능 구현

🔨작업 사항

  • 사용자가 검색 결과를 최대 10개 까지 캐시에 저장함
  • 구현된 기능을 통해 사용자가 검색창을 누르면 최근 검색어 10개를 조회할 수 있음

Summary by CodeRabbit

  • New Features

    • Recent search queries are now recorded when you perform a search.
    • Added an endpoint to view your last 10 recent search queries.
    • Users can remove individual recent search keywords from their history.
  • Bug Fixes

    • Improved reliability and error handling for recent search history features.

@Seungkiii Seungkiii requested a review from m3k0813 August 10, 2025 04:20
@Seungkiii Seungkiii self-assigned this Aug 10, 2025
@Seungkiii Seungkiii added Feature 새로운 기능 추가 Refactor 코드 리팩토링 labels Aug 10, 2025
@coderabbitai

coderabbitai Bot commented Aug 10, 2025

Copy link
Copy Markdown

Walkthrough

This update introduces a recent search history feature for users in the star domain. It adds endpoints and service methods to save, retrieve, and delete recent search keywords per user using Redis, with a maximum of 10 entries and a 30-day retention policy. Minor documentation and comment updates are also included.

Changes

Cohort / File(s) Change Summary
Controller & API
src/main/java/com_team_nebula/nebula/domain/star/api/StarV2Controller.java
Added a new endpoint to retrieve recent searches and updated the search method to save recent queries per user.
Command Service Interface & Implementation
src/main/java/com_team_nebula/nebula/domain/star/service/StarCommandService.java,
src/main/java/com_team_nebula/nebula/domain/star/service/StarCommandServiceImpl.java
Introduced methods to save and delete recent searches, with Redis-backed implementation handling deduplication, list trimming, and expiration.
Query Service Interface & Implementation
src/main/java/com_team_nebula/nebula/domain/star/service/StarQueryService.java,
src/main/java/com_team_nebula/nebula/domain/star/service/StarQueryServiceImpl.java
Added methods to retrieve recent searches from Redis, with caching and error handling.
Elasticsearch Service
src/main/java/com_team_nebula/nebula/domain/star/search/service/ElasticsearchService.java
Updated a method comment for clarity; no code or logic changes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant StarV2Controller
    participant StarCommandService
    participant StarQueryService
    participant Redis

    User->>StarV2Controller: GET /recent-searches (with userId)
    StarV2Controller->>StarQueryService: getRecentSearches(userId)
    StarQueryService->>Redis: LRANGE recent_searches:{userId} 0 9
    Redis-->>StarQueryService: List of recent search keywords
    StarQueryService-->>StarV2Controller: List<String>
    StarV2Controller-->>User: ApiResponse<List<String>>

    User->>StarV2Controller: GET /searchStars (with userId, query)
    StarV2Controller->>StarCommandService: saveRecentSearches(userId, query)
    StarCommandService->>Redis: LREM, LPUSH, LTRIM, EXPIRE recent_searches:{userId}
    StarV2Controller->>...: (proceeds with search logic)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15–20 minutes

Suggested reviewers

  • m3k0813

Poem

In the nebula's digital sky,
Search histories now flutter by—
Ten stars remembered, never more,
Redis holds each query's lore.
A rabbit logs each hop and leap,
So your searches, safe, it keeps!
🌟🐇

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/SCRUM-236

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.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @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 comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (6)
src/main/java/com/team_nebula/nebula/domain/star/search/service/ElasticsearchService.java (1)

180-180: Prefer JavaDoc over inline comment; clarify scope criteria.

Replace the inline comment with JavaDoc describing how this search differs from searchStars (e.g., fields searched, fuzziness, sort), so it stays accurate over time.

src/main/java/com/team_nebula/nebula/domain/star/service/StarQueryService.java (1)

41-41: Clarify API contract for recent searches.

  • Consider renaming to getRecentSearchKeywords for specificity.
  • Add JavaDoc to document ordering (most-recent-first), max count (10), normalization (e.g., lowercased?), and empty behavior.
src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandService.java (1)

28-31: Method naming and contract clarity.

  • saveRecentSearches handles a single keyword; consider renaming to saveRecentSearch for accuracy.
  • Document that keyword must be non-blank and whether normalization (trim/lowercase) will occur.
  • Consider returning boolean or long (count removed) from deleteRecentSearch to indicate outcome, if useful to callers.

Example interface adjustment:

-    public void saveRecentSearches(Long userId, String keyword);
+    public void saveRecentSearch(Long userId, String keyword);

-    public void deleteRecentSearch(Long userId, String keyword);
+    public boolean deleteRecentSearch(Long userId, String keyword);
src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandServiceImpl.java (1)

68-70: Use StringRedisTemplate (or RedisTemplate<String, String>) and extract key prefix.

Currently RedisTemplate<String, Object> + default serializers may store JDK-serialized strings unintentionally. Prefer a String-based template and a constant key prefix.

  • Inject StringRedisTemplate (or RedisTemplate<String, String>).
  • Add private static final String RECENT_SEARCH_KEY_PREFIX = "recent_search:";
  • Build keys via RECENT_SEARCH_KEY_PREFIX + userId.

This improves serialization correctness and maintainability.

src/main/java/com/team_nebula/nebula/domain/star/service/StarQueryServiceImpl.java (2)

12-12: Prefer StringRedisTemplate and return typed values directly

The list stores strings; using RedisTemplate<String, Object> forces JDK serialization and toString() mapping. StringRedisTemplate simplifies serialization, reduces risk of incompatible serializers, and lets you return List<String> directly.

- import org.springframework.data.redis.core.RedisTemplate;
+ import org.springframework.data.redis.core.StringRedisTemplate;
- private final RedisTemplate<String, Object> redisTemplate;
+ private final StringRedisTemplate redisTemplate;
  private static final int MAX_RECENT_SEARCHES = 10;
- List<Object> searches = redisTemplate.opsForList().range(key, 0, MAX_RECENT_SEARCHES - 1);
- return searches.stream()
-         .map(Object::toString)
-         .collect(Collectors.toList());
+ List<String> searches = redisTemplate.opsForList().range(key, 0, MAX_RECENT_SEARCHES - 1);
+ return searches == null ? Collections.emptyList() : searches;

Also applies to: 36-37, 193-197


193-197: Minor: be defensive and simplify collection conversion

If you keep RedisTemplate<String, Object>, guard against null from range, and use toList() for consistency with the rest of this class.

- List<Object> searches = redisTemplate.opsForList().range(key, 0, MAX_RECENT_SEARCHES - 1);
- return searches.stream()
-         .map(Object::toString)
-         .collect(Collectors.toList());
+ List<Object> searches = redisTemplate.opsForList().range(key, 0, MAX_RECENT_SEARCHES - 1);
+ if (searches == null || searches.isEmpty()) {
+     return Collections.emptyList();
+ }
+ return searches.stream()
+         .map(Object::toString)
+         .toList();
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8896357 and 8e3eef6.

📒 Files selected for processing (6)
  • src/main/java/com/team_nebula/nebula/domain/star/api/StarV2Controller.java (2 hunks)
  • src/main/java/com/team_nebula/nebula/domain/star/search/service/ElasticsearchService.java (1 hunks)
  • src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandService.java (2 hunks)
  • src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandServiceImpl.java (3 hunks)
  • src/main/java/com/team_nebula/nebula/domain/star/service/StarQueryService.java (1 hunks)
  • src/main/java/com/team_nebula/nebula/domain/star/service/StarQueryServiceImpl.java (4 hunks)
🔇 Additional comments (1)
src/main/java/com/team_nebula/nebula/domain/star/api/StarV2Controller.java (1)

98-103: LGTM: recent searches endpoint

Endpoint is clear and aligns with service contract (top 10). Ensure API docs/state that results may be normalized (lowercase) if applicable.

Comment on lines +83 to 85
starCommandService.saveRecentSearches(userId, query);
SearchResultResponseDTO result = starQueryService.searchStarsV2(query, userId, page, size);
return ApiResponse.onSuccess(result);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid saving blank queries; reconsider side-effect in GET.

  • Guard against blank q to prevent storing empty recent searches.
  • Saving state in a GET handler is debatable REST-wise. If possible, consider emitting an async domain event instead of directly updating state here.

Suggested guard:

-        starCommandService.saveRecentSearches(userId, query);
+        if (query != null && !query.isBlank()) {
+            starCommandService.saveRecentSearches(userId, query);
+        }

Confirm there are no other code paths persisting blank queries:


🏁 Script executed:

#!/bin/bash
rg -n --pretty -e 'saveRecentSearch(es)?\s*\(' -e '@GetMapping\(\"/search\"' -A3

Length of output: 240


🏁 Script executed:

#!/bin/bash
set -e

# Find all calls to saveRecentSearches in the codebase
rg -n --pretty "saveRecentSearches"

echo "-----"

# Find all @GetMapping annotations that include "search" in the path, with context
rg -n --pretty '@GetMapping.*search' -A3

Length of output: 1778


Guard blank search queries & reconsider side-effect in GET

We searched the entire codebase and found only one direct call to saveRecentSearches in a GET handler (StarV2Controller). To prevent empty keywords from being stored—and to keep this endpoint free of side-effects—please apply the following patch:

• File: src/main/java/com/team_nebula/nebula/domain/star/api/StarV2Controller.java

--- a/src/main/java/com/team_nebula/nebula/domain/star/api/StarV2Controller.java
+++ b/src/main/java/com/team_nebula/nebula/domain/star/api/StarV2Controller.java
@@ public ApiResponse<SearchResultResponseDTO> searchStars(
-        starCommandService.saveRecentSearches(userId, query);
+        if (query != null && !query.isBlank()) {
+            starCommandService.saveRecentSearches(userId, query);
+        }

Also consider emitting an asynchronous domain event for “recent search” recording instead of performing this side-effect directly within a GET handler.

📝 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
starCommandService.saveRecentSearches(userId, query);
SearchResultResponseDTO result = starQueryService.searchStarsV2(query, userId, page, size);
return ApiResponse.onSuccess(result);
if (query != null && !query.isBlank()) {
starCommandService.saveRecentSearches(userId, query);
}
SearchResultResponseDTO result = starQueryService.searchStarsV2(query, userId, page, size);
return ApiResponse.onSuccess(result);
🤖 Prompt for AI Agents
In src/main/java/com/team_nebula/nebula/domain/star/api/StarV2Controller.java
around lines 83 to 85, the method saveRecentSearches is called directly in a GET
handler without checking if the search query is blank, causing empty keywords to
be stored and introducing side-effects in a GET request. Fix this by adding a
guard clause to check if the query string is not blank before calling
saveRecentSearches. Additionally, consider refactoring this side-effect to emit
an asynchronous domain event for recording recent searches instead of performing
it synchronously within the GET handler.

Comment on lines +48 to +49
import static org.springframework.data.neo4j.core.ReactiveNeo4jClient.log;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Incorrect logger import and usage; use a proper class logger.

import static ...ReactiveNeo4jClient.log; is not appropriate. Also, current log.error(e, "...") order won’t log stack traces.

  • Add Lombok @Slf4j (or define a Logger).
  • Remove the static import.
  • Use log.error("...", e).

Within this file’s changed lines, remove the static import:

-import static org.springframework.data.neo4j.core.ReactiveNeo4jClient.log;

And update error calls in the methods (see diffs on those ranges).

📝 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
import static org.springframework.data.neo4j.core.ReactiveNeo4jClient.log;
🤖 Prompt for AI Agents
In
src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandServiceImpl.java
around lines 48 to 49, remove the incorrect static import of
ReactiveNeo4jClient.log. Instead, add the Lombok @Slf4j annotation to the class
to define a proper logger. Then, update all log.error calls in the file to use
the correct parameter order: pass the log message first and the exception
second, like log.error("message", e), to ensure stack traces are logged
properly.

Comment on lines +326 to +342
@Override
@Async
public void saveRecentSearches(Long userId, String keyword){
try {
String key = "recent_search:" + userId;
String normalizedKeyword = keyword.trim().toLowerCase();

// 중복 제거
redisTemplate.opsForList().remove(key, 0, normalizedKeyword);
redisTemplate.opsForList().leftPush(key, normalizedKeyword);
redisTemplate.opsForList().trim(key, 0, MAX_RECENT_SEARCHES - 1);
redisTemplate.expire(key, Duration.ofDays(30));
}
catch (Exception e) {
log.error(e, "Failed to save recent search");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden save logic: null/blank guard, locale-safe lowercasing, correct logging, and avoid unnecessary DB transactions.

  • Guard against null/blank to avoid storing empty entries.
  • Use Locale.ROOT for consistent lowercasing.
  • Fix log.error argument order.
  • Since class-level @Transactional applies, consider overriding with @Transactional(propagation = Propagation.NOT_SUPPORTED) here to avoid opening a JPA transaction for Redis I/O.

Proposed changes:

 @Override
 @Async
 public void saveRecentSearches(Long userId, String keyword){
     try {
-        String key = "recent_search:" + userId;
-        String normalizedKeyword = keyword.trim().toLowerCase();
+        if (keyword == null) return;
+        String trimmed = keyword.trim();
+        if (trimmed.isEmpty()) return;
+        String key = "recent_search:" + userId;
+        String normalizedKeyword = trimmed.toLowerCase(java.util.Locale.ROOT);

         // 중복 제거
         redisTemplate.opsForList().remove(key, 0, normalizedKeyword);
         redisTemplate.opsForList().leftPush(key, normalizedKeyword);
         redisTemplate.opsForList().trim(key, 0, MAX_RECENT_SEARCHES - 1);
         redisTemplate.expire(key, java.time.Duration.ofDays(30));
     }
     catch (Exception e) {
-        log.error(e, "Failed to save recent search");
+        log.error("Failed to save recent search", e);
     }
 }
📝 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
@Async
public void saveRecentSearches(Long userId, String keyword){
try {
String key = "recent_search:" + userId;
String normalizedKeyword = keyword.trim().toLowerCase();
// 중복 제거
redisTemplate.opsForList().remove(key, 0, normalizedKeyword);
redisTemplate.opsForList().leftPush(key, normalizedKeyword);
redisTemplate.opsForList().trim(key, 0, MAX_RECENT_SEARCHES - 1);
redisTemplate.expire(key, Duration.ofDays(30));
}
catch (Exception e) {
log.error(e, "Failed to save recent search");
}
}
@Override
@Async
public void saveRecentSearches(Long userId, String keyword){
try {
if (keyword == null) return;
String trimmed = keyword.trim();
if (trimmed.isEmpty()) return;
String key = "recent_search:" + userId;
String normalizedKeyword = trimmed.toLowerCase(java.util.Locale.ROOT);
// 중복 제거
redisTemplate.opsForList().remove(key, 0, normalizedKeyword);
redisTemplate.opsForList().leftPush(key, normalizedKeyword);
redisTemplate.opsForList().trim(key, 0, MAX_RECENT_SEARCHES - 1);
redisTemplate.expire(key, java.time.Duration.ofDays(30));
}
catch (Exception e) {
log.error("Failed to save recent search", e);
}
}
🤖 Prompt for AI Agents
In
src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandServiceImpl.java
around lines 326 to 342, enhance saveRecentSearches by adding a null or blank
check for keyword to prevent storing empty entries, use
keyword.toLowerCase(Locale.ROOT) for locale-safe lowercasing, correct the
log.error call to have the message first and exception second, and add
@Transactional(propagation = Propagation.NOT_SUPPORTED) to avoid opening
unnecessary JPA transactions during Redis operations.

Comment on lines +326 to +352
@Override
@Async
public void saveRecentSearches(Long userId, String keyword){
try {
String key = "recent_search:" + userId;
String normalizedKeyword = keyword.trim().toLowerCase();

// 중복 제거
redisTemplate.opsForList().remove(key, 0, normalizedKeyword);
redisTemplate.opsForList().leftPush(key, normalizedKeyword);
redisTemplate.opsForList().trim(key, 0, MAX_RECENT_SEARCHES - 1);
redisTemplate.expire(key, Duration.ofDays(30));
}
catch (Exception e) {
log.error(e, "Failed to save recent search");
}
}

@Override
public void deleteRecentSearch(Long userId, String keyword) {
try {
String key = "recent_search:" + userId;
redisTemplate.opsForList().remove(key, 0, keyword.trim().toLowerCase());
} catch (Exception e) {
log.error(e, "Failed to delete recent search");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Optional: prevent JPA transaction for Redis-only methods and add @slf4j.

Outside these changed lines, consider:

  • Annotate class with @Slf4j.
  • Annotate methods with @Transactional(propagation = Propagation.NOT_SUPPORTED) to avoid opening a JPA transaction for Redis ops.

Example (outside selected lines):

import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Propagation;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class StarCommandServiceImpl implements StarCommandService {

    @Override
    @Async
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void saveRecentSearches(Long userId, String keyword) { ... }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void deleteRecentSearch(Long userId, String keyword) { ... }
}

Check that async execution is enabled and key usage is consistent:


🏁 Script executed:

#!/bin/bash
echo "Search for @EnableAsync (required for @Async to work via proxies):"
rg -n '@EnableAsync'

echo ""
echo "Check consistent Redis key usage for recent searches:"
rg -n --pretty 'recent_search:'

Length of output: 604


Add @slf4j and mark Redis‐only methods as non‐transactional

To avoid opening unnecessary JPA transactions for Redis operations and enable logging in this class:

• Annotate StarCommandServiceImpl with @Slf4j (import lombok.extern.slf4j.Slf4j).
• Add @Transactional(propagation = Propagation.NOT_SUPPORTED) (import org.springframework.transaction.annotation.*) to both Redis‐only methods.

Suggested diff in src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandServiceImpl.java:

@@
-import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import lombok.extern.slf4j.Slf4j;

 @Slf4j
 @Service
 @RequiredArgsConstructor
 @Transactional
 public class StarCommandServiceImpl implements StarCommandService {
 
     @Override
     @Async
-    public void saveRecentSearches(Long userId, String keyword){
+    @Transactional(propagation = Propagation.NOT_SUPPORTED)
+    public void saveRecentSearches(Long userId, String keyword) {
         try {
             String key = "recent_search:" + userId;
             String normalizedKeyword = keyword.trim().toLowerCase();
@@
     @Override
-    public void deleteRecentSearch(Long userId, String keyword) {
+    @Transactional(propagation = Propagation.NOT_SUPPORTED)
+    public void deleteRecentSearch(Long userId, String keyword) {
         try {
             String key = "recent_search:" + userId;
             redisTemplate.opsForList().remove(key, 0, keyword.trim().toLowerCase());
📝 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
@Async
public void saveRecentSearches(Long userId, String keyword){
try {
String key = "recent_search:" + userId;
String normalizedKeyword = keyword.trim().toLowerCase();
// 중복 제거
redisTemplate.opsForList().remove(key, 0, normalizedKeyword);
redisTemplate.opsForList().leftPush(key, normalizedKeyword);
redisTemplate.opsForList().trim(key, 0, MAX_RECENT_SEARCHES - 1);
redisTemplate.expire(key, Duration.ofDays(30));
}
catch (Exception e) {
log.error(e, "Failed to save recent search");
}
}
@Override
public void deleteRecentSearch(Long userId, String keyword) {
try {
String key = "recent_search:" + userId;
redisTemplate.opsForList().remove(key, 0, keyword.trim().toLowerCase());
} catch (Exception e) {
log.error(e, "Failed to delete recent search");
}
}
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class StarCommandServiceImpl implements StarCommandService {
@Override
@Async
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void saveRecentSearches(Long userId, String keyword) {
try {
String key = "recent_search:" + userId;
String normalizedKeyword = keyword.trim().toLowerCase();
// 중복 제거
redisTemplate.opsForList().remove(key, 0, normalizedKeyword);
redisTemplate.opsForList().leftPush(key, normalizedKeyword);
redisTemplate.opsForList().trim(key, 0, MAX_RECENT_SEARCHES - 1);
redisTemplate.expire(key, Duration.ofDays(30));
} catch (Exception e) {
log.error(e, "Failed to save recent search");
}
}
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void deleteRecentSearch(Long userId, String keyword) {
try {
String key = "recent_search:" + userId;
redisTemplate.opsForList().remove(key, 0, keyword.trim().toLowerCase());
} catch (Exception e) {
log.error(e, "Failed to delete recent search");
}
}
}
🤖 Prompt for AI Agents
In
src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandServiceImpl.java
around lines 326 to 352, add the @Slf4j annotation to the class to enable
logging and import it from lombok.extern.slf4j.Slf4j. Also, add
@Transactional(propagation = Propagation.NOT_SUPPORTED) annotation to the
saveRecentSearches and deleteRecentSearch methods to mark them as
non-transactional for Redis operations, importing the necessary classes from
org.springframework.transaction.annotation. This prevents unnecessary JPA
transactions during Redis calls and enables proper logging.

Comment on lines +344 to +352
@Override
public void deleteRecentSearch(Long userId, String keyword) {
try {
String key = "recent_search:" + userId;
redisTemplate.opsForList().remove(key, 0, keyword.trim().toLowerCase());
} catch (Exception e) {
log.error(e, "Failed to delete recent search");
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Normalize with Locale.ROOT and fix logging; mirror save behavior.

Ensure delete uses the same normalization and correct logging:

 @Override
 public void deleteRecentSearch(Long userId, String keyword) {
     try {
         String key = "recent_search:" + userId;
-        redisTemplate.opsForList().remove(key, 0, keyword.trim().toLowerCase());
+        if (keyword == null) return;
+        String trimmed = keyword.trim();
+        if (trimmed.isEmpty()) return;
+        redisTemplate.opsForList().remove(key, 0, trimmed.toLowerCase(java.util.Locale.ROOT));
     } catch (Exception e) {
-        log.error(e, "Failed to delete recent search");
+        log.error("Failed to delete recent search", e);
     }
 }
📝 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
public void deleteRecentSearch(Long userId, String keyword) {
try {
String key = "recent_search:" + userId;
redisTemplate.opsForList().remove(key, 0, keyword.trim().toLowerCase());
} catch (Exception e) {
log.error(e, "Failed to delete recent search");
}
}
@Override
public void deleteRecentSearch(Long userId, String keyword) {
try {
String key = "recent_search:" + userId;
if (keyword == null) return;
String trimmed = keyword.trim();
if (trimmed.isEmpty()) return;
redisTemplate.opsForList().remove(key, 0, trimmed.toLowerCase(java.util.Locale.ROOT));
} catch (Exception e) {
log.error("Failed to delete recent search", e);
}
}
🤖 Prompt for AI Agents
In
src/main/java/com/team_nebula/nebula/domain/star/service/StarCommandServiceImpl.java
around lines 344 to 352, the deleteRecentSearch method normalizes the keyword
using toLowerCase() without specifying Locale.ROOT, unlike the save method.
Update the normalization to use keyword.trim().toLowerCase(Locale.ROOT) to
ensure consistent behavior. Also, fix the logging statement to correctly log the
exception by swapping the parameters so the exception is logged properly with
the message.


import lombok.RequiredArgsConstructor;

import static org.springframework.data.neo4j.core.ReactiveNeo4jClient.log;

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

Fix logger: invalid static import and wrong error(...) signature

  • Static-importing ReactiveNeo4jClient.log is incorrect here and likely won’t compile.
  • The call log.error(e, "Failed to get recent searches"); has reversed parameters.

Apply this diff and add Lombok logger:

- import static org.springframework.data.neo4j.core.ReactiveNeo4jClient.log;
+ import lombok.extern.slf4j.Slf4j;
-      log.error(e, "Failed to get recent searches");
+      log.error("Failed to get recent searches for userId={}", userId, e);

Outside this hunk, annotate the class:

// add above class declaration
@Slf4j

Also applies to: 199-199

🤖 Prompt for AI Agents
In
src/main/java/com/team_nebula/nebula/domain/star/service/StarQueryServiceImpl.java
at line 24 and line 199, remove the incorrect static import of
ReactiveNeo4jClient.log and instead add the Lombok @Slf4j annotation above the
class declaration to enable logging. Then, fix the log.error call by swapping
the parameters to have the message first and the exception second, i.e.,
log.error("Failed to get recent searches", e).

}

@Override
@Cacheable(value = "recent_searches", key = "#userId")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid stale results: @Cacheable without eviction on writes

@Cacheable("recent_searches") will cache results, but unless corresponding save/delete methods evict/refresh this cache, users can see stale lists.

  • Ensure write paths (save/delete recent search) use @CacheEvict(value = "recent_searches", key = "#userId") or @CachePut accordingly.
  • Confirm cache TTL aligns with the stated 30-day retention or choose a shorter TTL to reduce staleness risk.

Run this to check for cache eviction on write paths and cache TTL config:


🏁 Script executed:

#!/bin/bash
# Find recent searches write operations and verify cache eviction
rg -n --hidden --glob '!**/node_modules/**' '@CacheEvict\([^)]*value\s*=\s*"recent_searches"' -A 2
rg -n --hidden --glob '!**/node_modules/**' 'saveRecentSearch|saveRecentSearches|deleteRecentSearch' -A 3

# Look for CacheManager/RedisCacheManager TTL config
rg -n --hidden --glob '!**/node_modules/**' 'RedisCacheManager|CacheManager|cacheNames|ttl|timeToLive' -A 3

# Check for any other usage of "recent_searches" to avoid key/cachename collisions
rg -n --hidden --glob '!**/node_modules/**' '"recent_searches"'

Length of output: 6160


Ensure cache invalidation and correct TTL for recent_searches

  • No @CacheEvict found on the write methods in StarCommandServiceImpl—add
    @CacheEvict(value = "recent_searches", key = "#userId")
    to
    saveRecentSearches(Long userId, String keyword) (StarCommandServiceImpl.java:328)
    deleteRecentSearch(Long userId, String keyword) (StarCommandServiceImpl.java:345)
  • Global cache TTL is set to 5 minutes in CacheConfig.java:27–28. If you intend to retain “recent_searches” for 30 days, add an initial cache configuration for that cache, e.g.:
    cacheConfigurations.put(
      "recent_searches",
      RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofDays(30))
        … 
    );
  • The Redis list key "recent_search:<userId>" used by RedisTemplate has no expiration. If you need automatic data cleanup, apply an expire on that key when you write to it.
🤖 Prompt for AI Agents
In
src/main/java/com/team_nebula/nebula/domain/star/service/StarQueryServiceImpl.java
at line 189, the @Cacheable annotation for "recent_searches" lacks corresponding
cache eviction on write operations. To fix this, add @CacheEvict(value =
"recent_searches", key = "#userId") annotations to the methods
saveRecentSearches(Long userId, String keyword) and deleteRecentSearch(Long
userId, String keyword) in StarCommandServiceImpl.java at lines 328 and 345
respectively. Additionally, update CacheConfig.java around lines 27-28 to
configure a specific TTL of 30 days for the "recent_searches" cache by adding a
RedisCacheConfiguration with entryTtl(Duration.ofDays(30)). Finally, ensure that
the Redis list key "recent_search:<userId>" used by RedisTemplate has an
expiration set when writing to it to enable automatic cleanup.

Comment on lines +190 to +193
public List<String> getRecentSearches(Long userId) {
try {
String key = "recent_searches"+userId.toString();
List<Object> searches = redisTemplate.opsForList().range(key, 0, MAX_RECENT_SEARCHES - 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Validate userId and improve Redis key format

  • Add a null-guard consistent with other methods.
  • Use a namespaced key with a delimiter to avoid ambiguous keys and to align with Redis key best practices.
 public List<String> getRecentSearches(Long userId) {
-    try {
-        String key = "recent_searches"+userId.toString();
+    if (userId == null) {
+        throw new GeneralException(ErrorStatus._USER_NOT_FOUND);
+    }
+    try {
+        final String key = String.format("star:recent-searches:%d", userId);
📝 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
public List<String> getRecentSearches(Long userId) {
try {
String key = "recent_searches"+userId.toString();
List<Object> searches = redisTemplate.opsForList().range(key, 0, MAX_RECENT_SEARCHES - 1);
public List<String> getRecentSearches(Long userId) {
if (userId == null) {
throw new GeneralException(ErrorStatus._USER_NOT_FOUND);
}
try {
final String key = String.format("star:recent-searches:%d", userId);
List<Object> searches = redisTemplate.opsForList().range(key, 0, MAX_RECENT_SEARCHES - 1);
🤖 Prompt for AI Agents
In
src/main/java/com/team_nebula/nebula/domain/star/service/StarQueryServiceImpl.java
around lines 190 to 193, add a null check for userId at the start of the
getRecentSearches method to prevent null pointer exceptions, consistent with
other methods. Also, update the Redis key format to include a clear namespace
and delimiter, such as "recent_searches:userId", to avoid ambiguous keys and
follow Redis key naming best practices.

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

Labels

Feature 새로운 기능 추가 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant