Skip to content

fix: 납부자 목록 조회 정렬 중복 제거 및 criteria 파라미터 오동작 수정 - #150

Merged
tnals0924 merged 1 commit into
developfrom
fix/#142-filesort
Aug 17, 2026
Merged

fix: 납부자 목록 조회 정렬 중복 제거 및 criteria 파라미터 오동작 수정#150
tnals0924 merged 1 commit into
developfrom
fix/#142-filesort

Conversation

@tnals0924

@tnals0924 tnals0924 commented Aug 17, 2026

Copy link
Copy Markdown
Member

#️⃣연관된 이슈

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

#142 에서 Payer 인덱스를 조회 정렬 순서에 맞춰 (enrollment_year DESC, name) 으로 바꿨는데, 쿼리가 그 인덱스를 정렬에 쓸 수 없는 모양으로 나가고 있습니다. 인덱스는 만들어졌지만 쓰이지 않는 상태입니다.

1. ORDER BY 가 두 곳에서 지정되고 있습니다.

findAllByNameContaining()@Query 에 ORDER BY 가 하드코딩된 상태에서, PayerServicePageRequest 에도 Sort 를 넘기고 있습니다. Spring Data JPA 는 @Query 에 ORDER BY 가 있으면 Pageable 의 Sort 를 지우는 게 아니라 뒤에 이어붙입니다. 실제로 나가는 SQL:

order by p.enrollment_year desc, p.name asc, p.enrollment_year desc, p.id asc

앞 두 키까지는 인덱스와 맞지만 뒤에 키가 더 붙는 순간 MySQL 은 인덱스 순서로 정렬을 대체하지 못하고, 조건에 걸린 전체 행을 filesort 합니다.

부수적으로 criteria 파라미터가 죽어 있습니다. 하드코딩된 enrollment_year desc 가 항상 첫 번째 키라서 어떤 값을 넣어도 정렬이 바뀌지 않고, 엔티티에 없는 프로퍼티명이나 빈 문자열을 넣으면 p. 가 그대로 HQL 에 붙어 쿼리가 깨지면서 500 이 납니다.

2. 검색어가 없어도 LIKE '%%' 를 태웁니다.

SearchCondition.search 의 기본값이 "" 인데 분기가 없어, 목록 첫 진입에도 술어가 붙습니다. 한 건도 걸러내지 못하면서 선행 와일드카드 때문에 인덱스 접근만 막습니다.

❓ 왜 해결해야 하나요?

criteria 로 인한 500 은 지금 당장의 결함입니다. API 스펙에 노출된 파라미터인데 동작하지 않을 뿐 아니라, 임의값이 들어오면 서버 에러로 떨어지는 경로가 열려 있습니다.

정렬 중복은 지금은 비용이 작지만 잠복성 결함입니다. 현재 payer 테이블은 2천 건 규모라 filesort 비용이 유의미하지 않습니다. 다만 #142 가 목표한 "정렬 비용 제거" 가 실제로는 달성되지 않은 상태이고, 인덱스만 있고 쓰이지 않으니 쓰기 비용만 늘고 읽기 이득은 없습니다. 납부자는 학기마다 누적되는 데이터라 규모가 커질 때 드러납니다.

⭐ 어떻게 해결했나요?

PayerRepository.findAllByNameContaining()@Query 에서 ORDER BY 제거. 정렬은 Pageable 의 Sort 에만 맡깁니다. (RentalRepository.findAllByMemberNameContaining() 이 이미 쓰고 있는 방식과 동일)

PayerService.resolveSort() — 정렬 생성을 이 메서드 한 곳으로 모았습니다.

private fun resolveSort(criteria: String?): Sort = when (criteria) {
    "name" -> Sort.by(asc("name"), desc("enrollmentYear"), asc("id"))
    else   -> Sort.by(desc("enrollmentYear"), asc("name"), asc("id"))
}

기본 정렬이 idx_payer_enrollment_year_name(enrollment_year DESC, name) 의 순서와 일치합니다. InnoDB 보조 인덱스는 뒤에 PK 가 붙으므로 id ASC 까지 인덱스 순서 그대로입니다. criteria 는 화이트리스트로 받아 알 수 없는 값이면 기본 정렬로 떨어집니다.

PayerService.getAllPayers() — 검색어가 비어 있으면 술어 없이 findAll(pageable) 로 보냅니다.

