[chore] 운영서버 배포 25/08/13#209
Conversation
- 검색 완료일 경우에만 키워드가 최근 검색어로 저장되도록 수정
…t-in-all-search-api [hotfix] 검색 관련 api 의 request 에 검색중/검색 완료를 구분하는 flag 추가
Walkthrough검색 API들(책/방/사용자)에 boolean 파라미터 isFinalized가 추가되었고, 이를 서비스 계층까지 전달하도록 모든 관련 컨트롤러/포트/서비스 시그니처가 확장되었습니다. RecentSearchCreateManager는 isFinalized가 true일 때만 최근검색을 저장하도록 변경되었으며, 해당 호출부와 테스트가 일괄 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant C as Client
participant WC as Controller
participant S as SearchService
participant Ext as ExternalProvider
participant RSM as RecentSearchCreateManager
participant Repo as RecentSearchRepo
C->>WC: GET /search?keyword=...&isFinalized=...
WC->>S: search(..., isFinalized, userId)
S->>Ext: fetch results
Ext-->>S: results
alt isFinalized == true
S->>RSM: saveRecentSearchByUser(userId, keyword, type, true)
RSM->>Repo: find/upsert
Repo-->>RSM: ok
RSM-->>S: done
else isFinalized == false
S-->>S: skip persistence
end
S-->>WC: results
WC-->>C: response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 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 (
|
Test Results374 tests 374 ✅ 30s ⏱️ Results for commit 076b6a9. |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (6)
src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java (1)
17-26: 동시성 중복 저장 가능성 — DB 유니크 인덱스 + upsert 패턴 고려현재 find → save는 경쟁 상태에서 중복 레코드 생성 위험이 있습니다. (userId, type, keyword) 유니크 인덱스를 두고, 저장 시 중복 키 예외를 touch로 전환하거나 DB upsert를 제공하는 커맨드 포트를 도입하는 것을 권장합니다.
src/main/java/konkuk/thip/user/application/port/in/dto/UserSearchQuery.java (1)
6-11: 이전 호출부 호환용 팩토리 오버로드 추가 제안혹시라도 3-인자 팩토리 호출이 잔존한다면, 기본값(false) 오버로드를 추가해 이행 비용을 줄일 수 있습니다.
아래 메서드를 추가해 주세요(파일 내 적절한 위치에):
public static UserSearchQuery of(String keyword, Long userId, Integer size) { return of(keyword, userId, size, false); }또한 isFinalized의 의미(“사용자가 검색을 확정했음을 나타냄; true일 때만 최근검색 저장”)를 Javadoc으로 명시하면 가독성이 좋아집니다.
src/main/java/konkuk/thip/book/application/service/BookSearchService.java (2)
50-50: isFinalized 사용 패턴에 맞춘 트랜잭션 최적화 제안실사용에서 isFinalized=false 비율이 높다면 매 요청마다 쓰기 트랜잭션을 여는 비용이 큽니다. 본 메서드는 readOnly=true로 두고, 최근검색 저장 메서드에 REQUIRES_NEW를 부여하면:
- 불필요한 flush/dirty checking 회피
- DB 락/로그 부하 감소
- 최근검색 저장 실패가 본 검색 트랜잭션을 오염시키는 것 방지
적용 방향:
- 이 메서드: @transactional(readOnly = true)
- RecentSearchCreateManager.saveRecentSearchByUser: @transactional(propagation = REQUIRES_NEW)
참고 스니펫(외부 적용):
// BookSearchService @Transactional(readOnly = true) public NaverBookParseResult searchBooks(...) { ... } // RecentSearchCreateManager @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveRecentSearchByUser(...) { ... }
70-72: 저장 전 경량 가드 및 키워드 정규화 적용isFinalized=false에서도 매니저 호출이 일어나므로, 메서드 수준에서 가드하여 호출 자체를 생략하면 미세하지만 경로 상 비용을 줄일 수 있습니다. 또한 저장 시 trim 적용을 권장합니다.
- // 최근 검색어 저장 - recentSearchCreateManager.saveRecentSearchByUser(userId, keyword, BOOK_SEARCH, isFinalized); + // 최근 검색어 저장 (finalized일 때만 호출 + 간단 정규화) + if (isFinalized) { + recentSearchCreateManager.saveRecentSearchByUser(userId, keyword.trim(), BOOK_SEARCH, true); + }src/main/java/konkuk/thip/room/application/port/in/RoomSearchUseCase.java (1)
7-7: 파라미터 순서 일관성 확보 제안BookSearchUseCase는 (keyword, page, userId, isFinalized) 순서인데, 여기서는 (… page, isFinalized, userId) 입니다. 모듈 간 순서를 맞추면 호출부 가독성/오류 방지에 유리합니다. 또는 공통 DTO(예: SearchMeta { Long userId; boolean isFinalized; })로 인자 폭주와 순서 이슈를 근본적으로 해소하는 방법을 권장합니다.
정렬 예시:
- RoomSearchResponse searchRoom(String keyword, String category, String sort, int page, boolean isFinalized, Long userId); + RoomSearchResponse searchRoom(String keyword, String category, String sort, int page, Long userId, boolean isFinalized);src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (1)
36-36: 운영 배포 전 문서 문구 정리 필요 ("아직 수정중" 제거).프로덕션 배포(PR 제목 상)에서는 "아직 수정중" 문구가 혼선을 유발합니다. isFinalized 동작을 포함해 최종 스펙으로 설명을 명확히 해주세요.
다음과 같이 갱신을 제안합니다:
- description = "키워드, 카테고리, 정렬 방식, 페이지 번호를 기준으로 방을 검색합니다. 아직 수정중입니다. 일단 request param 만 수정한 상태입니다." + description = "키워드, 카테고리, 정렬 방식, 페이지 번호(1-base), isFinalized(입력 확정 여부)를 기준으로 방을 검색합니다. isFinalized=true일 때만 최근 검색어로 저장됩니다."
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java(1 hunks)src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java(1 hunks)src/main/java/konkuk/thip/book/application/service/BookSearchService.java(3 hunks)src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java(1 hunks)src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java(1 hunks)src/main/java/konkuk/thip/room/application/port/in/RoomSearchUseCase.java(1 hunks)src/main/java/konkuk/thip/room/application/service/RoomSearchService.java(3 hunks)src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java(1 hunks)src/main/java/konkuk/thip/user/application/port/in/dto/UserSearchQuery.java(1 hunks)src/main/java/konkuk/thip/user/application/service/UserSearchService.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java(5 hunks)src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java(5 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java(2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
⏰ 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 (12)
src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java (2)
17-26: isFinalized 게이트 추가로 불필요한 DB I/O 차단 — 좋습니다검색 완료 시점에만 최근검색어를 저장/갱신하도록 한 판단이 타당합니다. 상위 계층에서 탐색 중간 단계의 호출이 잦을 수 있는 점을 고려하면, 비용 절감 효과가 큽니다.
17-26: 트랜잭션 경계 확인 요청touch/save는 동일 트랜잭션 내에서 이루어져야 일관성이 보장됩니다. 본 매니저 또는 호출 서비스 단(예: Book/User/RoomSearchService)에 @transactional이 적용되어 있는지 확인해 주세요. 없다면 메서드 수준에 readOnly=false로 부여하는 것을 권장합니다.
필요 시 아래와 같이 클래스/메서드에 부여 가능합니다:
import org.springframework.transaction.annotation.Transactional; @Transactional public class RecentSearchCreateManager { @Transactional public void saveRecentSearchByUser(...) { ... } }src/main/java/konkuk/thip/user/application/port/in/dto/UserSearchQuery.java (1)
6-11: isFinalized 필드 추가 및 팩토리 갱신 — 일관성 있게 반영됨레코드 시그니처와 정적 팩토리가 함께 확장되어 호출부 혼동을 줄입니다.
src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java (1)
97-97: isFinalized 파라미터를 모든 경로에 전달 — 테스트 일관성 확보 좋습니다엔드포인트 시그니처 변경에 따라 모든 호출에서 isFinalized를 명시적으로 전달하도록 수정되어 회귀를 방지합니다.
Also applies to: 119-119, 141-141, 161-161
src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java (1)
75-75: isFinalized=true 전달 및 저장 검증 — 케이스 적합사용자 검색에서도 저장 플로우가 끝단까지 작동하는지 잘 검증되어 있습니다.
src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java (1)
187-187: isFinalized 파라미터 도입에 맞춘 테스트 보완 — 일관성 유지 OK검색 통합 테스트들에서 isFinalized=false를 명시하여 API 시그니처 변경에 따른 오작동을 방지하고 있습니다.
Also applies to: 237-237, 291-291, 331-331, 373-373
src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java (1)
8-9: 인터페이스 시그니처 반영 확인 완료searchBooks 호출부(BookQueryController)와 구현체(BookSearchService)가 모두 새 시그니처(4개 인자)를 사용하도록 업데이트된 것을 확인했습니다. 추가 조치 불필요합니다.
src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1)
46-47: 최근검색 저장 로직 중앙화: 좋습니다RecentSearchCreateManager로 위임하여 중복 로직 제거 및 정책 일관성 확보에 도움이 됩니다.
src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (1)
47-47: 호출 시그니처 정합성 확인됨컨트롤러 → 유스케이스 인자 순서(keyword, page, userId, isFinalized)가 인터페이스와 일치합니다.
src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (2)
48-48: UseCase 인자 전달 순서 일치 확인됨.
searchRoom(keyword, category, sort, page, isFinalized, userId)로 신규 시그니처에 맞게 정확히 전달되고 있습니다.
44-46: isFinalized 쿼리 파라미터에 기본값(false) 및 required=false 지정으로 호환성 보장
신규 boolean 쿼리 파라미터를 필수로 변경하면, 기존 클라이언트(앱/웹, 배치, 문서화된 예제 등)에서 파라미터 없이 호출 시 400 에러가 발생할 수 있습니다. 다음을 적용해주세요:• 주요 변경 사항
src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java에서- @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") - @RequestParam(name = "isFinalized") final boolean isFinalized, + @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") + @RequestParam(name = "isFinalized", required = false, defaultValue = "false") final boolean isFinalized,• 추가 검증
RoomSearchApiTest등 통합 테스트에서isFinalized없이 호출되는 케이스가 없는지 확인- Swagger/OpenAPI 문서에도 defaultValue 및 required=false 반영
src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (1)
126-126: 신규 4-인자 UserSearchQuery.of(...) 호출 시그니처 일치.
(keyword, userId, size, isFinalized)순서로 올바르게 전달되고 있습니다.
| public BaseResponse<BookSearchListResponse> showBookSearchList( | ||
| @Parameter(description = "검색 키워드", example = "해리포터") @RequestParam final String keyword, | ||
| @Parameter(description = "페이지 번호 (1부터 시작)", example = "1") @RequestParam final int page, | ||
| @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized, |
There was a problem hiding this comment.
브레이킹 체인지: 신규 필수 쿼리 파라미터로 기존 클라이언트 400 가능
isFinalized를 필수로 추가하면 구버전 앱/클라이언트가 400을 받습니다. 기본값 false로 하위호환을 유지하는 방식을 권장합니다.
- @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized,
+ @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false")
+ @RequestParam(name = "isFinalized", required = false, defaultValue = "false") final boolean isFinalized,가능하면 테스트에 “isFinalized 미전달 시 200 OK 및 기본 false 동작” 케이스 추가도 부탁드립니다.
📝 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.
| @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized, | |
| @Parameter( | |
| description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", | |
| example = "false" | |
| ) | |
| @RequestParam(name = "isFinalized", required = false, defaultValue = "false") | |
| final boolean isFinalized, |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java around
line 45, adding a required isFinalized request parameter is a breaking change;
make it optional with a default value of false to preserve backward
compatibility by updating the @RequestParam to required = false and supplying a
default value (or use Boolean and coalesce to false), and add a unit/integration
test verifying that when isFinalized is not provided the endpoint returns 200 OK
and behaves as if isFinalized == false.
| public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) { | ||
| // 검색완료일 경우에 최근검색어 추가 | ||
| if (isFinalized) { | ||
| // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인 | ||
| recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId, type) | ||
| .ifPresentOrElse( | ||
| recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신 | ||
| () -> recentSearchCommandPort.save(RecentSearch.withoutId(keyword, type, userId)) // 없으면 새로 저장 | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
빈/공백 키워드 저장 방지 및 trim 필요
현재는 isFinalized=true인 경우 키워드가 null/공백이어도 저장을 시도할 수 있습니다(특히 방/사용자 검색처럼 keyword가 Optional한 경우). 저장 데이터 품질을 위해 공백 트리밍 및 빈 문자열 차단을 권장합니다.
아래와 같이 정규화 후 빈 문자열을 early-return 하도록 보완해 주세요.
- public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) {
+ public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) {
// 검색완료일 경우에 최근검색어 추가
if (isFinalized) {
+ // 키워드 정규화 및 빈 문자열 저장 방지
+ final String normalizedKeyword = (keyword == null) ? "" : keyword.trim();
+ if (normalizedKeyword.isEmpty()) {
+ return;
+ }
// 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인
- recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId, type)
+ recentSearchQueryPort.findRecentSearchByKeywordAndUserId(normalizedKeyword, userId, type)
.ifPresentOrElse(
recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신
- () -> recentSearchCommandPort.save(RecentSearch.withoutId(keyword, type, userId)) // 없으면 새로 저장
+ () -> recentSearchCommandPort.save(RecentSearch.withoutId(normalizedKeyword, type, userId)) // 없으면 새로 저장
);
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) { | |
| // 검색완료일 경우에 최근검색어 추가 | |
| if (isFinalized) { | |
| // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인 | |
| recentSearchQueryPort.findRecentSearchByKeywordAndUserId(keyword, userId, type) | |
| .ifPresentOrElse( | |
| recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신 | |
| () -> recentSearchCommandPort.save(RecentSearch.withoutId(keyword, type, userId)) // 없으면 새로 저장 | |
| ); | |
| } | |
| public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) { | |
| // 검색완료일 경우에 최근검색어 추가 | |
| if (isFinalized) { | |
| // 키워드 정규화 및 빈 문자열 저장 방지 | |
| final String normalizedKeyword = (keyword == null) ? "" : keyword.trim(); | |
| if (normalizedKeyword.isEmpty()) { | |
| return; | |
| } | |
| // 동일 조건 (userId + keyword + type) 검색 기록이 이미 있는지 확인 | |
| recentSearchQueryPort.findRecentSearchByKeywordAndUserId(normalizedKeyword, userId, type) | |
| .ifPresentOrElse( | |
| recentSearchCommandPort::touch, // 있으면 modifiedAt 갱신 | |
| () -> recentSearchCommandPort.save( | |
| RecentSearch.withoutId(normalizedKeyword, type, userId) | |
| ) // 없으면 새로 저장 | |
| ); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java
around lines 17 to 26, the method may attempt to save null or whitespace-only
keywords; normalize the input by trimming the keyword and, if the trimmed result
is null or empty, return early (do not proceed to query/save). Apply the trim
before any Optional handling, use the trimmed value for subsequent lookups and
saves, and ensure you avoid saving empty strings while preserving behavior when
keyword is legitimately optional.
| import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; | ||
| import konkuk.thip.recentSearch.application.service.manager.RecentSearchCreateManager; | ||
| import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
애플리케이션 계층이 adapter 패키지에 의존: 계층 침식(Architecture Smell).
RecentSearchType이 adapter.out.jpa에 위치해 애플리케이션 서비스가 어댑터 계층에 의존하고 있습니다. 헥사고날/클린 아키텍처 원칙상, 상위 계층(애플리케이션)은 하위 계층(어댑터)에 의존하지 않아야 합니다.
권장:
RecentSearchType를 domain 혹은 application-common으로 승격(예:konkuk.thip.recentSearch.model.RecentSearchType).- JPA 어댑터에서는 도메인 타입과 영속화 표현을 매핑.
추후 변경 예시(요약):
- 도메인:
konkuk.thip.recentSearch.domain.RecentSearchType - 어댑터:
@Enumerated필드에서 도메인 enum을 직접 사용하거나, 변환기를 통해 매핑.
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/room/application/service/RoomSearchService.java
around lines 4 to 6, the service imports RecentSearchType from the adapter layer
which violates layered architecture; to fix, move the RecentSearchType enum out
of adapter.out.jpa into a domain or application-common package (e.g.,
konkuk.thip.recentSearch.domain or model), update all references/imports in
application services to the new package, and change the JPA adapter to map
between the domain enum and its persistence representation (either by using the
domain enum directly with @Enumerated or by adding a converter/DTO mapping in
the adapter).
| @Transactional // <- 최근 검색 저장으로 인한 트랜잭션 | ||
| public RoomSearchResponse searchRoom(String keyword, String category, String sort, int page, boolean isFinalized, Long userId) { | ||
| // 1. validation |
There was a problem hiding this comment.
🛠️ Refactor suggestion
검색 서비스 전체 트랜잭션을 write 가능으로 전환 → 성능/리소스 회귀 가능성.
검색(read)과 최근검색 저장(write)을 동일 트랜잭션으로 묶으면, 읽기 전용 최적화(리드 레플리카 라우팅, 플러시 최적화 등)를 잃고 DB 리소스 사용이 증가할 수 있습니다. 사용자의 선호(성능 최적화에 대한 근거) 관점에서도 분리 권장합니다.
옵션 A(간단, 선호): 서비스 메서드를 다시 readOnly로 유지하고, Manager 쪽에 REQUIRES_NEW로 별도 트랜잭션을 사용
- @Transactional // <- 최근 검색 저장으로 인한 트랜잭션
+ @Transactional(readOnly = true)
public RoomSearchResponse searchRoom(String keyword, String category, String sort, int page, boolean isFinalized, Long userId) {Manager 메서드(별도 파일) 예시:
// RecentSearchCreateManager.java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) {
...
}옵션 B(이벤트 분리): 검색은 readOnly 트랜잭션에서 처리하고, ApplicationEventPublisher로 이벤트를 발행하여 비동기/분리 트랜잭션으로 저장.
| // TODO 검색 완료일 경우, 최근 검색어로 저장되도록 | ||
| recentSearchCreateManager.saveRecentSearchByUser(userId, keyword, RecentSearchType.ROOM_SEARCH, isFinalized); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
불필요한 TODO와 불필요 저장 방지 가드 추가 제안(공백/빈 키워드).
이미 구현되었으므로 TODO는 제거하고, 공백 키워드 저장 방지로 I/O 감소 및 인덱스 오염 방지(성능)를 권장합니다. 사용자 선호(구체적 성능 근거)에 따라 실제 저장 건수/인덱스 부하를 줄일 수 있습니다.
다음과 같이 가드 추가를 제안합니다:
- // TODO 검색 완료일 경우, 최근 검색어로 저장되도록
- recentSearchCreateManager.saveRecentSearchByUser(userId, keyword, RecentSearchType.ROOM_SEARCH, isFinalized);
+ if (isFinalized && org.springframework.util.StringUtils.hasText(keyword)) {
+ recentSearchCreateManager.saveRecentSearchByUser(userId, keyword.trim(), RecentSearchType.ROOM_SEARCH, isFinalized);
+ }추가(해당 파일 상단 import 보강 필요):
import org.springframework.util.StringUtils;🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/room/application/service/RoomSearchService.java
around lines 45-47, remove the stale TODO comment and add a guard to avoid
saving empty or whitespace-only keywords: before calling
recentSearchCreateManager.saveRecentSearchByUser(...) check
StringUtils.hasText(keyword) (or equivalent) and only call save when true; also
add the import org.springframework.util.StringUtils at the top of the file.
Ensure the guard prevents null/blank saves to reduce I/O and index noise.
| @GetMapping("/users") | ||
| public BaseResponse<UserSearchResponse> showSearchUsers( | ||
| @Parameter(description = "검색어", example = "thip") @RequestParam @NotBlank(message = "검색어는 필수입니다.") final String keyword, | ||
| @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized, |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
isFinalized 필수화로 인한 API 호환성 이슈 가능. 기본값(false) 적용 권장.
기존 클라이언트는 isFinalized를 전송하지 않았을 가능성이 높습니다. 기본값을 false로 지정해 400 방지를 권장합니다. (스웨거에도 default 기재)
- @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized,
+ @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false")
+ @RequestParam(name = "isFinalized", required = false, defaultValue = "false") final boolean isFinalized,검증 스크립트:
🏁 Script executed:
#!/bin/bash
# /users 검색 API를 테스트/문서 등에서 isFinalized 없이 호출하는 흔적이 있는지 점검
rg -n --pretty $'/users(\\?|")(?![^\\n"]*isFinalized)' -S -g '!node_modules' -g '!build' -g '!dist' -g '!out' || trueLength of output: 365
추가 검증: 테스트 코드 및 Swagger/YAML 문서에서 isFinalized 없이 /users 호출 여부 확인
🏁 Script executed:
#!/bin/bash
# 테스트 코드에서 isFinalized 없이 /users 엔드포인트 호출 검증
grep -R -nP '/users(\?|")' --exclude-dir={node_modules,build,dist,out} src/test/java | grep -v 'isFinalized' || true
# Swagger/YAML 문서에서 isFinalized 파라미터 언급 여부 검증
grep -R -n '/users' --include='*.yaml' --exclude-dir={node_modules,build,dist,out} | grep -v 'isFinalized' || trueLength of output: 1486
isFinalized 기본값 지정 추가 권장
- UserSearchApiTest에서
GET("/users")만 호출하여isFinalized없이 요청하는 테스트가 존재함 @RequestParam에required=false,defaultValue="false"옵션을 추가하면 기존 클라이언트의 호환성(400 에러 방지)이 보장됨- OpenAPI(@Parameter/@Schema)에도 기본값
false를 명시해 주세요
아래와 같이 수정을 제안합니다:
@Parameter(
description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)",
example = "false",
+ schema = @Schema(defaultValue = "false")
)
-@RequestParam(name = "isFinalized") final boolean isFinalized,
+@RequestParam(name = "isFinalized", required = false, defaultValue = "false") final boolean isFinalized,- 수정 파일:
- src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (약 123번째 라인)
- 문서 반영:
- Java 어노테이션(@Schema) 또는 Swagger/YAML 내
default: false추가 필요
- Java 어노테이션(@Schema) 또는 Swagger/YAML 내
📝 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.
| @Parameter(description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", example = "false") @RequestParam(name = "isFinalized") final boolean isFinalized, | |
| @Parameter( | |
| description = "사용자가 검색어 입력을 '확정'했는지 여부 (입력 중: false, 입력 확정: true)", | |
| example = "false", | |
| schema = @Schema(defaultValue = "false") | |
| ) | |
| @RequestParam(name = "isFinalized", required = false, defaultValue = "false") final boolean isFinalized, |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java around
line 123, the @RequestParam for isFinalized is currently required which breaks
tests/clients that omit it; change the annotation to make the parameter optional
by adding required=false and defaultValue="false", and update the OpenAPI
annotations (e.g., @Parameter and/or @Schema) to declare a default value of
false so generated docs reflect the default; ensure the method signature still
accepts a boolean (or use Boolean if necessary) and adjust any downstream logic
to rely on the default false when not provided.
| recentSearchCreateManager.saveRecentSearchByUser(userSearchQuery.userId(), userSearchQuery.keyword(), RecentSearchType.USER_SEARCH, userSearchQuery.isFinalized()); | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
검색 정규화 불일치: 저장 시에도 검색과 동일한 정규화 적용 필요
현재 검색은 keyword.toLowerCase()로 수행하지만, 최근검색 저장은 원문을 그대로 저장합니다. 케이스/공백 차이로 중복 저장 및 정렬/집계 불일치가 생길 수 있습니다. 저장 시 trim + lower-case(가능하면 Locale 고정)로 정규화하거나, 정규화를 RecentSearchCreateManager에서 일괄 처리하도록 합의해 주세요.
적용 예시(간단 정규화 처리):
- recentSearchCreateManager.saveRecentSearchByUser(userSearchQuery.userId(), userSearchQuery.keyword(), RecentSearchType.USER_SEARCH, userSearchQuery.isFinalized());
+ recentSearchCreateManager.saveRecentSearchByUser(
+ userSearchQuery.userId(),
+ userSearchQuery.keyword().trim().toLowerCase(),
+ RecentSearchType.USER_SEARCH,
+ userSearchQuery.isFinalized()
+ );정규화가 매니저에서 이미 처리되는지 확인하는 스크립트:
🏁 Script executed:
#!/bin/bash
rg -n "saveRecentSearchByUser\(" -A 40 | sed -n '1,200p'Length of output: 11963
검색 키워드 정규화 누락 (trim + lowercase) 추가 필요
RecentSearchCreateManager에서 전달된 원본 키워드를 그대로 사용하고 있어, 검색 시에는 toLowerCase()로 비교하지만 저장 시엔 정규화를 하지 않습니다. 이로 인해 케이스 및 공백 차이로 중복 저장, 정렬·집계 불일치가 발생할 수 있습니다. 확인 결과 매니저 내부에는 별도의 정규화 로직이 없으므로, 서비스 또는 매니저 단계에서 다음과 같이 trim().toLowerCase(Locale.ROOT) 형태의 정규화를 반드시 적용해야 합니다.
수정 대상 위치:
- src/main/java/konkuk/thip/user/application/service/UserSearchService.java: 라인 32
- src/main/java/konkuk/thip/room/application/service/RoomSearchService.java: 라인 46
- src/main/java/konkuk/thip/book/application/service/BookSearchService.java: 라인 71
예시 (UserSearchService.java):
- recentSearchCreateManager.saveRecentSearchByUser(
- userSearchQuery.userId(),
- userSearchQuery.keyword(),
- RecentSearchType.USER_SEARCH,
- userSearchQuery.isFinalized()
- );
+ recentSearchCreateManager.saveRecentSearchByUser(
+ userSearchQuery.userId(),
+ userSearchQuery.keyword()
+ .trim()
+ .toLowerCase(Locale.ROOT),
+ RecentSearchType.USER_SEARCH,
+ userSearchQuery.isFinalized()
+ );또는 RecentSearchCreateManager 내부에 별도 private String normalize(String keyword) 메서드를 두어 일관 처리하는 방안을 검토해주세요.
Committable suggestion skipped: line range outside the PR's diff.
| .header("Authorization", "Bearer " + testToken) | ||
| .param("keyword", keyword) | ||
| .param("page", String.valueOf(page)) | ||
| .param("isFinalized", String.valueOf(true)) // 검색 완료일 경우 : 최근검색어로 저장된다 |
There was a problem hiding this comment.
🛠️ Refactor suggestion
isFinalized=true 시 저장 검증 OK — false 시 미저장 검증 테스트도 추가해 주세요
완료 상태 저장은 검증되었으나, 미완료(false)일 때 저장되지 않아야 함을 보장하는 역테스트가 없습니다. 간단한 케이스를 추가해 회귀를 막는 것을 권장합니다.
예시(새 테스트 메서드 추가):
@Test
@DisplayName("책 검색 성공 시 isFinalized=false이면 최근검색어가 저장되지 않는다")
void searchBooks_doesNotSaveRecentSearch_whenNotFinalized() throws Exception {
// given
String keyword = "테스트";
int page = 1;
// when
mockMvc.perform(get("/books")
.header("Authorization", "Bearer " + testToken)
.param("keyword", keyword)
.param("page", String.valueOf(page))
.param("isFinalized", String.valueOf(false)))
.andExpect(status().isOk());
// then
UserJpaEntity user = userJpaRepository.findAll().get(0);
boolean exists = recentSearchJpaRepository.findAll().stream()
.anyMatch(rs -> rs.getUserJpaEntity().getUserId().equals(user.getUserId())
&& rs.getSearchTerm().equals(keyword)
&& rs.getType() == RecentSearchType.BOOK_SEARCH);
assertThat(exists).isFalse();
}🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java
around line 182, add a new test method that verifies when isFinalized=false the
recent search is NOT persisted: create a request performing GET /books with
Authorization header, keyword, page and param("isFinalized",
String.valueOf(false)), assert status is OK, then query repositories to ensure
no RecentSearchJpaEntity exists for the test user with that keyword and
RecentSearchType.BOOK_SEARCH (assert false). Keep setup consistent with existing
tests (use testToken and existing repositories) and name the test/DisplayName to
indicate the negative case.
| .requestAttr("userId", 1L) | ||
| .param("keyword", "과학") | ||
| .param("sort", "deadline") | ||
| .param("isFinalized", String.valueOf(false)) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
isFinalized=true 저장 경로에 대한 통합 검증 추가 및 userId 주의
현재 모든 케이스가 false라 저장 비발생만 검증됩니다. 최소 1개 케이스에서 isFinalized=true로 설정 후 최근검색어가 저장되는지, 그리고 requestAttr("userId", 1L)가 실제로 존재하는 사용자와 매핑되는지 확인하는 테스트를 권장합니다. 존재하지 않는 사용자 ID로 저장을 시도하면 FK 제약에 의해 실패할 수 있습니다.
예시(요지):
@Test
@DisplayName("방 검색 isFinalized=true이면 최근검색어가 저장된다")
void searchRooms_savesRecentSearch_whenFinalized() throws Exception {
// 사전에 사용자 생성 후 그 ID를 requestAttr로 사용
AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias());
UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(alias, "searcher"));
mockMvc.perform(get("/rooms/search")
.requestAttr("userId", user.getUserId())
.param("keyword", "과학")
.param("sort", "deadline")
.param("isFinalized", String.valueOf(true))
.param("page", "1"))
.andExpect(status().isOk());
boolean saved = recentSearchJpaRepository.findAll().stream()
.anyMatch(rs -> rs.getUserJpaEntity().getUserId().equals(user.getUserId())
&& rs.getSearchTerm().equals("과학")
&& rs.getType() == RecentSearchType.ROOM_SEARCH);
assertThat(saved).isTrue();
}Also applies to: 237-237, 291-291, 331-331, 373-373
🏁 Script executed:
#!/bin/bash
# isFinalized 파라미터 사용 현황 확인
rg '\.param\("isFinalized"' -n src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java
# requestAttr("userId") 사용 현황 확인
rg 'requestAttr\("userId"' -n src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.javaLength of output: 762
RoomSearchApiTest에 isFinalized=true 케이스 및 실제 사용자 매핑 검증 추가 필요
현재 5개 테스트 모두 .param("isFinalized", "false")만 검증하고, requestAttr("userId", 1L)에 대응되는 실제 사용자 생성 로직이 없습니다. 최소 하나의 케이스에서 아래를 반영해주세요:
- 실제 JPA 리포지토리로
AliasJpaEntity·UserJpaEntity를 저장하고,
해당user.getUserId()를requestAttr("userId", …)에 사용 .param("isFinalized", String.valueOf(true))인 경우 recentSearch가 저장되는지 검증- false 케이스는 기존처럼 저장되지 않음을 검증
수정 대상 예시 위치 (파일: src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java)
- 184–187, 234–237, 287–290, 328–330, 369–373 중 최소 1곳
예시 코드:
@Test
@DisplayName("방 검색 시 isFinalized=true이면 최근검색어가 저장된다")
void searchRooms_savesRecentSearch_whenFinalized() throws Exception {
AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias());
UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(alias, "searcher"));
mockMvc.perform(get("/rooms/search")
.requestAttr("userId", user.getUserId())
.param("keyword", "과학")
.param("sort", "deadline")
.param("isFinalized", String.valueOf(true))
.param("page", "1"))
.andExpect(status().isOk());
boolean saved = recentSearchJpaRepository.findAll().stream()
.anyMatch(rs ->
rs.getUserJpaEntity().getUserId().equals(user.getUserId()) &&
rs.getSearchTerm().equals("과학") &&
rs.getType() == RecentSearchType.ROOM_SEARCH
);
assertThat(saved).isTrue();
}이 수정으로 userId FK 제약이 걸린 실제 사용자와 매핑된 상태에서 isFinalized=true 저장 경로가 검증됩니다.
| .requestAttr("userId", currentUserId) | ||
| .param("size", "10") | ||
|
|
||
| .param("isFinalized", String.valueOf(true)) // 검색 완료 -> 최근 검색어 저장 |
There was a problem hiding this comment.
🛠️ Refactor suggestion
false 경로(미저장) 및 touch 시나리오도 보완 테스트 권장
- isFinalized=false일 때 저장되지 않아야 함을 검증하는 테스트를 추가해 주세요.
- 동일 keyword로 1회 저장 이후 재검색(finalized=true) 시 중복 생성 없이 modifiedAt만 갱신(touch)되는지 검증하면 매니저 로직을 더욱 견고히 할 수 있습니다.
예시(간략):
@Test
@DisplayName("사용자 검색 isFinalized=false이면 최근 검색어가 저장되지 않는다")
void searchUsers_notFinalized_notSaved() throws Exception {
mockMvc.perform(get("/users")
.param("keyword", "thip")
.requestAttr("userId", currentUserId)
.param("size", "10")
.param("isFinalized", String.valueOf(false)))
.andExpect(status().isOk());
assertEquals(0, recentSearchJpaRepository.count());
}🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/user/adapter/in/web/UserSearchApiTest.java around
line 75, add two tests: 1) verify when isFinalized=false the recent-search is
not persisted — perform the GET with isFinalized=false and assert repository
count remains 0 (or unchanged) after the request; 2) verify touch behavior —
first perform a finalized=true search to create the entry, record its
modifiedAt, wait or advance time (or stub clock), perform another finalized=true
search with same keyword, assert repository count stays 1 and that the existing
entry's modifiedAt was updated (greater than previous timestamp) instead of
inserting a duplicate. Use mockMvc for requests, recentSearchJpaRepository for
counts and fetching the record, and ensure any clock/mock time handling is set
so modifiedAt comparison is deterministic.
#️⃣ 연관된 이슈
📝 작업 내용
📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
New Features
API Changes