analysis: 강의 검색 API 성능 측정과 병목 없음 판정(#100) - #102
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAUhvTJ9synDRSj5pRm1VR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAUhvTJ9synDRSj5pRm1VR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAUhvTJ9synDRSj5pRm1VR
Test Results286 tests 286 ✅ 4s ⏱️ Results for commit f7f703b. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 기준)
쿼리 통계 (요청 43,545건의 digest 합계)
findByKeyword)course_id IN (...))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