refactor : elasticsearch like filter 수정#15
Conversation
- like로 필터를 걸었을 때 좋아요한 강의가 없다면 빈 리스트를 반환합니다.
📝 WalkthroughWalkthrough검색·미리보기 흐름이 Elasticsearch 문서(LectureDocument) 중심으로 전환되었고, 좋아요 필터(Boolean like)와 likedIds 기반 조기 반환이 도입되었으며 정렬이 Spring Data Sort로 변경되고 조회수 증분용 incrementHits가 추가되었습니다. 인덱싱 시작 흐름과 문서 모델/변환 로직이 확장되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant LectureService
participant LectureSearchRepoImpl
participant LectureElasticService
participant LikeService
Client->>LectureService: sortListOfLectures(...)
LectureService->>LectureSearchRepoImpl: searchLecturesAtPreview(keyword, dept, sort, likeList, pageable, like)
LectureSearchRepoImpl->>LectureElasticService: searchLectureDocument(keyword, dept, sort, likedIds, page, size, like)
alt like == true and likedIds empty
LectureElasticService-->>LectureSearchRepoImpl: return empty Page<LectureDocument>
else
LectureElasticService-->>LectureSearchRepoImpl: return Page<LectureDocument> (documents + total)
end
LectureSearchRepoImpl-->>LectureService: Page<LectureDocument>
LectureService->>LikeService: count likes for document ids
LikeService-->>LectureService: likeCounts
LectureService-->>Client: LecturePageResponse (mapped from LectureDocument + likeCounts)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java(2 hunks)src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java(1 hunks)src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java(1 hunks)src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java (1)
src/main/java/inu/codin/codin/domain/like/service/LikeService.java (1)
applyLikeToLecture(88-96)
🔇 Additional comments (5)
src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (2)
54-54: 변수명 개선이 좋습니다.
liked에서likeList로 변경하여 변수의 타입과 용도를 더 명확하게 표현했습니다.
61-61: 메서드 시그니처 호출 일관성 확인 완료
searchLecturesAtPreview메서드는 인터페이스(LectureSearchRepositoryCustom), 구현체(LectureSearchRepositoryImpl), 서비스 호출 지점(LectureService) 모두에 새로 추가된Boolean like파라미터가 일관되게 반영되어 있어 추가 조치가 필요 없습니다.src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java (2)
54-56: 좋아요 필터 최적화가 잘 구현되었습니다.
like가 true이고 좋아요한 강의 ID 목록이 비어있을 때 빈 결과를 즉시 반환하는 로직이 올바르게 구현되었습니다. 이는 불필요한 Elasticsearch 쿼리를 방지하여 성능을 개선합니다.
49-50: 메서드 시그니처 일관성 확인 완료
LectureElasticService.searchIds메서드에Boolean like가 맨 끝에 추가된 것을 확인했습니다.LectureSearchRepositoryImpl(40행)에서 해당 메서드를 호출할 때도page, size, like순서로 올바르게 전달되고 있습니다.- 다른 호출 지점이 없어 파라미터 순서 변경으로 인한 영향은 없습니다.
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1)
39-40: 파라미터 전달 로직 확인로그 메시지와 Elasticsearch 서비스 호출에서 파라미터가 올바르게 전달되고 있습니다. 파라미터명만 일관성 있게 수정하면 됩니다.
|
|
||
| public interface LectureSearchRepositoryCustom { | ||
| Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> liked, Pageable pageable); | ||
| Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
인터페이스 파라미터 명명 일관성 문제
인터페이스의 List<Long> listList 파라미터명이 서비스 레이어의 likeList와 일치하지 않습니다. 전체 코드베이스에서 일관된 명명을 사용해야 합니다.
다음과 같이 수정을 권장합니다:
-Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like);
+Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> likeList, Pageable pageable, Boolean like);📝 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.
| Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like); | |
| Page<Lecture> searchLecturesAtPreview( | |
| String keyword, | |
| Department department, | |
| SortingOption sortingOption, | |
| List<Long> likeList, | |
| Pageable pageable, | |
| Boolean like | |
| ); |
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java
at line 13, the parameter name listList in the method searchLecturesAtPreview is
inconsistent with the service layer's naming likeList. Rename the parameter from
listList to likeList to maintain consistent naming conventions across the
codebase.
| public Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> liked, Pageable pageable) { | ||
| log.debug("[searchLecturesAtPreview] 강의 조회, keyword={}, department={}, sortingOption={}, liked={}", keyword, department, sortingOption, liked); | ||
| Page<Long> idPage = lectureElasticService.searchIds(keyword, department, sortingOption, liked, pageable.getPageNumber(), pageable.getPageSize()); | ||
| public Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
파라미터 명명 일관성 문제
List<Long> listList 파라미터명이 서비스 레이어의 likeList와 일치하지 않습니다. 코드 가독성을 위해 동일한 명명 규칙을 사용하는 것이 좋겠습니다.
다음과 같이 수정을 권장합니다:
-public Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like) {
+public Page<Lecture> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> likeList, Pageable pageable, Boolean like) {🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java
at line 38, the parameter name List<Long> listList is inconsistent with the
service layer's likeList. Rename listList to likeList to maintain naming
consistency and improve code readability.
- 스케출 필드 추가 - 평점 필드 추가 - 학점 필드 추가
- 서버 재시작 시 -> 문서 업로드 시로 변경
- 검색 메소드가 ID 대신 LectureDocument 페이지를 직접 반환하도 리팩토링하여 조회 효율 개선 - Spring Data의 Sort를 사용하여 동적 정렬 로직 구현 - 복잡한 검색 쿼리를 multiMatch를 사용하여 단순화 - Painless 스크립트를 이용한 강의 조회수(hits) 증가 기능 추가
- Lecture -> LectureDocument 반환에 따른 DTO 수정
LectureElasticService가 LectureDocument를 직접 반환하도록 변경됨에 따라, LectureSearchRepositoryImpl의 searchLecturesAtPreview 메소드 로직을 개선했습니다. 기존 방식의 문제점: - Elasticsearch에서 강의 ID 목록을 가져온 후, 다시 JPA를 통해 RDBMS에서 Lecture 엔티티를 조회하는 2단계 프로세스로 인해 불필요한 DB I/O가 발생했습니다. - Elasticsearch의 정렬 순서를 유지하기 위해 조회된 엔티티를 수동으로 재정렬하는 복잡한 코드가 필요했습니다. 개선된 내용: - 성능 향상 및 로직 단순화 - LectureElasticService로부터 Page<LectureDocument>를 직접 받아 반환하도록 변경하여, 추가적인 DB 조회 및 수동 정렬 로직을 모두 제거했습니다. 이를 통해 응답 성능과 코드의 가독성, 유지보수성을 크게 향상시켰습니다. - 반환 타입 변경 - 메소드의 반환 타입을 Page<Lecture>에서 Page<LectureDocument>로 변경하여, 데이터 소스(Elasticsearch)와 API 응답의 일관성을 맞췄습니다. - 불필요한 코드 제거 - 과거 QueryDSL 검색 방식에 사용되던 @deprecated 메소드들과 더 이상 사용하지 않는 LectureRepository 의존성을 완전히 제거하여 코드를 정리했습니다.
리포지토리 계층이 Elasticsearch 검색 결과를 직접(Page<LectureDocument>) 반환하도록 리팩토링됨에 따라, LectureService의 관련 로직을 다음과 같이 수정하고 최적화했습니다. 주요 변경 사항: - 강의 검색 로직 수정 (sortListOfLectures) - LectureSearchRepository로부터 Page<Lecture> 대신 Page<LectureDocument>를 받도록 수정했습니다. - DTO 변환 로직(getLecturePageResponse)도 LectureDocument를 직접 사용하도록 변경하여, 서비스 계층에서 불필요한 데이터 변환 과정을 제거했습니다. - 리스트 DTO 생성 로직 최적화 (getLecturePageResponse) 응답 데이터 변경: 각 강의의 총 좋아요 수(likeCount)를 조회하여 LecturePreviewResponseDto에 포함하도록 변경했습니다. - 조회수 동기화 로직 추가 (getLectureDetails) - 강의 상세 정보 조회 시, RDBMS의 조회수를 증가시키는 것과 더불어 Elasticsearch 문서의 hits 필드도 함께 증가시키도록 lectureElasticService.incrementHits() 호출을 추가했습니다. 이를 통해 두 데이터 소스(RDBMS, Elasticsearch) 간의 데이터 정합성을 유지합니다. 의존성 변경: - LectureElasticService에 대한 의존성을 추가했습니다.
There was a problem hiding this comment.
Actionable comments posted: 13
♻️ Duplicate comments (2)
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java (1)
13-15: 파라미터 명명(listList) 일관성 문제: likeList로 수정 권장서비스 레이어와의 명명 일관성 및 가독성을 위해
listList→likeList로 변경하세요. 이전 코멘트에서도 동일 이슈가 지적되었습니다.- Page<LectureDocument> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like); + Page<LectureDocument> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> likeList, Pageable pageable, Boolean like);또한 구현체/호출부 전역 반영 확인을 권장합니다:
#!/bin/bash # 인터페이스 구현/호출부 시그니처 일치 여부 확인 rg -n "searchLecturesAtPreview\(" -A 2src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1)
36-36: 파라미터 명명 일관성 문제
List<Long> listList파라미터명이 서비스 레이어의likeList와 일치하지 않습니다. 코드 가독성을 위해 동일한 명명 규칙을 사용하는 것이 좋겠습니다.다음과 같이 수정을 권장합니다:
-public Page<LectureDocument> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> listList, Pageable pageable, Boolean like) { +public Page<LectureDocument> searchLecturesAtPreview(String keyword, Department department, SortingOption sortingOption, List<Long> likeList, Pageable pageable, Boolean like) {
🧹 Nitpick comments (15)
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java (3)
29-29: 상수는 static final로 선언하세요.클래스 상수 성격이므로 static final로 선언하는 편이 관례적이며, 메모리/가독성 측면에서도 이점이 있습니다.
- private final int CHUNK_SIZE = 100; + private static final int CHUNK_SIZE = 100;
47-58: 운영 중 인덱스 즉시 삭제-재생성은 다운타임/권한 리스크가 큽니다. Alias 기반 블루-그린 전략을 고려하세요.현재 로직은 항상 기존 인덱스를 삭제 후 재생성합니다. 색인 중에는 검색이 완전히 비거나 오류가 날 수 있고(다운타임), 삭제 권한/리스크가 큽니다. 권장 전략:
- 신규 인덱스(예: lectures_v2_YYYYMMDDHHmm) 생성 및 매핑/설정 적용
- 신규 인덱스로 전체 색인 수행
- 완료 후 alias(예: lectures_read)를 신규 인덱스로 atomically 전환
- (선택) 이전 인덱스는 보관/삭제
이렇게 하면 무중단 전환과 롤백이 용이합니다. Spring Data Elasticsearch에서도 IndexOperations와 alias 전환을 조합해 구현 가능합니다.
79-79: 로그 태그 일관성"[indexAllLectures]"는 이전 구현의 흔적으로 보입니다. 현재 메서드명에 맞추어 태그를 정리하세요.
- log.info("[indexAllLectures] Indexed Lecture: {}, Total: {}", documents.size(), totalProcessed); + log.info("[performIndexing] Indexed Lecture: {}, Total: {}", documents.size(), totalProcessed);src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (3)
42-43: 로그 파일명은 getOriginalFilename() 사용 권장
MultipartFile#getName()은 파라미터명으로, 실제 업로드 파일명이 아닙니다. 운영 로그 가독성과 추적성을 위해getOriginalFilename()을 사용하세요.- log.info("[uploadNewSemesterLectures] {} 학기 강의 정보 업로드 완료", file.getName()); + log.info("[uploadNewSemesterLectures] {} 학기 강의 정보 업로드 완료", file.getOriginalFilename());- log.info("[uploadNewSemesterRooms] {} 강의실 현황 업데이트 완료", file.getName()); + log.info("[uploadNewSemesterRooms] {} 강의실 현황 업데이트 완료", file.getOriginalFilename());Also applies to: 63-63
71-83: 경로 결합 시 문자열 더하기 대신 File/Path API 사용 권장플랫폼 독립성과 경로 안전성을 위해 경로 결합은 File/Path API를 사용하세요.
- File savedFile = new File(UPLOAD_DIR + file.getOriginalFilename()); + File savedFile = new File(UPLOAD_DIR, file.getOriginalFilename());
86-91: ProcessBuilder 인자 경로 결합 개선스크립트/파일 경로를 문자열 덧셈으로 결합하면 경계 슬래시 문제를 유발할 수 있습니다. File API를 사용하세요.
- ProcessBuilder processBuilder = new ProcessBuilder( - PYTHON_DIR, UPLOAD_DIR + pythonNm, UPLOAD_DIR+ file.getOriginalFilename() - ); + ProcessBuilder processBuilder = new ProcessBuilder( + PYTHON_DIR, + new java.io.File(UPLOAD_DIR, pythonNm).getPath(), + new java.io.File(UPLOAD_DIR, file.getOriginalFilename()).getPath() + );src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
19-26: NPE 방어 로직 여부 확인
lectureSemester혹은 그 내부semester가 null일 가능성이 전혀 없다면 현재 구현으로 충분합니다. 다만, 데이터 정합성이 100% 보장되지 않는다면 NPE가 색인 전체를 중단시킬 수 있습니다. 필요 시 null 가드를 추가하고, 호출 측에서 null을 필터링하도록 합의하세요.예시:
- public static SemesterInfo from(LectureSemester lectureSemester) { - Semester semester = lectureSemester.getSemester(); + public static SemesterInfo from(LectureSemester lectureSemester) { + if (lectureSemester == null || lectureSemester.getSemester() == null) { + return null; + } + Semester semester = lectureSemester.getSemester();src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java (1)
81-83: 사용되지 않는 getTags 메서드 제거 권장
getTags정적 메서드가 더 이상 사용되지 않는 것으로 보입니다. 코드 정리를 위해 제거하는 것을 고려해보세요.- public static List<String> getTags(Set<LectureTag> tags) { - return tags.stream().map(tag -> tag.getTag().getTagName()).toList(); - }src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (2)
58-64: 좋아요 필터링 로직의 조기 반환 처리좋아요가 체크되었지만 사용자가 좋아요한 강의가 없는 경우, 빈 리스트가 Elasticsearch 쿼리에 전달됩니다. 이는 PR 목표와 일치하지만, 조기 반환으로 성능을 개선할 수 있습니다.
다음과 같이 조기 반환을 추가하는 것을 고려해보세요:
if (like != null && like) { likeList = likeService.getLiked(LikeType.LECTURE).stream() .map(likedResponseDto -> Long.valueOf(likedResponseDto.getLikeTypeId())) .toList(); + + // 좋아요한 강의가 없으면 빈 페이지 반환 + if (likeList.isEmpty()) { + return LecturePageResponse.of(List.of(), 0, -1); + } }
95-100: 좋아요 수 조회 성능 최적화각 강의마다 개별적으로 좋아요 수를 조회하는 것은 N+1 성능 문제를 야기할 수 있습니다.
배치로 좋아요 수를 조회하도록 개선해보세요:
private LecturePageResponse getLecturePageResponse(Page<LectureDocument> lecturePage) { + // 모든 강의 ID를 한 번에 조회 + List<Long> lectureIds = lecturePage.getContent().stream() + .map(LectureDocument::getId) + .toList(); + + Map<Long, Integer> likeCountMap = likeService.getLikeCounts(LikeType.LECTURE, lectureIds); return LecturePageResponse.of(lecturePage.stream() - .map(lectureDocument -> { - int likeCount = likeService.getLikeCount(LikeType.LECTURE, String.valueOf(lectureDocument.getId())); - - return LecturePreviewResponseDto.of(lectureDocument, likeCount); - } - ) + .map(lectureDocument -> { + int likeCount = likeCountMap.getOrDefault(lectureDocument.getId(), 0); + return LecturePreviewResponseDto.of(lectureDocument, likeCount); + }) .toList(),
LikeService에 배치 조회 메서드getLikeCounts(LikeType type, List<Long> ids)를 추가해야 합니다.src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1)
37-37: 로그 메시지의 파라미터명 불일치로그 메시지에서
liked={}로 표시되지만 실제로는listList값을 출력하고 있습니다. 혼란을 방지하기 위해 일관성을 맞춰야 합니다.- log.debug("[searchLecturesAtPreview] 강의 조회, keyword={}, department={}, sortingOption={}, liked={}", keyword, department, sortingOption, listList); + log.debug("[searchLecturesAtPreview] 강의 조회, keyword={}, department={}, sortingOption={}, likeList={}, like={}", keyword, department, sortingOption, listList, like);src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (1)
16-20: 엘라스틱 문서 역직렬화 호환성 보완(@NoArgsConstructor 권장)현재 @AllArgsConstructor(protected), @builder만 있습니다. Spring Data ES는 기본 생성자 없이도 빌더를 활용할 수 있으나, 역직렬화/프록시 등 호환성 측면에서 기본 생성자를 두는 편이 안전합니다.
아래 추가를 고려해 주세요.
@Getter @Builder @AllArgsConstructor(access = AccessLevel.PROTECTED) +@lombok.NoArgsConstructor(access = AccessLevel.PROTECTED) public class LectureDocument {src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java (3)
97-97: 하드코딩된 인덱스명 제거: 도큐먼트 기반 좌표 사용 권장"lectures" 문자열 하드코딩은 인덱스명 변경 시 누락 위험이 있습니다. Annotation 기반 인덱스 좌표를 사용하세요.
아래처럼 교체:
- SearchHits<LectureDocument> hits = elasticsearchOperations.search(query, LectureDocument.class, IndexCoordinates.of("lectures")); + SearchHits<LectureDocument> hits = elasticsearchOperations.search( + query, + LectureDocument.class, + elasticsearchOperations.getIndexCoordinatesFor(LectureDocument.class) + );
116-116: incrementHits에서도 인덱스명 하드코딩 제거동일한 맥락으로 Update에서도 인덱스 좌표를 도큐먼트로부터 가져오도록 변경하세요.
- elasticsearchOperations.update(uq, IndexCoordinates.of("lectures")); + elasticsearchOperations.update(uq, elasticsearchOperations.getIndexCoordinatesFor(LectureDocument.class));
45-46: JavaDoc 반환 타입 주석 불일치메서드 반환 타입이 Page로 변경되었으나 주석은 Page 입니다. 주석을 최신화하세요.
- * @return Page<Long> + * @return Page<LectureDocument>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (12)
src/main/java/inu/codin/codin/domain/elasticsearch/convertor/LectureDocumentConverter.java(1 hunks)src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java(3 hunks)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java(1 hunks)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java(1 hunks)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java(1 hunks)src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java(2 hunks)src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java(3 hunks)src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java(2 hunks)src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java(2 hunks)src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java(2 hunks)src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java(4 hunks)src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java(4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (2)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
Getter(9-33)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
Getter(10-27)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (2)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
Getter(9-32)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
Getter(10-27)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (2)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
Getter(9-32)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
Getter(9-33)
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (2)
src/main/java/inu/codin/codin/global/auth/filter/TokenValidationFilter.java (1)
RequiredArgsConstructor(21-65)src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (1)
Service(32-128)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (4)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
Getter(9-32)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
Getter(9-33)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
Getter(10-27)src/main/java/inu/codin/codin/domain/lecture/entity/Lecture.java (1)
Getter(16-89)
🔇 Additional comments (9)
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryCustom.java (2)
13-15: ES 문서 기반 반환 타입 전환은 타당합니다미리보기 검색을 Elasticsearch Document 중심으로 전환한 점 일관됩니다. 서비스 레이어(LectureService.sortListOfLectures)와도 시그니처가 부합합니다.
13-15: like 필터 조기 반환 전략 확인PR 목적(좋아요 필터가 활성화이고 좋아요 목록이 비어있을 때 빈 목록 반환)에 따라, 구현체에서
Boolean.TRUE.equals(like) && (likeList == null || likeList.isEmpty())조건 시 즉시 빈 Page를 반환하는지 확인 바랍니다. 불필요한 ES 쿼리 수행을 줄일 수 있습니다.src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
1-32: 구조 깔끔합니다.null 입력 처리와 필드 매핑이 명확합니다. 기존 DTO(ScheduleInfo, SemesterInfo)와도 일관된 스타일입니다.
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
1-33: LGTM! 일관된 DTO 구조다른 DTO 클래스들(
SemesterInfo,EmotionInfo)과 일관된 구조를 가지고 있으며, null 안전성도 적절하게 처리되었습니다. Elasticsearch 필드 타입도 적절히 설정되었습니다.src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (1)
77-85: 트랜잭션과 Elasticsearch 동기화 이슈데이터베이스 트랜잭션 내에서 Elasticsearch 인덱스를 업데이트하는 것은 데이터 일관성 문제를 야기할 수 있습니다. 트랜잭션이 롤백되어도 Elasticsearch 업데이트는 롤백되지 않습니다.
다음 중 하나의 방법을 고려해보세요:
옵션 1: 트랜잭션 후 업데이트
-@Transactional public LectureDetailResponseDto getLectureDetails(Long lectureId) { Lecture lecture = lectureRepository.findLectureWithScheduleAndTagsById(lectureId) .orElseThrow(() -> new LectureException(LectureErrorCode.LECTURE_NOT_FOUND)); - lecture.increaseHits(); + + // 트랜잭션 분리 + updateHits(lecture); Emotion emotion = emotionService.getOrMakeEmotion(lecture); - - lectureElasticService.incrementHits(lectureId); return LectureDetailResponseDto.of(lecture, emotion, userReviewStatsService.isOpenKeyword()); } +@Transactional +private void updateHits(Lecture lecture) { + lecture.increaseHits(); + // 트랜잭션 커밋 후 ES 업데이트 + lectureElasticService.incrementHits(lecture.getId()); +}옵션 2: 이벤트 기반 처리
Spring의@TransactionalEventListener를 사용하여 트랜잭션 커밋 후 ES 업데이트를 수행하세요.src/main/java/inu/codin/codin/domain/elasticsearch/convertor/LectureDocumentConverter.java (2)
52-80: LGTM! 일관된 null 안전성과 헬퍼 메서드 구조모든 헬퍼 메서드들이 일관된 null 안전성 검사를 수행하고, 적절한 변환 로직을 구현했습니다. 코드의 가독성과 유지보수성이 크게 향상되었습니다.
44-50: 선수과목 문자열 파싱 포맷 확인 필요
toPreCourses(String preCourse)에서 사용 중인preCourse.split(",\\s*")가 실제 DB에 저장된preCourse값 포맷(구분자, 공백 유무 등)과 일치하는지 확인해주세요.- DB에 저장된
preCourse샘플 문자열을 직접 조회해,
- 콤마 외에 세미콜론(
;), 슬래시(/) 등 다른 구분자가 사용되는지- 연속 콤마(
",,")나 선행·후행 공백 처리가 필요한 경우가 있는지- 빈 문자열, 특수문자 포함 케이스가 있는지
- 다양한 케이스를 검증할 수 있는 단위 테스트 추가도 함께 고려 바랍니다.
src/main/java/inu/codin/codin/domain/lecture/repository/LectureSearchRepositoryImpl.java (1)
38-49: LGTM! 깔끔한 Elasticsearch 통합JPA 기반의 복잡한 쿼리 로직을 Elasticsearch로 위임하여 코드가 훨씬 간결해졌습니다. 빈 결과에 대한 처리도 적절합니다.
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (1)
73-85: ES 8에서indexOptions = offsets는 허용됩니다 — 변경 불필요
ES 8 공식 문서에 따르면 Text 필드의index_options에offsets값을 지정해도 매핑 오류가 발생하지 않습니다.
따라서 아래 설정을 그대로 유지하셔도 무방합니다.•
offsets는 문서 ID, 용어 빈도, 위치 정보, 시작/끝 오프셋을 모두 색인해 하이라이팅 기능을 지원합니다.
• 하이라이팅이 필요 없다면 기본값인positions(문서 ID, 빈도, 위치)로 두어 저장 공간을 절약할 수 있습니다.Likely an incorrect or invalid review comment.
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java
Outdated
Show resolved
Hide resolved
| @Field(type = FieldType.Keyword) | ||
| private List<String> tags; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
tags를 Keyword로만 매핑했는데 검색에서 multiMatch 대상으로 사용 → 검색 품질 저하 가능
현재 검색 쿼리에서 tags를 multiMatch로 조회합니다(서비스 파일 참조). Keyword 타입은 토큰화가 되지 않아 부분/형태소 매칭이 어려워 recall이 떨어질 수 있습니다. Text+Keyword 멀티 필드로 전환을 권장합니다.
아래와 같이 멀티필드로 전환해 주세요(한국어 형태소 분석을 원하는 경우).
- @Field(type = FieldType.Keyword)
- private List<String> tags;
+ @MultiField(
+ mainField = @Field(type = FieldType.Text, analyzer = "nori", searchAnalyzer = "nori"),
+ otherFields = { @InnerField(suffix = "keyword", type = FieldType.Keyword) }
+ )
+ private List<String> tags;멀티필드를 도입하기 어렵다면, 검색 쿼리에서 tags를 multiMatch 대상에서 제외하고 정확 일치 기반(term/terms)으로만 사용해 주세요.
📝 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.
| @Field(type = FieldType.Keyword) | |
| private List<String> tags; | |
| @MultiField( | |
| mainField = @Field(type = FieldType.Text, analyzer = "nori", searchAnalyzer = "nori"), | |
| otherFields = { @InnerField(suffix = "keyword", type = FieldType.Keyword) } | |
| ) | |
| private List<String> tags; |
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java
around lines 64-66, the tags field is currently mapped only as Keyword which
prevents tokenization and hurts multiMatch recall; change tags to a multi-field
mapping with a text subfield (e.g., "tags": { "type":"keyword", "fields": {
"text": { "type":"text", "analyzer":"nori" } } }) so multiMatch queries can
target tags.text (use the appropriate Korean analyzer configured in your ES
setup), or if you cannot change the mapping, remove tags from multiMatch queries
and use term/terms exact-match queries for tags instead.
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java
Outdated
Show resolved
Hide resolved
src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java
Outdated
Show resolved
Hide resolved
| Query boolQuery = QueryBuilders.bool(b -> { | ||
| if (keyword != null && !keyword.isBlank()) { | ||
| b.should(s -> s.multiMatch(mm -> mm | ||
| .fields("lectureNm^3", "tags^2", "professor", "type", "lectureType", "evaluation", "preCourses") | ||
| .query(keyword) | ||
| .operator(Operator.Or) | ||
| )) | ||
| .should(s -> s.wildcard(w -> w | ||
| .field("lectureNm") | ||
| .value("*" + keyword + "*") | ||
| )) | ||
| .should(s -> s.match(m -> m | ||
| .field("tags") | ||
| .query(keyword) | ||
| )) | ||
| .minimumShouldMatch("1"); // 최소 매칭 갯수 | ||
| b.must(m -> m.multiMatch(mm -> mm | ||
| .fields("lectureNm^3", "tags^2", "professor", "type", "lectureType", "evaluation") | ||
| .query(keyword) | ||
| .operator(Operator.Or) | ||
| )); | ||
| } else { |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
multiMatch 대상 필드의 매핑 확인 필요(tags/type/lectureType/evaluation)
현재 multiMatch 대상에 "tags^2", "type", "lectureType", "evaluation"이 포함되어 있습니다. 이들 필드가 Keyword로 매핑되어 있으면 형태소/부분 매칭이 어려워 검색 품질이 저하됩니다. 최소한 tags는 Text(또는 Text+Keyword)로 매핑 전환이 필요합니다(문서 파일 코멘트 참조). 나머지 필드도 검색 요구사항에 따라 Text 멀티필드 도입을 검토해 주세요.
🏁 Script executed:
#!/bin/bash
# Locate Elasticsearch @Document definitions related to Lecture
doc_files=$(rg -l "@Document" --ignore-case | grep -i "lecture")
echo "Found document classes:"
echo "$doc_files"
# Inspect mapping annotations and field types in those files
for f in $doc_files; do
echo "=== $f ==="
rg -n "@Field" -A1 "$f"
rg -n "private .*tags" -C1 "$f"
rg -n "private .*type" -C1 "$f"
rg -n "private .*lectureType" -C1 "$f"
rg -n "private .*evaluation" -C1 "$f"
doneLength of output: 2914
LectureDocument 필드 매핑 개선 필요 (tags/type/lectureType/evaluation)
현재 LectureDocument의 아래 필드가 모두 FieldType.Keyword로 매핑되어 있어 멀티매치 검색 시 형태소 분석이나 부분 매칭이 불가능합니다.
- tags (
@Field(type = FieldType.Keyword)라인 64–65) - type (
@Field(type = FieldType.Keyword)라인 40–41) - lectureType (
@Field(type = FieldType.Keyword)라인 43–44) - evaluation (
@Field(type = FieldType.Keyword)라인 46–47)
조치 사항:
- tags 최소한
FieldType.Text(또는Text+Keyword멀티필드)로 매핑 전환 - 검색 UX 요구사항에 따라 type, lectureType, evaluation에도 Text 멀티필드 도입 검토
대상 파일
- src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java
src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java
Outdated
Show resolved
Hide resolved
| private LectureStartupIndexer indexer; | ||
|
|
There was a problem hiding this comment.
NPE 위험: indexer 필드가 주입되지 않습니다. final로 선언해 @requiredargsconstructor 주입을 활성화하세요.
현재 필드는 non-final이며 @Autowired도 없어 null 상태가 됩니다. line 44의 indexer.lectureIndex()에서 NPE가 발생합니다.
- private LectureStartupIndexer indexer;
+ private final LectureStartupIndexer indexer;📝 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.
| private LectureStartupIndexer indexer; | |
| private final LectureStartupIndexer indexer; |
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
around lines 27-28, the indexer field is non-final and not injected (causing NPE
at line 44 when calling indexer.lectureIndex()); make the field final and enable
constructor injection by annotating the class with Lombok's
@RequiredArgsConstructor (or manually add a constructor that accepts the
indexer) so Spring injects it properly; alternatively, if you prefer field
injection, add @Autowired to the field, but preferred fix is to mark the field
final and use @RequiredArgsConstructor for safe injection.
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
Outdated
Show resolved
Hide resolved
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
Show resolved
Hide resolved
| ProcessBuilder processBuilder = new ProcessBuilder( | ||
| PYTHON_DIR, UPLOAD_DIR + pythonNm, UPLOAD_DIR+ file.getOriginalFilename() | ||
| ); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
스크립트 경로 설정 확인 필요: 업로드 디렉토리(UPLOAD_DIR)에 스크립트가 존재합니까?
현재 UPLOAD_DIR + pythonNm로 스크립트 경로를 구성하고 있습니다. 일반적으로 스크립트는 업로드 폴더가 아닌 별도의 스크립트 디렉토리에 위치합니다. 설정 의도와 맞는지 점검 바랍니다. 의도된 구성이 아니라면 python.script.dir 등 별도 프로퍼티로 분리하는 것을 권장합니다.
다음 스크립트로 설정/사용 위치를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
# 설정 키 사용/선언 여부 점검
rg -n "lecture\.file\.path|lecture\.python\.path" -A 3
# 스크립트 파일 존재 가능성 확인(레포지토리 내에 있는지)
fd -a "infoOfLecture.py" || true
fd -a "dayTimeOfRoom.py" || trueLength of output: 859
🏁 Script executed:
#!/bin/bash
# Display the LectureUploadService to understand context
sed -n '1,200p' src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.javaLength of output: 3876
스크립트 경로 설정 검토 필요
현재 UPLOAD_DIR + pythonNm으로 스크립트를 찾도록 되어 있으나, 코드베이스 내에서 .py 파일(infoOfLecture.py, dayTimeOfRoom.py)이 발견되지 않습니다.
실제 런타임 환경에 업로드 디렉터리에 스크립트를 배치할 예정인지 확인해 주세요. 의도된 설정이 아니라면 스크립트 전용 디렉터리(예: lecture.script.path 또는 python.script.dir)를 별도 프로퍼티로 분리하는 것을 권장합니다.
점검 위치:
- 설정 프로퍼티:
@Value("${lecture.file.path}") UPLOAD_DIR(LectureUploadService.java:21)@Value("${lecture.python.path}") PYTHON_DIR(LectureUploadService.java:24)
- 스크립트 호출부:
executeGetExitCode()내new ProcessBuilder(PYTHON_DIR, UPLOAD_DIR + pythonNm, …)(LectureUploadService.java:86–88)
- 코드베이스 내 스크립트 파일 미발견:
infoOfLecture.py,dayTimeOfRoom.py검색 결과 없음
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
around lines 86 to 88, the ProcessBuilder is assembling the python script path
as UPLOAD_DIR + pythonNm which assumes the .py scripts live in the upload
directory; confirm whether scripts will actually be deployed there and if not,
change the implementation to use a dedicated script directory property (e.g.
add/use @Value("${lecture.python.script.dir}") or similar) for PYTHON_SCRIPT_DIR
and build the command as new ProcessBuilder(PYTHON_DIR, PYTHON_SCRIPT_DIR + "/"
+ pythonNm, UPLOAD_DIR + file.getOriginalFilename()), update related
configuration keys and docs, and/or ensure the expected .py files are
committed/deployed to the chosen script directory so the runtime process can
locate them.
- LectureDocument likes, hits 필드 long -> Integer로 변경 - LectureElasticService searchLectureDocument() like NPE 방지 - LecturePreviewResponseDto Type 필드 NPE 방지 - LectureStartupIndexer - totalProcessed 오버플로우 방지 - index refresh를 통한 가시성 확보 - LectureUploadService 에러 코드 혼재 사용 수정
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (4)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (1)
64-66: tags를 Keyword 단일 매핑 → 검색 multiMatch 대상이면 텍스트 서브필드 필요검색에서 tags를 부분/형태소 매칭하려면 tokenization 가능한 text 서브필드가 필요합니다. 아래와 같이 멀티필드 적용을 고려해 주세요(필터/정렬은 tags.keyword 사용, 풀텍스트 검색은 tags 사용).
- @Field(type = FieldType.Keyword) - private List<String> tags; + @MultiField( + mainField = @Field(type = FieldType.Text, analyzer = "nori", searchAnalyzer = "nori"), + otherFields = { @InnerField(suffix = "keyword", type = FieldType.Keyword) } + ) + private List<String> tags;변경 시 재색인이 필요합니다. 당장 멀티필드 도입이 어렵다면, 쿼리에서 tags를 multi_match 대상에서 제외하고 term/terms 기반의 정확 일치 필터만 사용해 주세요.
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java (1)
64-64: 이전 리뷰 의견이 부분적으로만 반영됨
totalProcessed를long으로 변경한 것은 좋습니다. 하지만refresh()호출의 오류 처리가 여전히 누락되어 있습니다.refresh 실패 시 로깅 추가:
- elasticsearchOperations.indexOps(LectureDocument.class).refresh(); + try { + elasticsearchOperations.indexOps(LectureDocument.class).refresh(); + } catch (Exception e) { + log.warn("Elasticsearch refresh 실패, 검색 가시성이 지연될 수 있습니다: {}", e.getMessage()); + }src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (2)
85-88: Python 스크립트 경로 설정 검증 필요이전 리뷰에서 지적된 대로, 스크립트 파일이 업로드 디렉토리에 위치하는 것이 의도된 설계인지 확인이 필요합니다.
스크립트 파일 존재 여부와 경로 설정을 확인하겠습니다:
#!/bin/bash # Python 스크립트 파일 검색 echo "=== Python 스크립트 파일 검색 ===" fd -e py "infoOfLecture|dayTimeOfRoom" # 설정 파일에서 경로 설정 확인 echo -e "\n=== application 설정 파일 검색 ===" fd -e yml -e yaml -e properties | xargs rg -l "lecture\.(file|python)\.path" | head -5 # 설정 값 확인 echo -e "\n=== 경로 설정 값 확인 ===" fd -e yml -e yaml -e properties | xargs rg "lecture\.(file|python)\.path" -A 1 -B 1
46-46: 로깅 방식 개선됨이전 리뷰에서 지적된 대로
log.error(e.getMessage(), e)로 수정되어 전체 스택 트레이스가 로그에 기록됩니다. 단, 65번 라인은 여전히 수정이 필요합니다.- log.error(e.getMessage(), e.getStackTrace()[0]); + log.error(e.getMessage(), e);Also applies to: 65-65
🧹 Nitpick comments (5)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (4)
86-93: aiSummary store=true는 저장소 비용 증가 — 필요 시에만 활성화 권장store=true는 _source와 별개로 필드를 저장해 디스크 사용량을 늘립니다. 조회에서 필드 저장(store)을 직접 사용하지 않는다면 제거해도 됩니다(일반적으로 _source에서 충분).
- @Field(type = FieldType.Text, - analyzer = "standard", - searchAnalyzer = "standard", - indexOptions = IndexOptions.positions, - termVector = TermVector.no, - store = true) + @Field(type = FieldType.Text, + analyzer = "standard", + searchAnalyzer = "standard", + indexOptions = IndexOptions.positions, + termVector = TermVector.no)필요하다면 나중에 field retrieval 경로에서만 선택적으로 store를 켜는 것을 고려해 주세요.
37-48: Enum 기반 필드의 Keyword 매핑 합리적(type/department/evaluation 등)정확 일치 필터 및 정렬에 적합합니다. 쿼리 설계에서 소문자 정규화가 필요하면 normalizer 도입을 검토해 주세요.
61-63: Nested 타입 사용 여부 재검토 필요 (semesters / schedule)
Nested 타입은 강력하지만 쿼리 비용이 높습니다. 중첩 문서 단위의 join-like 매칭이 실제로 필요하다면 적합하지만, 단순 필터/집계만 필요하다면 object 타입으로도 충분할 수 있습니다.
현재 코드베이스에서NestedQuery나NestedQueryBuilder등을 통해 해당 필드로 중첩 쿼리를 사용하는 흔적이 보이지 않습니다. 실제 쿼리 설계 및 사용 계획을 다시 한번 확인해 주세요.– 검토 대상 위치
• src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java
– Lines 61–63 (semesters)
– Lines 67–69 (schedule)
31-32: 인덱스 재생성 & 매핑 로직 확인, Alias 기반 마이그레이션 전략 검토 필요아래 위치에서 인덱스 삭제→생성→매핑 및 재색인 로직이 구현되어 있습니다.
- LectureStartupIndexer
(src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java:48–56)
• indexOps.exists() → indexOps.delete()
• indexOps.create() → indexOps.putMapping(...)
• lectureElasticRepository.saveAll(...) → indexOps.refresh()인덱스 설정(커스텀 분석기 포함)은 application.yml에 정의되어 있습니다.
- src/main/resources/application.yml:35–40, 51–54 (lectures.settings.analysis, analyzer 등)
다만, alias 생성/전환(reindex API, rollover) 또는 versioned index 명명 전략은 코드베이스에서 확인되지 않습니다.
무중단 마이그레이션이 필요하다면 alias 전환 전략을 추가 구현하거나,
downtime 재색인이 허용되는지 여부를 검토 후 회신 부탁드립니다.src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (1)
71-83: 파일 저장 로직 개선됨try-with-resources를 사용하여 리소스 관리가 개선되었습니다.
파일 덮어쓰기 전 기존 파일 백업을 고려하세요:
private void saveFile(MultipartFile file) { String originalName = file.getOriginalFilename(); if (originalName == null || originalName.isBlank()) { throw new LectureUploadException(LectureErrorCode.FILE_READ_FAIL, "파일 이름이 비어있습니다."); } File savedFile = new File(UPLOAD_DIR + file.getOriginalFilename()); + if (savedFile.exists()) { + File backup = new File(UPLOAD_DIR + "backup_" + System.currentTimeMillis() + "_" + originalName); + savedFile.renameTo(backup); + log.info("기존 파일 백업: {}", backup.getName()); + } try (FileOutputStream fos = new FileOutputStream(savedFile)) { fos.write(file.getBytes()); } catch (IOException e) { throw new LectureUploadException(LectureErrorCode.FILE_READ_FAIL, e.getMessage()); } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (5)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java(3 hunks)src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java(2 hunks)src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java(3 hunks)src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java(2 hunks)src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/inu/codin/codin/domain/elasticsearch/service/LectureElasticService.java
- src/main/java/inu/codin/codin/domain/lecture/dto/LecturePreviewResponseDto.java
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (2)
src/main/java/inu/codin/codin/global/auth/filter/TokenValidationFilter.java (1)
RequiredArgsConstructor(21-65)src/main/java/inu/codin/codin/domain/lecture/service/LectureService.java (1)
Service(32-128)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (5)
src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/EmotionInfo.java (1)
Getter(9-32)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/SemesterInfo.java (1)
Getter(10-27)src/main/java/inu/codin/codin/domain/elasticsearch/document/dto/ScheduleInfo.java (1)
Getter(9-33)src/main/java/inu/codin/codin/domain/lecture/entity/Lecture.java (1)
Getter(16-89)src/main/java/inu/codin/codin/domain/elasticsearch/event/LectureSavedEvent.java (1)
Getter(7-11)
🔇 Additional comments (6)
src/main/java/inu/codin/codin/domain/elasticsearch/document/LectureDocument.java (5)
55-60: likes/hits 타입 정합성 확보: Integer 매핑으로 수정된 점 좋습니다이전 매핑(Long) ↔ 자바 필드(Integer) 불일치 이슈가 해소되어 정렬/집계 시 충돌 리스크가 줄었습니다.
73-85: syllabus 멀티필드 + offsets/termVector 설정 적절하이라이팅(FVH) 및 구간 기반 스니펫 추출을 고려한 indexOptions=offsets와 termVector 설정이 합리적입니다.
31-32: credit(Integer) 필드 추가 적절도메인과 매핑이 일치하며 정렬/필터에 활용 가능해 보입니다.
49-51: 확인 완료: preCourse → preCourses 변환 로직이 안전하게 처리되고 있습니다
LectureDocumentConverter#toPreCourses메서드에서
preCourse가null또는 빈 문자열일 때 빈 리스트 반환- 쉼표+공백(
",\s*") 기준으로 안전하게 토큰 분리로직이 요구사항에 부합하므로 추가 수정은 불필요합니다.
25-26: nori_autocomplete 분석기 설정 확인됨application.yml(56–59행)에 이미
nori_autocomplete커스텀 분석기가 정의되어 있어, 해당 Analyzer를 사용하는 인덱스 생성 시점에 설정이 적용됩니다.• 설정 위치
- src/main/resources/application.yml:56–59
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java (1)
27-28: 의존성 주입이 올바르게 설정되었습니다이전 리뷰에서 지적된 NPE 위험이
final필드와@RequiredArgsConstructor를 통해 해결되었습니다.
| public void lectureIndex() { | ||
| if (!indexerEnabled) { | ||
| log.info("ElasticSearch indexer 사용할 수 없습니다."); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // 인덱스 존재 여부 확인 및 생성 | ||
| manageIndex(); | ||
| long totalProcessed = performIndexing(); | ||
|
|
||
| log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
인덱싱 오류 처리 보완 필요
lectureIndex() 메서드는 public으로 선언되어 외부(LectureUploadService)에서 호출되지만, Elasticsearch 연결 실패나 인덱싱 중 예외가 발생할 경우 호출자에게 전파되지 않고 있습니다. 강의 업로드는 성공했지만 인덱싱만 실패한 경우 사용자가 이를 인지하지 못할 수 있습니다.
예외를 명시적으로 처리하거나 호출자에게 전파하도록 수정:
- public void lectureIndex() {
+ public void lectureIndex() throws ElasticsearchException {
if (!indexerEnabled) {
log.info("ElasticSearch indexer 사용할 수 없습니다.");
return;
}
- manageIndex();
- long totalProcessed = performIndexing();
-
- log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed);
+ try {
+ manageIndex();
+ long totalProcessed = performIndexing();
+ log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed);
+ } catch (Exception e) {
+ log.error("ElasticSearch 인덱싱 실패: {}", e.getMessage(), e);
+ throw new ElasticsearchException("인덱싱 실패", 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.
| public void lectureIndex() { | |
| if (!indexerEnabled) { | |
| log.info("ElasticSearch indexer 사용할 수 없습니다."); | |
| return; | |
| } | |
| // 인덱스 존재 여부 확인 및 생성 | |
| manageIndex(); | |
| long totalProcessed = performIndexing(); | |
| log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed); | |
| } | |
| public void lectureIndex() throws ElasticsearchException { | |
| if (!indexerEnabled) { | |
| log.info("ElasticSearch indexer 사용할 수 없습니다."); | |
| return; | |
| } | |
| try { | |
| manageIndex(); | |
| long totalProcessed = performIndexing(); | |
| log.info("ElasticSearch 인덱싱 성공. Total Indexed: {}", totalProcessed); | |
| } catch (Exception e) { | |
| log.error("ElasticSearch 인덱싱 실패: {}", e.getMessage(), e); | |
| throw new ElasticsearchException("인덱싱 실패", e); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java
around lines 34 to 45, lectureIndex() swallows any exceptions from
manageIndex()/performIndexing() and will not notify the caller when
Elasticsearch is unavailable; change the method to either declare and rethrow a
checked exception (or a custom domain-specific exception) or rethrow the caught
runtime exception after logging so the caller (LectureUploadService) can react;
implement a try/catch that logs the error with details and then throws a
meaningful exception (or add throws to the method signature) so failures are
propagated, and keep the indexerEnabled early-return but ensure any unexpected
exceptions do not get swallowed.
| private void manageIndex() { | ||
| IndexOperations indexOps = elasticsearchOperations.indexOps(LectureDocument.class); | ||
| if (!indexOps.exists()) { | ||
| log.info("ElasticSearch lectures 인덱스가 존재하지 않아 직접 생성합니다."); | ||
| indexOps.create(); | ||
| indexOps.putMapping(indexOps.createMapping(LectureDocument.class)); | ||
| log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료."); | ||
| if (indexOps.exists()) { | ||
| log.info("ElasticSearch lectures 문서 전체 재색인을 위해 인덱스를 삭제합니다."); | ||
| indexOps.delete(); | ||
| } | ||
|
|
||
| long existingCount = lectureElasticRepository.count(); | ||
| if (existingCount > 0) { | ||
| log.info("ElasticSearch 인덱스가 이미 존재합니다 ({}개), 전체 재인덱싱을 진행합니다.", existingCount); | ||
| // 선택적으로 기존 인덱스 삭제 후 재인덱싱 | ||
| lectureElasticRepository.deleteAll(); | ||
| log.info("기존 인덱스를 삭제했습니다."); | ||
| } | ||
| log.info("ElasticSearch lectures 인덱스를 생성합니다."); | ||
| indexOps.create(); | ||
| indexOps.putMapping(indexOps.createMapping(LectureDocument.class)); | ||
| log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료."); | ||
| } |
There was a problem hiding this comment.
기존 데이터를 모두 삭제하는 위험한 동작
manageIndex() 메서드가 무조건 기존 인덱스를 삭제하고 재생성하는 구조입니다. 이는 강의 업로드 시마다 전체 검색 데이터가 초기화되어 서비스 중단이 발생할 수 있습니다.
인덱스 삭제 대신 증분 업데이트나 별칭(alias)를 활용한 무중단 재색인을 고려하세요:
private void manageIndex() {
IndexOperations indexOps = elasticsearchOperations.indexOps(LectureDocument.class);
- if (indexOps.exists()) {
- log.info("ElasticSearch lectures 문서 전체 재색인을 위해 인덱스를 삭제합니다.");
- indexOps.delete();
- }
-
- log.info("ElasticSearch lectures 인덱스를 생성합니다.");
- indexOps.create();
- indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
- log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
+ if (!indexOps.exists()) {
+ log.info("ElasticSearch lectures 인덱스를 생성합니다.");
+ indexOps.create();
+ indexOps.putMapping(indexOps.createMapping(LectureDocument.class));
+ log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료.");
+ } else {
+ log.info("기존 ElasticSearch lectures 인덱스를 사용합니다.");
+ }
}📝 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.
| private void manageIndex() { | |
| IndexOperations indexOps = elasticsearchOperations.indexOps(LectureDocument.class); | |
| if (!indexOps.exists()) { | |
| log.info("ElasticSearch lectures 인덱스가 존재하지 않아 직접 생성합니다."); | |
| indexOps.create(); | |
| indexOps.putMapping(indexOps.createMapping(LectureDocument.class)); | |
| log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료."); | |
| if (indexOps.exists()) { | |
| log.info("ElasticSearch lectures 문서 전체 재색인을 위해 인덱스를 삭제합니다."); | |
| indexOps.delete(); | |
| } | |
| long existingCount = lectureElasticRepository.count(); | |
| if (existingCount > 0) { | |
| log.info("ElasticSearch 인덱스가 이미 존재합니다 ({}개), 전체 재인덱싱을 진행합니다.", existingCount); | |
| // 선택적으로 기존 인덱스 삭제 후 재인덱싱 | |
| lectureElasticRepository.deleteAll(); | |
| log.info("기존 인덱스를 삭제했습니다."); | |
| } | |
| log.info("ElasticSearch lectures 인덱스를 생성합니다."); | |
| indexOps.create(); | |
| indexOps.putMapping(indexOps.createMapping(LectureDocument.class)); | |
| log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료."); | |
| } | |
| private void manageIndex() { | |
| IndexOperations indexOps = elasticsearchOperations.indexOps(LectureDocument.class); | |
| if (!indexOps.exists()) { | |
| log.info("ElasticSearch lectures 인덱스를 생성합니다."); | |
| indexOps.create(); | |
| indexOps.putMapping(indexOps.createMapping(LectureDocument.class)); | |
| log.info("ElasticSearch lectures 인덱스 및 매핑 생성 완료."); | |
| } else { | |
| log.info("기존 ElasticSearch lectures 인덱스를 사용합니다."); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/elasticsearch/indexer/LectureStartupIndexer.java
around lines 47 to 58, the manageIndex() method always deletes and recreates the
index which wipes existing data; change it to avoid unconditional index deletion
by: check if the index exists and if so only update mappings via
indexOps.putMapping(...) when compatible, otherwise implement a zero-downtime
reindex flow (create a new versioned index, index new/updated documents or call
Elasticsearch Reindex API, then switch an alias to the new index); if you need
to recreate because of incompatible mapping, create a new index name (e.g.,
append timestamp/version), populate it, then atomically swap the alias instead
of deleting the live index.
| log.info("[uploadNewSemesterRooms] {} 강의실 현황 업데이트 완료", file.getName()); | ||
| log.info("[uploadNewSemesterLectures] {} 학기 강의 정보 업로드 완료", file.getName()); | ||
|
|
||
| indexer.lectureIndex(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
인덱싱 실패 시 트랜잭션 롤백 고려 필요
강의 데이터는 DB에 저장되었지만 Elasticsearch 인덱싱이 실패할 경우, 데이터 불일치가 발생할 수 있습니다. 검색 결과와 실제 데이터가 다를 수 있습니다.
인덱싱 실패를 처리하거나 트랜잭션 범위를 확장하세요:
+ @Transactional
public void uploadNewSemesterLectures(MultipartFile file){
try {
saveFile(file);
int exitCode = executeGetExitCode(file, LECTURE_PROGRAM);
if (exitCode != 0) {
log.error("[uploadNewSemesterLectures] {} 업로드 실패", file.getOriginalFilename());
throw new LectureUploadException(LectureErrorCode.LECTURE_UPLOAD_FAIL, exitCode);
}
log.info("[uploadNewSemesterLectures] {} 학기 강의 정보 업로드 완료", file.getName());
- indexer.lectureIndex();
+ try {
+ indexer.lectureIndex();
+ } catch (Exception e) {
+ log.error("Elasticsearch 인덱싱 실패, 데이터 롤백 필요: {}", e.getMessage());
+ throw new LectureUploadException(LectureErrorCode.INDEXING_FAIL, "인덱싱 실패");
+ }
} catch (IOException | InterruptedException e) {Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java
around line 44, calling indexer.lectureIndex() outside of the DB transaction
risks DB/Elasticsearch inconsistency when indexing fails; modify the flow so
indexing is part of a coordinated operation: either (a) perform indexing within
the same transactional boundary by deferring commit until indexing succeeds
(e.g., wrap DB save + index in a single service method and only commit after
index success), or (b) if indexing must be async, implement reliable
compensation and retry (store pending index records or publish an event and
retry with dead-letter handling) and ensure failures roll back the DB save or
mark the entity as not-indexed for later reconciliation; also add
logging/metrics for indexing failures and test the failure path to confirm
consistency.
Summary by CodeRabbit
버그 수정
신규 기능
개선
리팩터링