Skip to content

analysis: 강의 검색 API 성능 측정과 병목 없음 판정(#100) - #102

Merged
xunssoie merged 3 commits into
devfrom
refactor/100-search-performance
Aug 28, 2026
Merged

analysis: 강의 검색 API 성능 측정과 병목 없음 판정(#100)#102
xunssoie merged 3 commits into
devfrom
refactor/100-search-performance

Conversation

@xunssoie

Copy link
Copy Markdown
Member

PR Summary

강의 검색 API(GET /api/v1/courses/search)의 응답시간이 어디에 쓰이는지 운영 10배 규모에서 측정했고, DB와 애플리케이션 어느 쪽에도 개선할 병목이 없다고 판정해 코드 변경 없이 마무리합니다. 측정을 재현 가능하게 만들기 위해 성능 최적화 스킬의 구조와 시드 모듈, 측정 프로파일을 함께 정리했습니다.


Problem

문제 1 - 검색 API의 시간 사용처를 추정으로만 알고 있었음

이슈는 검색 결과의 강의마다 시간표를 다시 조회하고, 검색어가 넓을수록 조회 횟수와 응답 크기가 같이 늘어난다는 전제로 열렸습니다. 그런데 실제로 어느 구간에서 시간이 쓰이는지 잰 적이 없어, 인덱스나 fetch join 같은 기법을 고르더라도 효과를 판단할 기준이 없었습니다. 개선을 하려면 먼저 기준선과 병목의 성격이 필요했습니다.


문제 2 - 이전 측정 시도가 재현 불가능한 상태로 중단됨

앞선 시도는 근거 없이 2,000,000행(운영 대비 약 820배)을 적재하려다 스킬이 적재 완료를 기다리며 토큰만 소모한 채 끊겼습니다. 시드 모듈은 마이그레이션이 추가된 스키마와 컬럼이 맞지 않아 쓰지 못했고, 측정 프로파일과 토큰 발급 절차가 각 시도마다 손으로 다시 만들어졌습니다. 같은 조건으로 다시 잴 수 없으면 개선 전후 비교 자체가 성립하지 않습니다.


Solution

해결 1 - 운영 10배 규모에서 기준선을 재고 병목 없음으로 판정

검색 쿼리가 학년도, 학기로 거르지 않아 학기가 쌓일수록 훑는 행이 늘어나는 구조이므로, 학기 약 10개가 쌓인 상태를 운영 10배(강의 26,439건, 시간표 79,819행)로 재현했습니다. 키워드당 매칭은 실데이터 선택도와 같은 수준(약 26건)으로 맞췄습니다. 이슈의 전제였던 강의별 시간표 재조회는 @BatchSize(1000) 덕에 실제로는 요청당 1건이었고, N+1은 없었습니다.

부하 지표 (강의 26,439건, VU 30, 유지 1분, 캐시 warm 기준)

지표
p95 114.4 ms
p99 172.7 ms
처리량 362.9 rps
에러율, check 통과율 0%, 100%
요청당 쿼리 수 2.00 (검색 1 + 시간표 배치 1), 트랜잭션 제어문 5
요청당 DB 시간 4.6 ms
커넥션 풀 점유율 약 17% (풀 10)

쿼리 통계 (요청 43,545건의 digest 합계)

쿼리 요청당 평균 비중 스캔 행 / 반환 행
FULLTEXT 검색 (findByKeyword) 1.00 2.44 ms 53.1% 2.0
시간표 배치 로딩 (course_id IN (...)) 1.00 1.65 ms 35.8% 1.0
트랜잭션 제어문 4종 5.00 0.10 ms 11.1% -

DB 시간이 응답시간의 7%에 그쳐 나머지가 애플리케이션 처리인지 부하 포화인지 가르기 위해 VU 1로 단건을 따로 쟀습니다. 단건 응답은 중앙값 7.3 ms로 DB 4.6 ms와의 차이가 3~4 ms에 불과했고, VU 30에서 늘어난 74 ms는 요청 하나의 처리가 아니라 k6, JVM, MySQL이 같은 8코어를 나눠 쓴 포화 대기였습니다. 읽고 버리는 행이 없고(스캔 행 / 반환 행 2.0, 1.0), 단건 7.3 ms에서 줄일 폭이 ms 단위라 이 규모에서는 개선 기법을 적용하지 않기로 했습니다. 실행 계획과 Handler_% 카운터는 사이클에 들어가지 않아 캡처하지 않았습니다.


해결 2 - 측정을 재현 가능하게 만드는 스킬 구조 정리

스킬의 역할을 사실과 판단으로 갈라, 수치 재구성과 쿼리 출처 매핑은 스킬이 하고 병목과 기법의 판단은 호출자가 하도록 고정했습니다. 적재, 부하, DB 조회는 호출자가 직접 실행하고 스킬은 명령 블록(commands.md)과 결과 파일만 다루게 해 대기 중 토큰 소모를 없앴습니다. 시드는 변수 블록만 이슈 디렉토리에 두고 본문은 현재 스키마에 맞춘 공용 모듈을 이어 붙이는 방식으로 바꿨고, 규모는 운영 대비 배수와 이유를 함께 확정하도록 했습니다. 측정 프로파일(application-perf.yml), 셸 환경(perf-env.sh), 토큰 서명 도구(_shared/mint-tokens.sh)를 레포에 두어 새 터미널에서 같은 조건을 바로 만들 수 있습니다.


측정 조건과 산출물

로컬(macOS, 8코어, MySQL 8.0 Docker, 버퍼 풀 128MiB)에서 쟀으므로 처리량과 응답시간은 하드웨어에 의존하는 값입니다. 판정은 요청당 쿼리 수, 스캔 행 / 반환 행, 요청당 DB 시간과 단건 응답시간의 차이 같은 하드웨어 독립 지표로 했습니다. 기록과 원본 수치는 .claude/resources/perf/100/search/record.md와 같은 디렉토리의 k6-test-summary-0.json, query-stats-summary-0.md, k6-probe-vu1.json에 있습니다. 측정 중 검색 결과에 학년 정렬이 없고 관련도 정렬 방향이 정책과 반대인 점을 발견해 #101로 분리했습니다.


Related Issue

@xunssoie xunssoie self-assigned this Aug 28, 2026
@github-actions

Copy link
Copy Markdown

Test Results

286 tests   286 ✅  4s ⏱️
 91 suites    0 💤
 91 files      0 ❌

Results for commit f7f703b.

@xunssoie
xunssoie merged commit 6357274 into dev Aug 28, 2026
2 checks passed
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.

refactor: 강의 검색 API 응답 성능 개선

1 participant