결과 SQL (검색어 없는 경우):

-- before
select ... from payer p where p.name like concat('%','%')
  order by p.enrollment_year desc, p.name asc, p.enrollment_year desc, p.id asc limit 10
select count(p.payer_id) from payer p where p.name like concat('%','%')

-- after
select ... from payer p
  order by p.enrollment_year desc, p.name asc, p.id asc limit 10
select count(p.payer_id) from payer p

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

응답시간 개선 효과는 지금 규모에선 사실상 없습니다. 2천 행 filesort 는 1ms 미만입니다. 이 PR 은 성능 수치를 움직이려는 게 아니라, 정렬이 두 곳에 흩어져 있던 걸 한 곳으로 모으고 criteria 의 500 을 막는 게 목적입니다. 성능은 규모가 커졌을 때를 위한 예방입니다.

EXPLAIN 으로 확인하지 않았습니다. 코드 레벨 수정만 했고, MySQL 이 실제로 이 인덱스를 정렬에 채택하는지는 검증되지 않았습니다. 보조 인덱스 뒤에 붙는 PK 를 ORDER BY 에 활용하는 건 optimizer_switchuse_index_extensions 에 의존하고, DESC 인덱스는 MySQL 8.0 이상이 필요합니다.

검색어가 있는 경로는 그대로입니다. LIKE '%김%' 은 선행 와일드카드라 여전히 인덱스로 범위를 좁히지 못합니다. 분기로 풀 수 있는 문제가 아니고 검색 방식 자체를 바꿔야 해서 이번 범위에서 뺐습니다.

count 쿼리는 여전히 매 요청 전체를 셉니다. WHERE 가 빠져 행마다 LIKE 를 평가하는 비용은 사라졌지만 count(*) 자체는 남습니다.

깊은 페이지는 개선되지 않습니다. pageNo=500 이면 LIMIT 5000, 10 으로 5010 건을 읽고 버립니다. 커서 기반 페이징은 API 스펙 변경이라 별도 논의가 필요합니다.

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

응답의 행 순서는 바뀌지 않습니다. 기존에도 중복으로 붙던 id asc 가 동점 처리 역할을 하고 있어서, before/after 의 최종 정렬 결과는 동일합니다. 정렬 방법만 바뀝니다.

criteria 파라미터의 동작이 달라집니다. 이전에는 어떤 값이든 무시됐지만 이제 criteria=name 이 실제로 동작합니다. 프론트에서 이 값을 보내고 있다면 정렬이 실제로 바뀌므로 확인이 필요합니다. AdminPayerApi 에는 주석 처리만 되어 있어 실사용 여부를 코드에서 판단하지 못했습니다.

공백만 있는 검색어의 동작이 달라집니다. isEmpty() 가 아니라 isBlank() 로 판단해서, search=" " 는 이제 "검색어 없음" 으로 전체 목록을 반환합니다. 이전에는 LIKE '% %' 로 이름에 공백이 든 행만 찾았습니다.

DB 스키마 변경은 없습니다. 다른 도메인에 미치는 영향도 없습니다.

🔀 Edge Case & 실패 시나리오

입력 before after
criteria 미지정 enrollment_year desc, name asc 동일
criteria=name 무시됨 name asc, enrollment_year desc, id asc
criteria=존재하지않는필드 HQL 파싱 실패 → 500 기본 정렬로 폴백
criteria= (빈 문자열) p. 가 붙어 쿼리 깨짐 → 500 기본 정렬로 폴백
search 미지정 LIKE '%%' 풀스캔 술어 없음
search=" " LIKE '% %' 전체 목록
search=김 LIKE '%김%' 동일 (미개선)

📋 검토한 대안과 선택 이유

@query 를 남기고 Pageable 의 Sort 를 없애는 방법 — 정렬이 리포지토리에 고정되어 criteria 를 아예 못 쓰게 됩니다. 또 대여 이력 조회(RentalRepository)가 이미 "정렬은 Pageable" 방식이라 두 도메인의 패턴이 갈립니다. Pageable 쪽으로 통일했습니다.

WHERE (:name IS NULL OR p.name LIKE ...) 로 한 쿼리에 합치는 방법 — 분기가 사라져 코드는 짧아지지만, 옵티마이저가 실행계획을 하나로 고정해버려 검색어 없는 경우에도 인덱스를 못 타게 될 수 있습니다. 명시적 분기를 택했습니다.

