-
Notifications
You must be signed in to change notification settings - Fork 3
refactor : 검색 API ES 대신 임시 코드로 대체 #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- 우선은 검색어 자동완성 기능을 비활성화하고, 문제 검색 기능은 제목과 설명만 포함되도록 함 - ES 비활성화 테스트를 위한 임시방편이므로 추후 추가적인 작업 필요
Walkthrough이 PR은 Elasticsearch 기반의 ProblemSearchDocument에서 JPA 기반의 Problem 엔티티로 검색 기능을 마이그레이션합니다. 새로운 저장소 인터페이스와 구현체를 도입하고, 도메인 서비스를 업데이트하며, 검색 서비스에 캐싱을 추가하고, ProblemSearchResponse에 팩토리 메서드를 확장합니다. Changes
Sequence DiagramsequenceDiagram
participant Client
participant ProblemSearchService as ProblemSearchService<br/>(캐싱)
participant ProblemSearchDomainService as ProblemSearchDomainService
participant ProblemSearchRepository as ProblemSearchRepository<br/>(인터페이스)
participant ProblemSearchRepositoryImpl as ProblemSearchRepositoryImpl
participant ProblemSearchJpaRepository as ProblemSearchJpaRepository<br/>(JPA)
participant Database as Database
rect rgb(200, 220, 255)
note over Client,Database: 새로운 검색 흐름 (JPA 기반)
Client->>ProblemSearchService: getProblemSuggestions(keyword)
alt 캐시 히트
ProblemSearchService-->>Client: 캐시된 결과
else 캐시 미스
ProblemSearchService->>ProblemSearchDomainService: searchByKeywordMatch(keyword)
ProblemSearchDomainService->>ProblemSearchRepository: searchProblems(keyword)
ProblemSearchRepository->>ProblemSearchRepositoryImpl: searchProblems(keyword)
ProblemSearchRepositoryImpl->>ProblemSearchJpaRepository: searchByKeyword(keyword)
ProblemSearchJpaRepository->>Database: SELECT * FROM problems<br/>WHERE title LIKE ? OR description LIKE ?
Database-->>ProblemSearchJpaRepository: List<Problem>
ProblemSearchJpaRepository-->>ProblemSearchRepositoryImpl: List<Problem>
ProblemSearchRepositoryImpl-->>ProblemSearchRepository: List<Problem>
ProblemSearchRepository-->>ProblemSearchDomainService: List<Problem>
ProblemSearchDomainService-->>ProblemSearchService: List<Problem>
ProblemSearchService->>ProblemSearchService: 결과 캐싱
ProblemSearchService-->>Client: List<Problem>
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. 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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.java (1)
3-5: 사용하지 않는 import 제거.Line 3의
HashSet과 Line 5의Setimport가 더 이상 사용되지 않습니다. 코드를 정리하기 위해 제거하세요.다음 수정을 적용하세요:
-import java.util.HashSet; import java.util.List; -import java.util.Set;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.java(2 hunks)src/main/java/org/ezcode/codetest/application/problem/service/ProblemSearchService.java(2 hunks)src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemSearchRepository.java(1 hunks)src/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.java(2 hunks)src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemSearchJpaRepository.java(1 hunks)src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemSearchRepositoryImpl.java(1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: Kimminu7
Repo: ezcode-my/backend PR: 133
File: src/test/java/org/ezcode/codetest/domain/problem/service/ProblemDomainServiceTest.java:92-99
Timestamp: 2025-07-02T12:05:54.917Z
Learning: ProblemDomainService의 removeProblem 메서드는 DB에서 Problem을 삭제한 후 Elasticsearch에서도 해당 ProblemSearchDocument를 찾아서 삭제합니다. 만약 Elasticsearch에서 문서를 찾지 못하면 ProblemException(PROBLEM_NOT_FOUND)을 던지므로, 테스트에서는 problem.getId()와 searchRepository.findById() 모두 적절하게 mock해야 합니다.
Learnt from: chat26666
Repo: ezcode-my/backend PR: 31
File: src/main/java/org/ezcode/codetest/domain/problem/model/entity/ProblemSearchDocument.java:49-56
Timestamp: 2025-06-05T00:52:54.460Z
Learning: ProblemSearchDocument.from() 메서드는 트랜잭션 내에서 DB에서 실제로 조회한 Problem 엔티티만 사용하므로 null 체크가 불필요함. 이 컨텍스트에서는 null Problem이 있을 경우 이미 이전 단계에서 예외가 발생함.
Learnt from: Kimminu7
Repo: ezcode-my/backend PR: 63
File: src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java:24-40
Timestamp: 2025-06-14T14:33:58.372Z
Learning: ProblemController에서 ProblemSearchCondition 객체는 항상 new ProblemSearchCondition(category, difficulty)로 유효한 인스턴스를 생성해서 전달하므로, ProblemQueryRepositoryImpl의 searchByCondition 메서드에서 searchCondition 파라미터 자체에 대한 null 체크는 불필요하다. category와 difficulty 필드만 각각 null일 수 있다.
Learnt from: chat26666
Repo: ezcode-my/backend PR: 31
File: src/main/java/org/ezcode/codetest/domain/problem/service/ProblemDomainService.java:25-27
Timestamp: 2025-06-04T15:11:19.343Z
Learning: In ProblemDomainService, when saving problems to both database and Elasticsearch, maintain transactional consistency by allowing rollback if either save operation fails. Data integrity between DB and ES is prioritized over availability - if Elasticsearch save fails, the entire transaction should roll back to prevent data inconsistency.
📚 Learning: 2025-07-02T12:05:54.917Z
Learnt from: Kimminu7
Repo: ezcode-my/backend PR: 133
File: src/test/java/org/ezcode/codetest/domain/problem/service/ProblemDomainServiceTest.java:92-99
Timestamp: 2025-07-02T12:05:54.917Z
Learning: ProblemDomainService의 removeProblem 메서드는 DB에서 Problem을 삭제한 후 Elasticsearch에서도 해당 ProblemSearchDocument를 찾아서 삭제합니다. 만약 Elasticsearch에서 문서를 찾지 못하면 ProblemException(PROBLEM_NOT_FOUND)을 던지므로, 테스트에서는 problem.getId()와 searchRepository.findById() 모두 적절하게 mock해야 합니다.
Applied to files:
src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemSearchRepository.javasrc/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemSearchJpaRepository.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemSearchRepositoryImpl.javasrc/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.javasrc/main/java/org/ezcode/codetest/application/problem/service/ProblemSearchService.java
📚 Learning: 2025-06-14T14:33:58.372Z
Learnt from: Kimminu7
Repo: ezcode-my/backend PR: 63
File: src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java:24-40
Timestamp: 2025-06-14T14:33:58.372Z
Learning: ProblemController에서 ProblemSearchCondition 객체는 항상 new ProblemSearchCondition(category, difficulty)로 유효한 인스턴스를 생성해서 전달하므로, ProblemQueryRepositoryImpl의 searchByCondition 메서드에서 searchCondition 파라미터 자체에 대한 null 체크는 불필요하다. category와 difficulty 필드만 각각 null일 수 있다.
Applied to files:
src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemSearchRepository.javasrc/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemSearchJpaRepository.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemSearchRepositoryImpl.javasrc/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.java
📚 Learning: 2025-06-05T00:52:54.460Z
Learnt from: chat26666
Repo: ezcode-my/backend PR: 31
File: src/main/java/org/ezcode/codetest/domain/problem/model/entity/ProblemSearchDocument.java:49-56
Timestamp: 2025-06-05T00:52:54.460Z
Learning: ProblemSearchDocument.from() 메서드는 트랜잭션 내에서 DB에서 실제로 조회한 Problem 엔티티만 사용하므로 null 체크가 불필요함. 이 컨텍스트에서는 null Problem이 있을 경우 이미 이전 단계에서 예외가 발생함.
Applied to files:
src/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.javasrc/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.java
📚 Learning: 2025-06-15T04:37:29.231Z
Learnt from: chat26666
Repo: ezcode-my/backend PR: 64
File: src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/EncounterChoiceRepositoryImpl.java:0-0
Timestamp: 2025-06-15T04:37:29.231Z
Learning: EncounterChoiceRepositoryImpl in src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/EncounterChoiceRepositoryImpl.java is intentionally a skeleton implementation that is work-in-progress and will be completed later.
Applied to files:
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemSearchRepositoryImpl.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemSearchRepository.java (1)
7-11: LGTM!검색 전용 리포지토리 인터페이스가 깔끔하게 정의되어 있습니다. 메서드 시그니처도 명확합니다.
src/main/java/org/ezcode/codetest/application/problem/service/ProblemSearchService.java (1)
26-32: LGTM!새로운
searchByKeywordMatch메서드를 사용하고Problem엔티티에서ProblemSearchResponse로 변환하는 로직이 올바르게 구현되었습니다.src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemSearchRepositoryImpl.java (1)
11-21: LGTM!리포지토리 구현이 깔끔하게 JPA 리포지토리에 위임하고 있습니다. 구조가 명확하고 단순합니다.
src/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.java (1)
17-23: LGTM!
ProblemSearchRepository로 교체하고 메서드명을searchByKeywordMatch로 변경한 것이 새로운 검색 아키텍처와 잘 맞습니다.List<Problem>을 반환하는 것도 적절합니다.
| public static ProblemSearchResponse from(Problem problem) { | ||
|
|
||
| return ProblemSearchResponse.builder() | ||
| .id(problem.getId()) | ||
| .title(problem.getTitle()) | ||
| .category(null) // TODO: 카테고리 입력해줘야 함 | ||
| .difficulty(problem.getDifficulty().getDifficulty()) | ||
| .reference(problem.getReference().toString()) | ||
| .description(problem.getDescription()) | ||
| .score(problem.getScore()) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for ProblemSearchResponse usages and how the category field is accessed
# Find files that import or use ProblemSearchResponse
rg -nP --type=java 'ProblemSearchResponse' -B2 -A5 | head -100
# Also specifically look for category field access
echo "=== Searching for category field/method access on ProblemSearchResponse ==="
rg -nP --type=java '\.getCategory\(\)|\.category' -B3 -A3Repository: ezcode-my/backend
Length of output: 16749
🏁 Script executed:
#!/bin/bash
# Search for tests or client code that use ProblemSearchResponse
rg -nP --type=java '\.category|ProblemSearchResponse' src/test -B2 -A2 | head -80
# Also check for frontend files that might consume this API
find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \) | grep -i problem | head -20Repository: ezcode-my/backend
Length of output: 109
🏁 Script executed:
#!/bin/bash
# Read the frontend file to see how category is used
cat src/main/resources/static/js/problem-search.jsRepository: ezcode-my/backend
Length of output: 4297
The category field must be populated before sending the response to clients.
Line 46 sets category to null with a TODO comment. The frontend code (problem-search.js, line 72-73) directly renders this field without null checks: <div><b>Category:</b> ${item.category}</div>. When category is null, the UI will display "Category: null", degrading user experience.
Additionally, there's an inconsistency: the from(ProblemSearchDocument) factory method (line 33) populates category with document.getCategories(), while from(Problem) (line 46) leaves it null. API consumers will receive inconsistent response structures.
Populate the category field using Problem's category relationship before the TODO is resolved, or remove the field temporarily if it cannot be populated reliably.
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.java
around lines 41-52, the from(Problem) factory sets category to null which causes
the frontend to render "null" and creates inconsistency with the
from(ProblemSearchDocument) factory that uses document.getCategories(); replace
the null assignment by populating category from the Problem entity (match the
same shape as the other factory: e.g. use problem.getCategories() or map
problem.getCategory() to the same type), and guard against null by defaulting to
an empty list/string as appropriate so the response always contains a non-null
category value.
| // Redis 캐시 적용 (Cache Name: suggestions, Key: keyword) | ||
| @Cacheable(value = "suggestions", key = "#keyword", unless = "#result.isEmpty()") | ||
| public Set<String> getProblemSuggestions(String keyword) { | ||
|
|
||
| return searchDomainService | ||
| .getSuggestionsByKeyword(keyword) | ||
| .stream() | ||
| .flatMap(doc -> { | ||
| Stream.Builder<String> searchedKeyword = Stream.builder(); | ||
|
|
||
| Optional.ofNullable(doc.getTitle()).ifPresent(searchedKeyword::add); | ||
| Optional.ofNullable(doc.getReference()) | ||
| .map(Enum::toString) | ||
| .ifPresent(searchedKeyword::add); | ||
| Optional.ofNullable(doc.getDifficulty()) | ||
| .ifPresent(searchedKeyword::add); | ||
| Optional.ofNullable(doc.getDescription()) | ||
| .ifPresent(searchedKeyword::add); | ||
| Optional.ofNullable(doc.getDifficultyEn()) | ||
| .map(Enum::toString) | ||
| .ifPresent(searchedKeyword::add); | ||
| Optional.ofNullable(doc.getReferenceKor()) | ||
| .ifPresent(searchedKeyword::add); | ||
|
|
||
| Optional.ofNullable(doc.getCategories()) | ||
| .stream() | ||
| .flatMap(Collection::stream) | ||
| .forEach(searchedKeyword::add); | ||
|
|
||
| Optional.ofNullable(doc.getCategoriesKor()) | ||
| .stream() | ||
| .flatMap(Collection::stream) | ||
| .forEach(searchedKeyword::add); | ||
|
|
||
| return searchedKeyword.build(); | ||
| }) | ||
| .map(String::toUpperCase) | ||
| .collect(Collectors.toSet()); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null 반환으로 인한 NullPointerException 위험.
이 메서드는 다음과 같은 심각한 문제들이 있습니다:
- 반환 타입은
Set<String>인데null을 반환하면 호출자에서 NullPointerException이 발생합니다. unless = "#result.isEmpty()"는null에 대해 작동하지 않습니다.null.isEmpty()를 평가하려다 예외가 발생하거나, 캐시 조건이 제대로 작동하지 않습니다.- 결과적으로
null값이 캐시에 저장될 수 있습니다.
자동완성 기능을 비활성화하려면 빈 Set을 반환하거나, 메서드 시그니처를 변경하는 것이 안전합니다.
다음 수정을 적용하세요:
@Cacheable(value = "suggestions", key = "#keyword", unless = "#result.isEmpty()")
public Set<String> getProblemSuggestions(String keyword) {
- return null;
+ return Set.of(); // 빈 Set 반환으로 NPE 방지
}📝 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.
| // Redis 캐시 적용 (Cache Name: suggestions, Key: keyword) | |
| @Cacheable(value = "suggestions", key = "#keyword", unless = "#result.isEmpty()") | |
| public Set<String> getProblemSuggestions(String keyword) { | |
| return searchDomainService | |
| .getSuggestionsByKeyword(keyword) | |
| .stream() | |
| .flatMap(doc -> { | |
| Stream.Builder<String> searchedKeyword = Stream.builder(); | |
| Optional.ofNullable(doc.getTitle()).ifPresent(searchedKeyword::add); | |
| Optional.ofNullable(doc.getReference()) | |
| .map(Enum::toString) | |
| .ifPresent(searchedKeyword::add); | |
| Optional.ofNullable(doc.getDifficulty()) | |
| .ifPresent(searchedKeyword::add); | |
| Optional.ofNullable(doc.getDescription()) | |
| .ifPresent(searchedKeyword::add); | |
| Optional.ofNullable(doc.getDifficultyEn()) | |
| .map(Enum::toString) | |
| .ifPresent(searchedKeyword::add); | |
| Optional.ofNullable(doc.getReferenceKor()) | |
| .ifPresent(searchedKeyword::add); | |
| Optional.ofNullable(doc.getCategories()) | |
| .stream() | |
| .flatMap(Collection::stream) | |
| .forEach(searchedKeyword::add); | |
| Optional.ofNullable(doc.getCategoriesKor()) | |
| .stream() | |
| .flatMap(Collection::stream) | |
| .forEach(searchedKeyword::add); | |
| return searchedKeyword.build(); | |
| }) | |
| .map(String::toUpperCase) | |
| .collect(Collectors.toSet()); | |
| return null; | |
| } | |
| // Redis 캐시 적용 (Cache Name: suggestions, Key: keyword) | |
| @Cacheable(value = "suggestions", key = "#keyword", unless = "#result.isEmpty()") | |
| public Set<String> getProblemSuggestions(String keyword) { | |
| return Set.of(); // 빈 Set 반환으로 NPE 방지 | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/problem/service/ProblemSearchService.java
around lines 19 to 24, the method currently returns null which risks
NullPointerException and breaks the Cacheable unless expression; change the
method to never return null (return an empty Set when no suggestions, e.g.
Collections.emptySet()) and update the cache condition to safely handle null by
using unless = "#result == null || #result.isEmpty()" (or keep unless =
"#result.isEmpty()" if you ensure non-null returns), ensuring no null values are
stored in the cache.
| // TODO: FULLTEXT 인덱스 추가 | ||
| @Query(value = "SELECT * FROM problem WHERE title LIKE CONCAT('%', :keyword, '%') OR description LIKE CONCAT('%', :keyword, '%')", nativeQuery = true) | ||
| List<Problem> searchByKeyword(@Param("keyword") String keyword); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LIKE 패턴의 성능 문제와 결과 제한 필요.
Line 13의 쿼리에 다음 문제들이 있습니다:
- Leading wildcard (
%keyword%)는 인덱스를 사용할 수 없어 전체 테이블 스캔이 발생합니다. 데이터가 많아질수록 심각한 성능 저하가 예상됩니다. - 결과 개수 제한이 없어 대량의 결과가 반환될 수 있습니다.
- TODO에서 언급된 FULLTEXT 인덱스를 추가하면 성능이 개선되지만, 현재는 구현되지 않았습니다.
임시 코드라고 하더라도 프로덕션 환경에서 성능 문제를 일으킬 수 있습니다.
다음 개선 방안을 제안합니다:
- 결과에
LIMIT절을 추가하여 최대 반환 개수를 제한 - FULLTEXT 인덱스 구현 이슈를 생성하여 추적
개선된 쿼리를 생성하거나 FULLTEXT 인덱스 구현 이슈를 생성하시겠습니까?
Summary by CodeRabbit
릴리스 노트
성능 개선
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.