Conversation
73d8e65 to
53f3957
Compare
345a545 to
ed066bf
Compare
|
@claude review |
| val expression = toBooleanModeExpression(keyword) ?: return null | ||
| return Expressions.numberTemplate( | ||
| Double::class.javaObjectType, | ||
| "function('match_against', {0}, {1}, '$expression')", |
There was a problem hiding this comment.
[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) |
There was a problem hiding this comment.
[HIGH] Loan.bookItem 연관관계의 fetch 전략이 FetchType.LAZY에서 FetchType.EAGER로 변경되었습니다.
프로젝트 규칙: "연관관계는 반드시 FetchType.LAZY 사용 (FetchType.EAGER 금지)".
이 변경은 이번 PR의 목적(Book 검색 API)과 무관해 보이며, 모든 Loan 조회 시마다 BookItem을 즉시 로딩하게 되어 불필요한 조인/N+1 위험을 초래합니다. 의도된 변경이 아니라면 원복이 필요합니다.
| @ManyToOne(fetch = FetchType.EAGER) | |
| @ManyToOne(fetch = FetchType.LAZY) |
| val bookItem = | ||
| bookItemRepository.findByCallNumber(request.callNumber) | ||
| ?: throw ErrorCode.BOOK_ITEM_NOT_FOUND.toException() | ||
| ?: throw RuntimeException("해당 소장본을 찾을 수 없습니다.") |
There was a problem hiding this comment.
[HIGH] 프로젝트의 ErrorCode → DomainException 예외 흐름 대신 RuntimeException을 직접 사용하고 있습니다.
프로젝트 규칙: "예외는 반드시 Service 내부 ErrorCode enum → DomainException.from() 흐름을 따를 것" / "throw RuntimeException(message) ... 직접 사용 금지".
기존 코드는 ErrorCode.BOOK_ITEM_NOT_FOUND.toException()을 사용했고, 해당 ErrorCode는 여전히 존재하지만 (동일 파일 57번째 줄) 더 이상 참조되지 않습니다. 이 변경으로 인해 "소장본을 찾을 수 없음"이 의도한 404 응답 대신 전역 SystemExceptionHandler의 Exception 핸들러에 걸려 500 Internal Server Error로 처리됩니다. 바로 다음 줄은 여전히 ErrorCode.BOOK_ITEM_NOT_AVAILABLE.toException()을 올바르게 사용하고 있어 일관성도 깨집니다.
| ?: throw RuntimeException("해당 소장본을 찾을 수 없습니다.") | |
| ?: throw ErrorCode.BOOK_ITEM_NOT_FOUND.toException() |
관련 이슈
작업 내용
book(title, author)컬럼에 ngram 파서 기반 FULLTEXT 인덱스 생성BookQueryRepository검색 로직 교체containsIgnoreCase) → FULLTEXT (match_against커스텀 함수) 방식으로 전환테스트
주의 사항