criteria 를 enum 으로 받는 방법 — 타입 레벨에서 막히지만 PageableCondition 이 대여 등 다른 도메인과 공유되는 글로벌 DTO라 도메인별 정렬 키를 하나의 enum 으로 표현하기 어렵습니다. 서비스 안 화이트리스트로 두었습니다.

💬 리뷰 포인트

  • [r] criteria 가 실제로 동작하기 시작합니다. 프론트에서 이 파라미터를 보내고 있는지 확인 부탁드립니다. 보내고 있다면 정렬 결과가 바뀝니다.
  • [c] 이 PR 은 성능 수치를 목표로 하지 않습니다. 현재 관측 중인 GET /admin/members/payers 의 p95 는 이 변경으로 개선되지 않을 가능성이 높고, 원인은 따로 봐야 합니다.
  • [c] resolveSort() 의 화이트리스트에 name 만 넣었습니다. 필요한 정렬 기준이 더 있으면 알려주세요.
  • [a] isBlank() vs isEmpty() — 공백만 입력한 경우를 "검색어 없음" 으로 봤는데, 이견 있으면 isEmpty() 로 바꾸겠습니다.

🧪 테스트

./gradlew compileKotlin 통과. 납부자 도메인에는 현재 테스트가 없어 자동 검증은 없는 상태입니다.

#142에서 인덱스를 (enrollment_year DESC, name) 순서로 맞췄지만 정렬 비용은
그대로 남아 있었다. 원인은 두 가지다.

1. ORDER BY가 두 곳에서 지정됨
   findAllByNameContaining()의 @query에 ORDER BY가 하드코딩된 상태에서
   PageRequest에도 Sort를 넘기고 있었다. Spring Data는 @query에 ORDER BY가
   있으면 Pageable의 Sort를 지우지 않고 뒤에 이어붙이기 때문에 실제 SQL은
   `order by enrollment_year desc, name asc, enrollment_year desc, id asc`가
   되고, 정렬 키가 인덱스와 어긋나 매 조회마다 filesort가 발생했다.
   덤으로 하드코딩된 키가 항상 앞에 오는 탓에 criteria 파라미터는 무시됐고,
   엔티티에 없는 이름을 넣으면 HQL이 깨져 500이 났다.

   @query에서 ORDER BY를 걷어내고 정렬 생성을 resolveSort() 한 곳으로 모았다.
   criteria는 화이트리스트로 받아 알 수 없는 값이면 기본 정렬로 떨어진다.

2. 검색어가 없어도 LIKE '%%'를 태움
   SearchCondition.search의 기본값이 ""라 목록 첫 진입에도 술어가 붙었다.
   한 건도 걸러내지 못하면서 선행 와일드카드로 인덱스 접근만 막는다.
   검색어가 비어 있으면 술어 없이 findAll(pageable)로 보낸다.

응답의 행 순서는 바뀌지 않는다. 기존에도 뒤에 붙던 id asc가 동점 처리
역할을 하고 있어 최종 정렬 결과는 동일하고, 정렬 방법만 filesort에서
인덱스 순서 스캔으로 바뀐다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tnals0924 tnals0924 changed the title fix: 납부자 목록 조회의 filesort 제거 - #142 fix: 납부자 목록 조회의 filesort 제거 Aug 17, 2026
@tnals0924 tnals0924 self-assigned this Aug 17, 2026
@tnals0924 tnals0924 added the bug 버그 fix label Aug 17, 2026
@tnals0924 tnals0924 changed the title fix: 납부자 목록 조회의 filesort 제거 fix: 납부자 목록 조회 정렬 중복 제거 및 criteria 파라미터 오동작 수정 - #142 Aug 17, 2026
@tnals0924 tnals0924 changed the title fix: 납부자 목록 조회 정렬 중복 제거 및 criteria 파라미터 오동작 수정 - #142 fix: 납부자 목록 조회 정렬 중복 제거 및 criteria 파라미터 오동작 수정 Aug 17, 2026
@tnals0924
tnals0924 merged commit be4f51c into develop Aug 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 버그 fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: 납부자 목록 조회 정렬 순서와 불일치하는 인덱스 수정

1 participant