-
Notifications
You must be signed in to change notification settings - Fork 3
refactor : ElasticSearch 제거 및 QueryDSL 기반 검색/자동완성 기능 구현 #198
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 비활성화 테스트를 위한 임시방편이므로 추후 추가적인 작업 필요
- 스프링이 기본 트랜잭션 매니저를 찾지 못해 JPA를 사용할 때마다 NoSuchBeanDefinitionException 예외가 발생하고 있었음 - 이전에 테스트했을 때는 정상적으로 동작했었는데, 코드를 업데이트하면서 문제가 발생한 것으로 보임 - 현재 프로젝트에는 JPA(MySQL)와 MongoDB 두 가지 데이터 소스가 공존하고 있음 - 이름 없는 @transactional은 무조건 JPA(MySQL)가 처리하도록 명시하기 위해 `JpaTransactionConfig` 클래스를 생성함
- Elastic Search에서 담당하던 키워드 검색 기능을 기존의 문제 목록 조회 API에 덧붙이는 형식으로 구현함 - 기존에는 키워드 검색 기능과 카테고리, 난이도별 필터링 검색 API 분리되어 있었음
- API 경로나 반환 타입 등은 기존과 동일하게 유지 - 문제의 제목, 설명과 카테고리 한글명 등을 대상으로 검색어 자동완성을 수행함 - 설명(description)에 키워드가 있어도, 자동완성 목록에는 '제목(title)'을 보여주도록 함
WalkthroughElasticsearch 통합(의존성, 설정, 인덱스, 문서·리포지토리·DSL·어댑터, 관련 DTO/서비스/컨트롤러)을 제거하고, JPA 기반 검색·자동완성(findAutoComplete) 기능과 관련 엔드포인트/컨버터/트랜잭션 빈을 추가·통합했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as ProblemController
participant AppService as ProblemService
participant Domain as ProblemDomainService
participant Repo as ProblemRepository (JPA)
participant DB as RDBMS
Client->>Controller: GET /api/problems/suggestions?keyword=...
Controller->>AppService: getSearchKeywordSuggestions(keyword)
AppService->>Domain: getSearchKeywordSuggestions(keyword)
Domain->>Repo: findAutoComplete(keyword)
Repo->>DB: execute queries (category korName, title/description)
DB-->>Repo: rows
Repo-->>Domain: Set<String> suggestions
Domain-->>AppService: Set<String> suggestions
AppService-->>Controller: Set<String>
Controller-->>Client: 200 OK (Set<String>)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분
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)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
⏰ 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)
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: 1
🧹 Nitpick comments (10)
src/test/java/org/ezcode/codetest/application/problem/service/ProblemServiceTest.java (1)
111-129: 주석 처리된 목록 조회 테스트는 새 검색 로직에 맞게 살리거나, 불필요하다면 제거하는 편이 좋습니다
- 현재
getProblemWithCondition_shouldReturnMappedResponses전체가 주석 처리되어 있어, 목록 조회 + 카테고리 매핑 동작이 이 클래스 기준으로는 더 이상 검증되지 않습니다. ES → QueryDSL로 구현이 변경된 것뿐이라면, 새 검색/조인/정렬 로직에 맞게 이 테스트를 리팩터링해서 유지하는 편이 안전합니다.- 만약 동일 책임을 다른 테스트(예: QueryDSL 기반 검색/자동완성 전용 테스트)에서 충분히 검증하고 있다면, 이 블록은 완전히 삭제해서 “죽은 주석 코드”를 없애는 것이 유지보수 측면에서 더 깔끔합니다.
현재 구현과 테스트 커버리지 구조를 한 번 확인하시고, 재활성화/리팩터링 또는 삭제 중 하나로 정리하는 것을 권장합니다.
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java (3)
32-35: Q타입 필드는private static final상수로 두는 편이 더 명확합니다.
QProblem,QCategory,QProblemCategory는 상태 없는 메타모델이므로 인스턴스 필드보다는 클래스 상수로 두면 재할당 가능성을 줄이고 관례에도 더 잘 맞습니다.- QProblem problem = QProblem.problem; - QCategory category = QCategory.category; - QProblemCategory problemCategory = QProblemCategory.problemCategory; + private static final QProblem problem = QProblem.problem; + private static final QCategory category = QCategory.category; + private static final QProblemCategory problemCategory = QProblemCategory.problemCategory;
71-103: 검색 조건 빌더 로직은 깔끔하지만, 정렬은createdAt고정이라Pageable의Sort가 무시됩니다.
BooleanBuilder+ 개별 헬퍼(eqDifficulty,eqCategoryCode,containsKeyword)로 where 절을 조합한 구조는 가독성 좋고 QueryDSL 패턴에도 잘 맞습니다.- 다만 현재는
orderBy(problem.createdAt.desc())로 고정되어 있어, 컨트롤러에서?sort=categoryCode,desc등으로 넘어온 정렬 조건이 실제 쿼리에는 반영되지 않습니다.정렬 요구사항이
createdAt desc로 고정된 것이 아니라면,Pageable.getSort()를OrderSpecifier<?>로 매핑하는 유틸을 하나 두고 사용하는 쪽을 추천합니다.예시(개념):
private OrderSpecifier<?>[] toOrderSpecifiers(Pageable pageable) { return pageable.getSort().stream() .map(order -> { return switch (order.getProperty()) { case "categoryCode" -> order.isAscending() ? category.code.asc() : category.code.desc(); case "difficulty" -> order.isAscending() ? problem.difficulty.asc() : problem.difficulty.desc(); default -> order.isAscending() ? problem.createdAt.asc() : problem.createdAt.desc(); }; }) .toArray(OrderSpecifier[]::new); }그리고:
List<Problem> content = query .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .orderBy(toOrderSpecifiers(pageable)) .fetch();정렬을 createdAt 고정으로 두는 것이 의도라면, 관련 API 스펙/문서를 그렇게 명시해 두는 것도 좋겠습니다.
105-124: 조건 헬퍼는 ProblemSearchCondition의 trim/blank→null 정책과 잘 맞지만, null/blank 처리 의존성을 주석으로 남겨 두면 좋겠습니다.현재 구현은:
eqDifficulty:difficulty != null일 때만 조건 추가.eqCategoryCode:categoryCode != null일 때만 조건 추가.containsKeyword:keyword == null이면 조건 제거.PR 설명대로
ProblemSearchConditioncompact 생성자가 trim 및 빈 문자열→null 처리를 하고 있다면 문제는 없지만, 이후 다른 곳에서 이 레코드를 직접 생성할 때 같은 불변식을 지키지 않으면 blank 키워드가 그대로 조건에 들어갈 수 있습니다.
containsKeyword에 간단히 주석을 추가해// ProblemSearchCondition에서 blank는 null로 정규화됨정도만 적어 두면, 후속 변경 시 오해를 줄일 수 있겠습니다.src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemRepositoryCustom.java (1)
3-12: 자동완성 전용 메서드 추가는 책임 분리에 도움이 됩니다.
ProblemRepositoryCustom에findAutoComplete(String keyword)를 분리해 둔 덕분에:
- 리스트 검색(
searchByCondition)과- 가벼운 자동완성 후보 조회
가 명확히 구분되어 있어 이후 성능/캐싱 전략을 다르게 가져가기 좋겠습니다. 키워드의 null/blank 처리 규칙은 구현체(
ProblemQueryRepositoryImpl)에 이미 들어가 있으니, Javadoc 수준으로만 인터페이스에 규약을 남겨 두면 더 이해하기 쉬울 것 같습니다.src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/StringToDifficultyConverter.java (1)
8-17: Difficulty 변환 시trim및 한글 난이도 호환성 여부를 한 번 더 점검해 주세요.현재 구현은:
source.toUpperCase()만 적용한 뒤Difficulty.valueOf(...)를 호출하고,- 실패 시
ProblemException(DIFFICULTY_NOT_FOUND)를 던집니다.이 상태에서는:
" easy "처럼 공백이 섞인 값은 모두 실패하며,- (과거 러닝 기준) 한글 난이도 문자열을 사용하던 클라이언트가 아직 있다면 전부 예외로 떨어질 수 있습니다.
- 최소한 공백/널 방어는 아래처럼 보완하는 것을 권장합니다.
- @Override - public Difficulty convert(String source) { - try { - return Difficulty.valueOf(source.toUpperCase()); - } catch (IllegalArgumentException e) { - throw new ProblemException(ProblemExceptionCode.DIFFICULTY_NOT_FOUND); - } - } + @Override + public Difficulty convert(String source) { + if (source == null) { + throw new ProblemException(ProblemExceptionCode.DIFFICULTY_NOT_FOUND); + } + + String normalized = source.trim().toUpperCase(); + try { + return Difficulty.valueOf(normalized); + } catch (IllegalArgumentException e) { + throw new ProblemException(ProblemExceptionCode.DIFFICULTY_NOT_FOUND); + } + }
- 기존 API/프론트가 한글 난이도(예:
"쉬움","보통")를 보내던 경우가 있었다면,
- 이제는 영문 enum name으로 변경된 것이 맞는지,
- 아니면
Difficulty.getDifficultyFromKor(...)같은 헬퍼를 이 컨버터 안에서 함께 사용해 양쪽 형식을 모두 지원할지정책을 한 번 더 확인해 보시는 게 좋겠습니다.
src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemSearchRepository.java (1)
7-10: 새ProblemSearchRepository의 역할과 사용처를 한 번 더 정리할 필요가 있어 보입니다.이 인터페이스는
List<Problem> searchProblems(String keyword)만 노출하고 있는데,
동일 PR 내에 이미ProblemRepository.searchByCondition(...)+ QueryDSL 구현이 존재해 검색 책임이 어느 쪽에 있는지 약간 애매합니다.
- 현재 이 인터페이스를 구현·사용하는 클래스가 없다면,
이번 PR에서는 추가를 미루고 실제 사용 시점에 도입하는 것도 한 방법이고,- 유지한다면
ProblemRepository기반 검색과의 차이(예: 단순 키워드 검색 전용, 특정 읽기 모델 전용 등)를 주석이나 이름으로 조금 더 드러내 주면 좋겠습니다.src/main/java/org/ezcode/codetest/application/problem/service/ProblemService.java (1)
82-98: 검색 메서드 이름 변경은 의미가 더 분명해졌지만, 호출부 정합성만 한 번 더 확인해 주세요.
getProblemWithCondition(Pageable, ProblemSearchCondition)로 이름이 바뀌면서:
- 의미상 "조건을 통한 문제 조회"가 잘 드러나고,
- 내부 구현도 도메인 서비스의
getProblemBySearchCondition을 그대로 따르고 있어 동작은 동일해 보입니다.다만 기존에
getProblemsList(...)이름으로 사용하던 컨트롤러/다른 서비스가 모두 이 새로운 이름을 사용하도록 변경되었는지만 한 번만 점검해 두시면 좋겠습니다.src/main/java/org/ezcode/codetest/domain/problem/service/ProblemDomainService.java (1)
51-62: 메서드명 변경을 고려하세요.메서드가 더 이상 검색 엔진을 업데이트하지 않으므로 메서드명
updateCategoryAndSearchEngine이 실제 동작과 일치하지 않습니다.updateProblemCategories와 같이 더 정확한 이름으로 변경하는 것을 권장합니다.다음 diff를 적용하여 메서드명을 변경하세요:
-public void updateCategoryAndSearchEngine(Problem problem, List<String> categories) { +public void updateProblemCategories(Problem problem, List<String> categories) {src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/ProblemController.java (1)
52-63: 검색 조건 바인딩이 올바르게 구현되었습니다.
@ModelAttribute를 사용한ProblemSearchCondition바인딩이 적절합니다. Record의 compact constructor가 입력 정제를 처리하므로 깔끔한 분리가 이루어졌습니다.선택사항: 명시적인 검증이 필요한 경우
@Valid를 추가하고ProblemSearchCondition필드에 검증 어노테이션을 추가할 수 있습니다:-public ResponseEntity<Page<ProblemResponse>> getProblemsListWithCondition( - @ModelAttribute ProblemSearchCondition condition, +public ResponseEntity<Page<ProblemResponse>> getProblemsListWithCondition( + @Valid @ModelAttribute ProblemSearchCondition condition,단, 현재 구조(compact constructor에서 정제)도 충분히 작동하므로 이는 선택사항입니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (31)
.github/workflows/ci.yml(0 hunks)build.gradle(0 hunks)readme.md(1 hunks)src/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.java(0 hunks)src/main/java/org/ezcode/codetest/application/problem/service/ProblemSearchService.java(0 hunks)src/main/java/org/ezcode/codetest/application/problem/service/ProblemService.java(2 hunks)src/main/java/org/ezcode/codetest/common/config/WebConfig.java(1 hunks)src/main/java/org/ezcode/codetest/domain/problem/model/ProblemSearchCondition.java(1 hunks)src/main/java/org/ezcode/codetest/domain/problem/model/entity/ProblemSearchDocument.java(0 hunks)src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemDocumentRepository.java(0 hunks)src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemRepository.java(1 hunks)src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemSearchRepository.java(1 hunks)src/main/java/org/ezcode/codetest/domain/problem/service/ProblemDomainService.java(3 hunks)src/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.java(0 hunks)src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/config/ElasticsearchConfig.java(0 hunks)src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchAdapter.java(0 hunks)src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepository.java(0 hunks)src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDsl.java(0 hunks)src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDslImpl.java(0 hunks)src/main/java/org/ezcode/codetest/infrastructure/persistence/config/JpaTransactionConfig.java(1 hunks)src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java(4 hunks)src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemRepositoryCustom.java(1 hunks)src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemRepositoryImpl.java(2 hunks)src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/ProblemController.java(4 hunks)src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/ProblemSearchController.java(0 hunks)src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/StringToDifficultyConverter.java(1 hunks)src/main/resources/application.properties(0 hunks)src/main/resources/elasticsearch/my_index-settings.json(0 hunks)src/test/java/org/ezcode/codetest/application/problem/service/ProblemServiceTest.java(1 hunks)src/test/java/org/ezcode/codetest/domain/problem/service/ProblemDomainServiceTest.java(0 hunks)src/test/java/org/ezcode/codetest/infrastructure/notification/MongoTransactionTest.java(1 hunks)
💤 Files with no reviewable changes (16)
- src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/ProblemSearchController.java
- build.gradle
- src/main/java/org/ezcode/codetest/domain/problem/model/entity/ProblemSearchDocument.java
- src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/config/ElasticsearchConfig.java
- .github/workflows/ci.yml
- src/main/resources/elasticsearch/my_index-settings.json
- src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDslImpl.java
- src/test/java/org/ezcode/codetest/domain/problem/service/ProblemDomainServiceTest.java
- src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepository.java
- src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchAdapter.java
- src/main/java/org/ezcode/codetest/application/problem/service/ProblemSearchService.java
- src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemDocumentRepository.java
- src/main/resources/application.properties
- src/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.java
- src/main/java/org/ezcode/codetest/infrastructure/elasticsearch/repository/ProblemElasticsearchRepositoryDsl.java
- src/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.java
🧰 Additional context used
🧠 Learnings (7)
📓 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해야 합니다.
📚 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/service/ProblemDomainService.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemRepositoryCustom.javasrc/main/java/org/ezcode/codetest/domain/problem/repository/ProblemSearchRepository.javasrc/main/java/org/ezcode/codetest/application/problem/service/ProblemService.javasrc/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/ProblemController.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.javasrc/main/java/org/ezcode/codetest/domain/problem/repository/ProblemRepository.javasrc/test/java/org/ezcode/codetest/application/problem/service/ProblemServiceTest.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/service/ProblemDomainService.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemRepositoryCustom.javasrc/main/java/org/ezcode/codetest/domain/problem/repository/ProblemSearchRepository.javasrc/main/java/org/ezcode/codetest/application/problem/service/ProblemService.javasrc/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/ProblemController.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.javasrc/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemRepositoryImpl.javasrc/main/java/org/ezcode/codetest/domain/problem/repository/ProblemRepository.javasrc/main/java/org/ezcode/codetest/domain/problem/model/ProblemSearchCondition.javasrc/test/java/org/ezcode/codetest/application/problem/service/ProblemServiceTest.java
📚 Learning: 2025-06-04T15:11:19.343Z
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.
Applied to files:
src/main/java/org/ezcode/codetest/domain/problem/service/ProblemDomainService.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/domain/problem/service/ProblemDomainService.java
📚 Learning: 2025-06-14T14:55:06.361Z
Learnt from: Kimminu7
Repo: ezcode-my/backend PR: 63
File: src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/ProblemController.java:39-44
Timestamp: 2025-06-14T14:55:06.361Z
Learning: ProblemQueryRepositoryImpl에서 String 타입의 difficulty를 Difficulty enum과 비교할 때는 Difficulty.getDifficultyFromKor() 메서드를 사용해서 String을 enum으로 변환한 후 비교해야 한다. 컨트롤러에서 검증 로직을 추가하지 않고 레포지토리 계층에서 타입 변환을 처리하는 것이 관심사 분리 원칙에 적합하다.
Applied to files:
src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/StringToDifficultyConverter.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/ProblemRepositoryImpl.java
🧬 Code graph analysis (1)
src/main/java/org/ezcode/codetest/common/config/WebConfig.java (1)
src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/StringToDifficultyConverter.java (1)
StringToDifficultyConverter(8-18)
🔇 Additional comments (12)
readme.md (1)
143-143: 기술 스택 정확도 개선Elasticsearch 제거에 따른 기술 스택 업데이트가 올바르게 반영되었습니다. QueryDSL 기반 RDB 검색으로 전환되어 MySQL, Redis, MongoDB만 사용하는 것이 정확히 반영되었습니다.
src/main/java/org/ezcode/codetest/infrastructure/persistence/config/JpaTransactionConfig.java (1)
11-18: JPA 트랜잭션 매니저 빈 정의가 명확하고 적절합니다
@Configuration클래스에서@Primary+@Bean("transactionManager")로 JPA용PlatformTransactionManager를 명시적으로 등록한 구조가 깔끔합니다.- 현재 요구사항 기준으로는 별도 옵션 없이
new JpaTransactionManager(entityManagerFactory)만으로도 충분해 보입니다. 추후 커스텀 설정이 필요해지면 반환 타입을JpaTransactionManager로 두고 추가 프로퍼티 설정을 고려하면 좋겠습니다.src/test/java/org/ezcode/codetest/infrastructure/notification/MongoTransactionTest.java (1)
23-23: mongoTransactionManager 빈 정의 확인됨.
mongoTransactionManager빈이MongoTransactionConfig클래스에서 올바르게 정의되어 있습니다 (라인 11). 테스트에서@Transactional(transactionManager = "mongoTransactionManager")로 지정한 것은 정확하며, JPA가 primary 트랜잭션 매니저로 설정된 환경에서 MongoDB 작업이 올바른 트랜잭션 매니저를 사용하도록 보장합니다.src/main/java/org/ezcode/codetest/common/config/WebConfig.java (1)
8-14: Difficulty 컨버터 등록 방식 적절합니다.
WebMvcConfigurer#addFormatters를 통해StringToDifficultyConverter를 등록한 구조가 단순하고 Spring MVC 바인딩과 잘 맞습니다. 별도 의존성이 없는 컨버터라 new로 직접 생성하는 것도 무리 없어 보입니다.src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemRepositoryImpl.java (1)
33-36: 자동완성 위임 레이어 구성 괜찮습니다.
ProblemRepositoryImpl.findAutoComplete가 custom 레포(ProblemRepositoryCustom)로 바로 위임하는 구조라 테스트/확장성이 좋고, 도메인 레포 인터페이스에 불필요한 인프라 의존도 추가하지 않아 깔끔합니다.src/main/java/org/ezcode/codetest/domain/problem/repository/ProblemRepository.java (1)
18-21: 도메인 레포 수준의 자동완성/검색 API 구성이 자연스럽습니다.
Set<String> findAutoComplete(String keyword)로 자동완성을,Page<Problem> searchByCondition(Pageable, ProblemSearchCondition)로 조건 검색을 노출하는 구조가 명확하고,- 구현체(
ProblemRepositoryImpl+ProblemQueryRepositoryImpl)와도 잘 맞습니다.
ProblemSearchCondition이 내부에서 입력 trim/blank→null 정규화를 해주고 있으므로, 메서드 시그니처에서는 별도의 @nonnull 제약이 없는 것도 괜찮아 보입니다. 상위 계층(서비스/컨트롤러)에서 모두 이 레코드를 통해서만 검색을 만들고 있는지만 한 번만 확인해 두시면 좋겠습니다.src/main/java/org/ezcode/codetest/application/problem/service/ProblemService.java (1)
73-79: 자동완성 서비스 계층 위임 구조/트랜잭션 설정이 적절합니다.
@Transactional(readOnly = true)로 읽기 전용임을 명시했고,- 구현은
problemDomainService.getSearchKeywordSuggestions(keyword)로만 위임하여
도메인/인프라 세부 구현을 잘 숨기고 있습니다.별도 비즈니스 로직이 없으므로, 이후 캐시(Caffeine 등)를 도입하게 되면 이 메서드에 프록시/데코레이터를 거는 식으로 확장하기도 좋겠습니다.
src/main/java/org/ezcode/codetest/domain/problem/service/ProblemDomainService.java (2)
101-104: 문제 삭제 로직 단순화가 적절합니다.ElasticSearch 삭제 로직이 제거되어 메서드가 단순화되었습니다. 이제 RDB만 사용하므로 데이터 일관성 관리가 더 간단해졌습니다.
85-88: Proper null/empty keyword handling confirmedThe method correctly delegates to the repository. The underlying
findAutoCompleteimplementation inProblemQueryRepositoryImplproperly handles null and blank keywords by returning an empty set (lines 38-40).src/main/java/org/ezcode/codetest/domain/problem/model/ProblemSearchCondition.java (2)
15-19: Compact constructor를 활용한 입력 정제가 우수합니다.Java Record의 compact constructor를 사용하여 입력값을 일관되게 정제하는 것은 좋은 패턴입니다. 이를 통해 모든 인스턴스가 정제된 상태를 유지하며, 빈 문자열과 공백을 null로 변환하여 쿼리 로직을 단순화합니다.
21-27: 입력 정제 로직이 정확합니다.
cleanString메서드가 null, 빈 문자열, 공백 전용 문자열을 올바르게 처리합니다. 간결하고 명확한 구현입니다.src/main/java/org/ezcode/codetest/presentation/problemmanagement/problem/ProblemController.java (1)
37-50: 자동완성 엔드포인트 구현이 우수합니다.새로운 자동완성 엔드포인트가 적절한 검증 제약사항과 함께 잘 구현되었습니다:
@NotBlank로 빈 검색어 방지@Size(min=2, max=25)로 적절한 길이 제한- 명확한 API 문서화
...zcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java
Show resolved
Hide resolved
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: 1
🧹 Nitpick comments (2)
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java (2)
52-61: 자동완성 문제 쿼리에 정렬 기준을 추가하면 결과 일관성이 더 좋아집니다현재
limit(10)만 있고orderBy가 없어 DB 구현/플랜에 따라 자동완성 후보 순서가 바뀔 수 있습니다.LinkedHashSet으로 합칠 때 이 순서가 그대로 노출되므로, 정렬 기준을 하나 명시해 두는 것이 좋겠습니다.예를 들어, 생성일 역순이나 제목 사전순 정렬이 자연스러워 보입니다.
- List<String> problems = jpaQueryFactory - .select(problem.title) - .from(problem) - .where(/* 생략 */) - .limit(10) - .fetch(); + List<String> problems = jpaQueryFactory + .select(problem.title) + .from(problem) + .where(/* 생략 */) + .orderBy(problem.createdAt.desc()) // 또는 problem.title.asc() + .limit(10) + .fetch();자동완성 UX를 조금 더 안정적으로 가져가고 싶을 때 고려해볼 만한 수준의 리팩터링입니다.
106-125: 동적 조건 헬퍼 메서드 구현 전반적으로 적절하며, keyword blank 처리만 살짝 더 견고하게 할 수 있습니다
eqDifficulty,eqCategoryCode가 입력이null일 때null을 반환하고,BooleanBuilder.and(null)이 no-op으로 동작하는 패턴은 QueryDSL에서 널리 쓰이는 방식이라 유지보수에 부담이 적습니다.containsKeyword에서 category.korName까지 함께 검색하도록 한 것도 검색/자동완성 기준을 맞추는 데 도움이 됩니다.다만
containsKeyword는 현재keyword == null만 체크하고 있는데,ProblemSearchCondition의 compact constructor에서 trim/빈 문자열 → null 처리가 들어간다는 전제에 의존하고 있습니다(이번 PR 설명 기준). 이 전제에 덜 의존하고 싶다면, 아래처럼 isBlank까지 함께 방어해 두는 것도 선택지입니다.- if (keyword == null) { + if (keyword == null || keyword.isBlank()) { return null; }레코드 쪽에서 이미 보장하고 있다면 그대로 두셔도 되고, 도메인/DTO 변경에 덜 민감하게 만들고 싶을 때 적용해 볼 만한 정도입니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java(4 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 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해야 합니다.
📚 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/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.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/ProblemQueryRepositoryImpl.java
📚 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/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java
🔇 Additional comments (4)
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/ProblemQueryRepositoryImpl.java (4)
3-6: 컬렉션/BooleanExpression import 추가 문제 없음
Collections.emptySet(),LinkedHashSet,Set,BooleanExpression사용에 맞게 필요한 import만 추가되어 있고 불필요한 의존성은 없어 보입니다.
32-35: Q 타입 필드 멤버로 분리한 구성 적절합니다
QProblem,QCategory,QProblemCategory를 필드로 빼서 재사용하는 구조는 QueryDSL 쿼리 가독성과 중복 제거 측면에서 괜찮습니다. 현재 스코프(리포지토리 구현 클래스 한 곳)에서만 쓰이므로 static 상수로까지 올릴 필요도 없어 보입니다.
37-70: 자동완성 전체 흐름(카테고리+문제 병합) 구조는 적절합니다
null/blank키워드에 대해 바로Collections.emptySet()을 반환하는 방어 로직이 명확합니다.- 카테고리와 문제를 각각 가볍게 조회한 뒤
LinkedHashSet으로 병합해 “카테고리 우선 + 중복 제거”를 달성한 구조도 의도에 잘 맞습니다.자동완성 동작 자체의 큰 구조는 이대로 가도 무방해 보입니다.
72-104: 검색 쿼리에서 BooleanBuilder/헬퍼 메서드 활용 방식이 잘 잡혀 있습니다
eqDifficulty,eqCategoryCode,containsKeyword로 조건을 분리해두고BooleanBuilder에.and(...)로 조합한 구조라 가독성이 좋고, 조건 추가/변경 시 영향 범위가 명확합니다.- 본문/카운트 쿼리 모두 동일한
booleanBuilder를.where(...)에 사용해서, 페이지네이션 total과 content 필터 조건이 항상 일치하는 점도 👍입니다.- soft delete는
booleanBuilder.and(problem.isDeleted.isFalse())로 공통 필터로 빼 둔 것이 검색/자동완성 기준 일관성 측면에서 적절합니다.selectDistinct(problem)+problem.countDistinct()조합과leftJoin(problemCategory)...leftJoin(category)패턴도 카테고리 연관 문제 중복 제거 목적에 부합해 보입니다.searchCondition자체에 대한 null 체크를 두지 않은 것도, 컨트롤러에서 항상 유효 인스턴스를 생성해 넘긴다는 기존 러닝과 잘 맞습니다. Based on learnings, this is fine.현재 설계 관점에서 크게 손볼 부분은 없어 보입니다.
| List<String> problems = jpaQueryFactory | ||
| .select(problem.title) | ||
| .from(problem) | ||
| .where( | ||
| problem.isDeleted.isFalse() | ||
| .and(problem.title.containsIgnoreCase(keyword)) | ||
| .or(problem.description.containsIgnoreCase(keyword)) | ||
| ) | ||
| .limit(10) // 문제는 최대 10개까지만 노출 | ||
| .fetch(); |
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.
자동완성 문제 쿼리에서 soft delete 필터가 description에는 제대로 적용되지 않습니다
현재 where 절 체인은 아래와 같이 해석됩니다.
problem.isDeleted.isFalse()
.and(problem.title.containsIgnoreCase(keyword))
.or(problem.description.containsIgnoreCase(keyword));QueryDSL에서는 메서드 체인이 ((A.and(B)).or(C))로 평가되기 때문에, 논리식은:
(isDeleted == false AND title LIKE keyword) OR description LIKE keyword
가 됩니다.
이렇게 되면 description 에 키워드가 포함된 soft-deleted 문제도 자동완성에 노출될 수 있습니다(이전 리뷰에서 지적되었던 soft delete 노출 이슈가 형태만 바뀌어 남아 있는 셈입니다).
isDeleted == false가 title/description 양쪽에 모두 적용되도록 괄호를 조정하는 편이 안전합니다.
- List<String> problems = jpaQueryFactory
- .select(problem.title)
- .from(problem)
- .where(
- problem.isDeleted.isFalse()
- .and(problem.title.containsIgnoreCase(keyword))
- .or(problem.description.containsIgnoreCase(keyword))
- )
- .limit(10) // 문제는 최대 10개까지만 노출
- .fetch();
+ List<String> problems = jpaQueryFactory
+ .select(problem.title)
+ .from(problem)
+ .where(
+ problem.isDeleted.isFalse()
+ .and(
+ problem.title.containsIgnoreCase(keyword)
+ .or(problem.description.containsIgnoreCase(keyword))
+ )
+ )
+ .limit(10) // 문제는 최대 10개까지만 노출
+ .fetch();이렇게 하면 title이든 description이든 키워드가 일치하는 경우에도 항상 isDeleted=false 인 문제만 자동완성에 포함됩니다.
# Conflicts: # src/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.java # src/main/java/org/ezcode/codetest/application/problem/service/ProblemSearchService.java # src/main/java/org/ezcode/codetest/domain/problem/service/ProblemSearchDomainService.java
PR: ElasticSearch 제거 및 QueryDSL 기반 검색/자동완성 구현
📝 요약 (Summary)
기존 ElasticSearch로 구현되어 있던 검색 및 자동완성 기능을 제거하고, JPA & QueryDSL을 사용하여 RDB 기반으로 재구현했습니다.
현재 데이터 규모(문제 약 200개, 카테고리 약 60개) 대비 ElasticSearch는 오버엔지니어링으로 판단되었으며, 이를 제거함으로써 서버 리소스 절약 및 유지보수 복잡도를 낮추는 것이 목적입니다.
🛠 변경 사항 (Key Changes)
1. 🗑️ ElasticSearch 관련 의존성 및 설정 제거
build.gradle:spring-boot-starter-data-elasticsearch의존성 삭제application.yml: ES 연결 설정 및 데이터 소스 설정 삭제docker-compose.yml: ES 컨테이너 제거 (서버 비용 절감)Repository및Document클래스 삭제2. ✨ 검색 기능 구현 (
/api/problems/search)ProblemRepositoryCustom및 구현체(ProblemRepositoryImpl) 추가Category,Difficulty,Keyword조건에 따른BooleanExpression활용Left Join을 사용하여 연관된 카테고리까지 검색 범위 확장 (제목, 설명, 카테고리명 검색)distinct()를 사용하여 1:N 조인 시 발생하는 결과 중복 제거Pageable의 Sort 객체를 QueryDSL의OrderSpecifier로 직접 매핑하여, DTO와 엔티티 간 필드명 불일치(categoryCode등)로 인한 에러 방지3. ⚡️ 자동완성 기능 구현
Category와Problem테이블을 각각 가볍게 조회한 후 메모리에서 병합LinkedHashSet을 사용하여 카테고리 -> 문제 제목 순서 보장 및 중복 제거Set<String>형태로 간결하게 반환4. ♻️ 리팩토링 및 버그 수정
ProblemSearchCondition):trim()및 빈 문자열("") ->null치환 로직 내장 (Service 계층에 정제된 데이터 전달 보장)ProblemCategoryRepository: 잘못된 메서드 명명(findByProblemIdsIn)으로 인한 오류를@Query를 사용하여 명시적으로 해결@EntityGraph: 존재하지 않는 속성(categoryCode)을 참조하던 오류를 올바른 연관관계 필드(category)로 수정📸 테스트 결과 (Test)
[카테고리 명, ..., 문제 제목, ...]순서로 중복 없이 반환 확인?sort=categoryCode,desc요청 시 에러 없이 카테고리 코드 기준 정렬 동작 확인🗣️ 비고 (Notes)
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정
리팩터링
문서
테스트
잡일
✏️ Tip: You can customize this high-level summary in your review settings.