Skip to content

feat(#22): Book 검색 API (FULL TEXT)#29

Open
fnzl54 wants to merge 2 commits into
mainfrom
feat/#22
Open

feat(#22): Book 검색 API (FULL TEXT)#29
fnzl54 wants to merge 2 commits into
mainfrom
feat/#22

Conversation

@fnzl54

@fnzl54 fnzl54 commented Jul 2, 2026

Copy link
Copy Markdown
Member

관련 이슈

작업 내용

  • Flyway 기반 DB 버전 관리 추가 (DB 변화 로깅용)
  • book(title, author) 컬럼에 ngram 파서 기반 FULLTEXT 인덱스 생성
  • BookQueryRepository 검색 로직 교체
    • LIKE(containsIgnoreCase) → FULLTEXT (match_against 커스텀 함수) 방식으로 전환
    • 정렬 기준 : 키워드 있음 → score 내림차순, 키워드 없음 → title 오름차순
  • 작업 관련 노션 (3. FULL TEXT INDEX (검색 성능 개선) & 테스트 결과)

테스트

  • 로컬 테스트 시 정상 조회 확인
  • nGrinder 부하 테스트 완료 (결과 상단 노션에서 확인 가능)

주의 사항

  • 본 PR은 에픽([EPIC] 도서 조회 API #20)의 FULL TEXT 기반 구현
  • 성능 저하 문제는 해결했으나 아래 한계 때문에 Elasticsearch 도입 예정
    • 오타 발생 시 관련 검색 결과 전혀 반환되지 않음
    • 특정 필드(title/author)에 가중치 차등 적용 불가

@fnzl54 fnzl54 self-assigned this Jul 2, 2026
@fnzl54 fnzl54 force-pushed the feat/#22 branch 2 times, most recently from 73d8e65 to 53f3957 Compare July 2, 2026 15:27
@fnzl54 fnzl54 force-pushed the feat/#22 branch 6 times, most recently from 345a545 to ed066bf Compare July 3, 2026 01:01
@fnzl54

fnzl54 commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

@claude review

val expression = toBooleanModeExpression(keyword) ?: return null
return Expressions.numberTemplate(
Double::class.javaObjectType,
"function('match_against', {0}, {1}, '$expression')",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[BLOCKER] FULLTEXT 검색 키워드가 QueryDSL 파라미터 바인딩 없이 SQL 문자열에 직접 삽입되고 있습니다 (SQL/HQL 인젝션 위험).

Expressions.numberTemplate에서 {0}, {1}book.title, book.author로 바인딩되고, 검색어($expression)는 Kotlin 문자열 보간으로 템플릿에 직접 삽입되어 작은따옴표로 감싸지고 있습니다:

"function('match_against', {0}, {1}, '$expression')"

toBooleanModeExpression의 sanitizer BOOLEAN_OPERATORS = "[+\\-><()~*\"@\\\\]"+ - > < ( ) ~ * \" @ \\ 문자는 제거하지만 작은따옴표(')는 제거하지 않습니다. 따라서 O'Brien처럼 작은따옴표가 포함된 검색어를 입력하면 '+O'Brien*'가 되어 AGAINST(...) 문자열 리터럴을 깨뜨리고, 조작된 입력값으로는 HQL 인젝션까지 가능합니다.

프로젝트 규칙: "동적 쿼리에서 사용자 입력값을 문자열로 직접 삽입 금지 (SQL 인젝션)" / "반드시 Expressions.numberTemplate 등 QueryDSL 파라미터 바인딩 사용".

수정 방향: 검색어도 title/author처럼 바인딩 파라미터로 전달해야 합니다. match_against 함수가 3개 인자(?1, ?2, ?3)를 받도록 이미 등록되어 있으므로, 아래처럼 {2}로 바인딩하는 것을 권장합니다.

return Expressions.numberTemplate(
    Double::class.javaObjectType,
    "function('match_against', {0}, {1}, {2})",
    book.title,
    book.author,
    Expressions.constant(expression),
)

@Table(name = "loan")
class Loan(
@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.EAGER)

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] Loan.bookItem 연관관계의 fetch 전략이 FetchType.LAZY에서 FetchType.EAGER로 변경되었습니다.

프로젝트 규칙: "연관관계는 반드시 FetchType.LAZY 사용 (FetchType.EAGER 금지)".

이 변경은 이번 PR의 목적(Book 검색 API)과 무관해 보이며, 모든 Loan 조회 시마다 BookItem을 즉시 로딩하게 되어 불필요한 조인/N+1 위험을 초래합니다. 의도된 변경이 아니라면 원복이 필요합니다.

Suggested change
@ManyToOne(fetch = FetchType.EAGER)
@ManyToOne(fetch = FetchType.LAZY)

val bookItem =
bookItemRepository.findByCallNumber(request.callNumber)
?: throw ErrorCode.BOOK_ITEM_NOT_FOUND.toException()
?: throw RuntimeException("해당 소장본을 찾을 수 없습니다.")

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] 프로젝트의 ErrorCodeDomainException 예외 흐름 대신 RuntimeException을 직접 사용하고 있습니다.

프로젝트 규칙: "예외는 반드시 Service 내부 ErrorCode enum → DomainException.from() 흐름을 따를 것" / "throw RuntimeException(message) ... 직접 사용 금지".

기존 코드는 ErrorCode.BOOK_ITEM_NOT_FOUND.toException()을 사용했고, 해당 ErrorCode는 여전히 존재하지만 (동일 파일 57번째 줄) 더 이상 참조되지 않습니다. 이 변경으로 인해 "소장본을 찾을 수 없음"이 의도한 404 응답 대신 전역 SystemExceptionHandlerException 핸들러에 걸려 500 Internal Server Error로 처리됩니다. 바로 다음 줄은 여전히 ErrorCode.BOOK_ITEM_NOT_AVAILABLE.toException()을 올바르게 사용하고 있어 일관성도 깨집니다.

Suggested change
?: throw RuntimeException("해당 소장본을 찾을 수 없습니다.")
?: throw ErrorCode.BOOK_ITEM_NOT_FOUND.toException()

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