Skip to content

feat(#32): 도서 검색 opensearch 옵션 추가 - #34

Merged
fnzl54 merged 3 commits into
mainfrom
feat/#32
Aug 20, 2026
Merged

feat(#32): 도서 검색 opensearch 옵션 추가#34
fnzl54 merged 3 commits into
mainfrom
feat/#32

Conversation

@fnzl54

@fnzl54 fnzl54 commented Aug 20, 2026

Copy link
Copy Markdown
Member

연관 이슈

작업 사항

  • feat(#32): 도서 검색 opensearch 옵션 추가
    • OpenSearchBookSearchAdapter: nori 형태소 분석 + fuzziness 기반 multi_match 검색 추가
    • search.engine=opensearch로 활성화
  • feat(#32): 검색 성능 비교 (opensearch) devtools
    • ExplainAnalyzer: LIKE / FULLTEXT(MySQL) / OpenSearch 실측 응답시간 비교 추가
    • OpenSearchIndexGenerator: MySQL book 데이터를 OpenSearch books 인덱스로 색인하는 devtool 추가

테스트

  • 로컬 서버 테스트 완료
  • devtools ExplainAnalyzer 실행해 LIKE/FULLTEXT/OpenSearch 3-way 비교 확인 (관련 노션 - 4장)

주의 사항 및 참고사항

  • 해당 PR의 OpenSearch로 실행하려면 yml 파일 search.engine=opensearch 수정

@fnzl54 fnzl54 self-assigned this Aug 20, 2026

@Bean(destroyMethod = "close")
fun openSearchRestClient(): RestClient =
RestClient.builder(HttpHost(host, port, "http")).build()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH] core/config에 book 검색 전용 RestClient 빈이 배치됨

Problem: core/ 는 "도메인 지식 0, 거의 안 변함, 여러 모듈이 씀" 3가지를 모두 만족해야 하는 Shared Kernel입니다. OpenSearchConfig가 만드는 RestClient 빈은 전체 코드베이스에서 OpenSearchBookSearchAdapter 단 하나만 소비하는, book 검색 엔진 전용 설정이라 세 번째 조건("여러 모듈이 씀")을 만족하지 못합니다.

Evidence: core/config/ 에는 원래 전역적으로 쓰이는 JpaConfig, WebConfig만 있었는데, 이번 PR에서 단일 소비자용 설정이 함께 들어갔습니다. (git grep -n "RestClient" -- src/main 실행 결과 소비자가 OpenSearchBookSearchAdapter 하나뿐입니다.)

Fix direction: OpenSearchConfig를 core/config/ 가 아니라 어댑터와 같은 위치(예: adapter/book/search/opensearch/OpenSearchClientConfig.kt)로 옮기고, 지금과 동일한 ConditionalOnProperty 가드와 host/port 프로퍼티 주입 방식을 그대로 유지하면 됩니다. mysql_fulltext, mysql_like 어댑터가 별도 core 설정 없이 각자 위치에서 동작하는 것과 동일한 패턴입니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

RestClient는 도서 검색에 종속된 설정이 아니라 "OpenSearch 클러스터에 어떻게 붙을지"에 대한 인프라 설정으로 이후 다른 모듈(예: 소장본/대출 검색)이 OpenSearch를 붙이게 되면 재사용할 수 있어서, core/config에 두는 게 맞다고 판단

"track_total_hits": true,
"query": $queryClause
}
""".trimIndent()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH] OpenSearch 어댑터에 정렬이 전혀 없어 "최신 등록순" API 계약이 깨짐

Problem: BookController의 /books/search Swagger 설명(BookController.kt 68번째 줄)은 검색어 유무와 무관하게 결과를 "최신 등록순"(createdAt DESC)으로 반환한다고 명시합니다. MysqlLikeBookSearchAdapter, MysqlFullTextBookSearchAdapter는 둘 다 Sort.by(DESC, "createdAt")으로 이를 지킵니다. 그런데 이 OpenSearchBookSearchAdapter의 requestBody에는 sort 필드가 전혀 없어 match_all(검색어 없음)일 때는 색인 순서(사실상 무작위), 키워드 검색일 때는 relevance score 순으로 반환되어 문서화된 계약과 어긋납니다.

Evidence: 이 문제는 이미 한 번 sibling PR에서 [HIGH]로 지적되고 수정된 이력이 있습니다. PR #33(MysqlFullTextBookSearchAdapter)에서 동일한 "최신 등록순 계약 깨짐" 이슈가 지적되어 "searchFullText의 정렬기준을 created_at desc 수정"으로 고쳐졌는데, 이번 OpenSearch 구현에서 다시 재발했습니다. 더 근본적으로, opensearch/books_index.json 매핑(17~23번째 줄)에는 createdAt 필드 자체가 색인되어 있지 않고, OpenSearchIndexGenerator도 id/title/author/publisher/isbn만 색인하므로 지금 상태로는 sort 절을 추가해도 정렬할 필드가 아직 없습니다.

Fix direction: (1) books_index.json 매핑에 createdAt(date 타입) 필드 추가, (2) OpenSearchIndexGenerator의 조회 쿼리와 bulk 색인 payload에 created_at 값 포함, (3) OpenSearchBookSearchAdapter의 requestBody JSON에 정렬 절을 추가해 track_total_hits 옆에 다음을 넣으세요.

"sort": [ { "createdAt": "desc" } ]

키워드 검색까지 "최신 등록순"을 유지할지, relevance 순을 허용하도록 API 문서를 바꿀지는 팀 논의가 필요합니다.

관련 링크:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

수정 완료

초기 데이터 적재 시 OpenSearchIndexGenerator가 MySQL created_at 컬럼을 함께 조회,색인하도록 반영
OpenSearchBookSearchAdapter의 요청 본문에 "sort": [ { "createdAt": "desc" } ] 추가

emptyMap()
} else {
bookItemRepository.countActiveByBookIdIn(bookIds).associate { it.bookId to it.itemCount }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MEDIUM] OpenSearch 인덱스가 book 생성/수정/삭제와 동기화되지 않아 소프트 삭제된 책이 계속 검색됨

Problem: search.engine=opensearch로 활성화하면 이 어댑터가 실제 운영 트래픽에서 BookSearchPort 구현체로 선택됩니다. 그런데 OpenSearch 색인은 devtools의 OpenSearchIndexGenerator가 한 번 배치로 채울 뿐이고, CreateBookService/UpdateBookService/DeleteBookService 어디에도 색인 동기화 로직이 없습니다. 책을 소프트 삭제해도 OpenSearch 인덱스에는 그대로 남아 검색 결과에 계속 노출됩니다.

Evidence: MysqlLikeBookSearchAdapter, MysqlFullTextBookSearchAdapter는 모두 deletedAt is null 조건으로 삭제된 책을 걸러내며, 커밋 b605544("도서 삭제 시 소장본 cascade soft-delete 처리")가 이 불변식을 더 강화했습니다. 이 어댑터에는 그런 필터링 지점 자체가 없습니다(색인이 갱신되지 않으므로). 게다가 bookItemCounts는 countActiveByBookIdIn로 활성 소장본만 세기 때문에, 삭제된 책이 "소장본 0권인 정상 도서"처럼 보이는 부작용도 있습니다(75번째 줄).

Fix direction: 최소한 book 도메인 이벤트(생성/수정/삭제) 발생 시 OpenSearch 문서를 upsert/삭제하는 동기화 경로를 추가하거나, 이 어댑터가 아직 실 트래픽용이 아니라면 그 사실을 PR 설명이나 설정 문서에 명시하세요. 즉시 반영이 부담스럽다면 최소 조치로 검색 결과의 bookId를 bookRepository.findAllByIdInAndDeletedAtIsNull로 한 번 더 필터링해 삭제된 책만이라도 응답에서 제외하는 방법이 있습니다.

@fnzl54 fnzl54 Aug 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

후속 이슈로 분리해 작업 예정

@fnzl54
fnzl54 merged commit d50be5c into main Aug 20, 2026
@fnzl54
fnzl54 deleted the feat/#32 branch August 20, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant