Skip to content

[Fix/#162] 물품·회원 검색에 검색어 이스케이프와 정리 방식 통일 - #163

Merged
jjunh33 merged 4 commits into
mainfrom
fix/#162-unify-search-like-escape
Oct 22, 2026
Merged

jjunh33 merged 4 commits into
mainfrom
fix/#162-unify-search-like-escape

Conversation

@jjunh33

@jjunh33 jjunh33 commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

검색 기능이 LikePatterns(검색어의 %·_ 이스케이프)를 쓰는 곳은 공지뿐이고, 물품 검색과 회비 목록의 이름·학번 검색(회원 ID 조회)은 각자 다르게 구현되어 있습니다.

  • 회비 목록의 이름·학번 검색(관리자 회비 확인 요청 목록): 사용자가 입력한 keyword가 AdminFeeSearchUseCase를 거쳐 내부 조회 MemberService.searchIdsByKeyword(이름·학번이 맞는 회원 ID를 구하는 보조 조회, 사용처는 이 UseCase 하나)로 내려갑니다. 이 조회의 MemberJpaRepository.searchIdsByKeyword가 LIKE CONCAT('%', :keyword, '%')를 이스케이프 없이 써서, 회비 목록 검색창에 %만 입력해도 모든 회원이 걸려 사실상 모든 요청이 조회되고 _도 한 글자 와일드카드로 동작합니다(버그). 앞뒤 공백도 정리하지 않아 "홍길동 "처럼 공백이 붙은 검색어는 결과가 없을 수 있습니다.
  • 물품 검색(학생 물품 목록, ItemJpaRepository.findSlice): LOCATE로 구현되어 %·_가 글자 그대로 검색되어 결과는 올바르지만 공지와 방식이 다릅니다.
  • 검색어 정리("공백뿐이면 검색 없음", 앞뒤 공백 제거)가 검색마다 제각각입니다. 공지는 서비스, 물품은 요청 파라미터 레코드(ItemListParams.toKeyword()), 회원은 UseCase에 있거나 아예 없습니다.

❓ 왜 해결해야 하나요?

회비 목록 검색의 와일드카드 문제는 실제 오동작입니다(관리자가 검색창에 %를 입력하면 전체가 조회). 내부 조회 메서드라도 사용자 입력이 그대로 닿는 경로입니다. 또 같은 검색 처리가 여러 방식으로 흩어져 있으면 이후 검색을 추가할 때 어느 쪽을 따를지 매번 판단해야 하고, 한 곳만 고치면 나머지가 어긋납니다. 이스케이프와 정리를 한 곳(LikePatterns.escape)으로 모읍니다.

⭐ 어떻게 해결했나요?

  • 회비 목록 검색의 회원 ID 조회 (fix): 쿼리의 이름·학번 두 LIKE에 ESCAPE '!'를 붙이고, MemberRepositoryImpl.searchIdsByKeyword가 LikePatterns.escape(keyword)를 호출합니다.
  • 물품 (refactor): LOCATE(:keyword, i.name) > 0을 i.name LIKE CONCAT('%', :keyword, '%') ESCAPE '!'로 바꾸고, ItemRepositoryImpl.findSlice가 LikePatterns.escape를 호출합니다. 검색 결과는 이전과 같습니다.
  • 검색어 정리를 LikePatterns.escape로 통일 (refactor): escape가 이스케이프 전에 정리까지 맡도록 확장했습니다(메서드 이름은 그대로). 검색어가 null이거나 공백뿐이면 "검색 없음"이라 null을 돌려주고, 아니면 앞뒤 공백을 지운 뒤 %·_·! 앞에 !를 붙입니다. 저장소 구현이 escape를 호출하므로 서비스는 검색어를 가공하지 않고 그대로 넘깁니다.
    • 물품: ItemListParams.toKeyword()를 삭제하고 컨트롤러는 params.keyword()를 그대로 서비스에 넘깁니다. ItemServiceImpl.getItems에도 정리 코드가 없습니다.
    • 회원 ID 조회: MemberServiceImpl.searchIdsByKeyword도 정리 없이 저장소에 넘깁니다.
    • ItemService, ItemRepository, MemberService의 Javadoc을 "null·공백이면 검색하지 않음, 앞뒤 공백 제거, %·_는 글자 그대로"로 맞췄습니다.

🧩 이 PR의 한계 & 트레이드오프

  • MySQL로 확인하지 못했습니다. 로컬에 Docker가 없어 H2 임시 테스트(커밋하지 않음)로만 확인했습니다. 확인한 것: 회원 % → %가 든 이름만, _ → _가 든 이름만, " 길동 " → 공백 제거 후 검색, 공백·빈 값·null → 오류 없이 빈 결과; 물품 %·_·100%가 글자 그대로 검색되고 공백·빈 값은 전체 조회, " 우산 " → 우산 검색. LIKE ... ESCAPE '!'는 표준 문법이라 위험이 낮지만 dev에서 쿼리가 정상 기동·조회되는지 확인이 필요합니다. 쿼리가 틀리면 앱 기동이 실패합니다.
  • 검색 규칙이 인프라 계층(LikePatterns)으로 이동했습니다. "공백이면 검색하지 않는다"가 서비스가 아니라 infrastructure:db에 있어서, getItems나 searchIdsByKeyword만 읽어서는 이 규칙이 보이지 않습니다. 대신 서비스마다 같은 한 줄 정리를 반복하지 않고 한 곳에서 관리합니다. 서비스 쪽에는 Javadoc으로 동작을 적었습니다.
  • escape가 이스케이프 외에 공백 정리와 null 변환도 합니다. 이름이 하는 일보다 작게 읽히는 점은 감수했습니다. 이스케이프만 필요한 호출처가 생기면 분리를 검토합니다.
  • 회원 ID 조회는 공백이면 빈 결과입니다. 공지·물품 쿼리는 :keyword IS NULL OR … 가드로 null이면 조건이 빠지지만, 회원 쿼리에는 이 가드가 없어 escape가 null을 돌려주면 LIKE CONCAT('%', NULL, '%')가 아무 행도 걸리지 않습니다. 실제 호출자인 AdminFeeSearchUseCase는 keyword != null && !keyword.isBlank()로 공백을 먼저 걸러 이 경로를 타지 않으며, 이 검사는 "회원 필터를 쓸지 말지"를 정하는 흐름 제어라 유지했습니다.
  • 공지에는 같은 정리가 한동안 중복됩니다. NoticeServiceImpl.search([Refactor/#144] 공지 관리자 기능을 AdminNoticeService로 분리 #145 머지 후에는 AdminNoticeServiceImpl)에 있는 normalizedKeyword 줄은 이 PR 범위 밖이라 그대로 두었습니다. 이미 정리된 값을 escape에 넣어도 결과가 같아서 동작에는 영향이 없고, 해당 줄은 #145를 머지할 때 지우면 됩니다.
  • 포함 검색(LIKE '%…%')이라 인덱스를 타지 못하는 점은 이전(LOCATE, 이스케이프 없는 LIKE)과 같습니다.
  • 새 테스트는 추가하지 않았습니다.

⛓️ 기존 기능에 미치는 영향

  • API 경로·요청·응답은 그대로입니다.
  • 회비 관리자 목록의 이름·학번 검색은 %·_를 입력했을 때의 결과가 달라집니다(전체 요청 → 해당 문자가 든 이름·학번의 회원 요청만). 검색어 앞뒤 공백이 제거됩니다.
  • 물품 검색은 검색 결과가 이전과 같습니다. ItemListParams.toKeyword()가 사라지므로 이를 쓰던 AppRentalController를 고쳤습니다(다른 사용처 없음).
  • 공지 검색은 LikePatterns.escape의 동작이 확장됐지만 이미 정리된 값만 넘어와 결과가 같습니다.
  • 스키마 변경·마이그레이션은 없습니다.

🔀 Edge Case & 실패 시나리오

상황 결과
회비 목록(이름·학번)/물품 검색어가 % 또는 _ 와일드카드가 아니라 글자 그대로 검색 (해당 문자가 든 항목만)
검색어에 ! 포함 이스케이프 문자도 글자 그대로 검색
물품 검색어가 null, 빈 문자열, 공백뿐 검색하지 않고 전체 조회
검색어 앞뒤 공백 (" 우산 ") 제거 후 검색 (회비 목록의 이름·학번 검색도 이제 제거됨)
회원 ID 조회의 검색어가 null, 빈 문자열, 공백뿐 일치하는 회원 없음(빈 목록). 실제로는 UseCase가 먼저 걸러 호출되지 않음
회비 목록 keyword가 공백뿐 회원 필터 없이 전체 요청 조회 (UseCase)
회원 ID 조회 결과가 0명 회비 목록도 빈 페이지 (기존과 동일)

📋 검토한 대안과 선택 이유

  • 검색어 정리를 도메인 서비스에 두기(이 PR의 이전 커밋에서 구현): 규칙이 도메인에 보이는 장점이 있었지만, 검색마다 서비스에 같은 한 줄이 반복되고(물품·공지·회원) 규칙이 바뀌면 여러 곳을 같이 고쳐야 해서, 한 곳에서 처리하는 쪽으로 바꿨습니다.
  • 정리를 요청 파라미터(api)에 두기: 물품이 쓰던 방식인데, 관리자·학생 API마다 같은 정리를 반복해야 하고 서비스를 직접 부르는 다른 경로에서는 규칙이 빠집니다.
  • core:common에 공용 정리 함수를 두고 서비스가 호출: 규칙을 도메인에 두면서 중복도 없앨 수 있지만 서비스마다 호출이 필요하고 새 공용 유틸이 하나 더 생겨 제외했습니다.
  • 물품은 LOCATE를 유지: 결과가 이미 올바르므로 바꾸지 않아도 됩니다. 다만 같은 목적의 검색이 두 가지 구현으로 남는 것이 혼란이어서 통일했습니다.
  • 메서드 이름을 escape에서 바꾸기: 하는 일이 더 늘었지만 호출처가 이미 escape를 쓰고 있어 이름은 유지하기로 했습니다.

💬 리뷰 포인트

  • [r] 회원 ID 조회·물품 JPQL의 LIKE … ESCAPE '!'가 dev(MySQL)에서 정상 기동·조회되는지
  • [c] 검색어 정리를 LikePatterns.escape(인프라)로 모은 것. 서비스 쪽에서는 규칙이 보이지 않는 점을 Javadoc으로 충분히 보완했는지
  • [c] 회원 ID 조회가 공백이면 빈 결과가 되는 점과 AdminFeeSearchUseCase의 검사를 유지한 판단
  • [a] ItemListParams.toKeyword() 삭제, 공지의 중복 정리 줄은 #145에서 정리

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jjunh33
jjunh33 merged commit 8552e8a into main Oct 22, 2026
1 check passed
@jjunh33
jjunh33 deleted the fix/#162-unify-search-like-escape branch October 22, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

물품·회원 검색에 검색어 이스케이프와 정리 방식 통일

2 participants