feat: k6 부하테스트 스크립트 및 Grafana 대시보드 추가 - #93
Conversation
k6 시나리오 테스트: 매장 탐색, 예약 동시성, 전체 플로우, 서킷브레이커 k6 단일 API 테스트: 매장 목록, PostGIS 지리 쿼리, 잔여석 조회, 쿠폰 선착순 발급 k6 내구성 테스트: 10VU x 30분 Soak 테스트 (메모리 누수, 커넥션풀 고갈 탐지) run.ps1: 테스트 번호로 실행하는 래퍼 스크립트 (01~08, 10) Grafana 백엔드 모니터링 대시보드 JSON (수동 import용) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces Grafana dashboards and a comprehensive suite of k6 load testing scripts (covering store browsing, reservation concurrency, full user flow, circuit breaker, store list, nearby stores, remains, coupon issue, and soak tests) along with a PowerShell runner script. The reviewer feedback identifies several critical issues in the k6 scripts that need to be addressed: using Korean string values instead of English Enum constants for Category and District (which will cause 400 Bad Request errors), using UTC dates that can lead to test failures before 9 AM KST, specifying k6 timeouts as strings (e.g., '65s') instead of millisecond numbers, and potential k6 runner crashes when parsing non-JSON error responses without a try-catch block.
|
|
||
| export default function () { | ||
| const storeId = STORE_IDS[Math.floor(Math.random() * STORE_IDS.length)]; | ||
| const today = new Date().toISOString().split('T')[0]; |
There was a problem hiding this comment.
new Date().toISOString()는 UTC 기준 시간을 반환하므로, 한국 시간(KST, UTC+9) 기준으로 오전 9시 이전에 테스트를 실행하면 어제 날짜가 생성됩니다. 이로 인해 과거 날짜에 대한 잔여석 조회(GET /api/v1/remains)가 요청되어 API가 실패하거나 빈 결과를 반환할 수 있습니다. 한국 시간 기준으로 안전하게 오늘 날짜를 구하도록 수정하는 것이 좋습니다.
| const today = new Date().toISOString().split('T')[0]; | |
| const today = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().split('T')[0]; |
| } else if (res.status === 400 || res.status === 409) { | ||
| const body = res.json(); | ||
| const code = body?.code || ''; |
There was a problem hiding this comment.
고부하 상황에서 서버가 502, 503, 504 등 비정상 응답(HTML 에러 페이지 등)을 반환할 경우, res.json() 호출 시 예외가 발생하여 k6 VU(가상 사용자) 실행이 중단될 수 있습니다. 08-coupon-issue.js에서 적용하신 것처럼 안전하게 try-catch로 감싸서 예외를 방지하는 것이 좋습니다.
| } else if (res.status === 400 || res.status === 409) { | |
| const body = res.json(); | |
| const code = body?.code || ''; | |
| } else if (res.status === 400 || res.status === 409) { | |
| const body = (() => { try { return res.json(); } catch { return {}; } })(); | |
| const code = body?.code || ''; |
| export default function () { | ||
| const start = Date.now(); | ||
| const storeId = STORE_IDS[Math.floor(Math.random() * STORE_IDS.length)]; | ||
| const today = new Date().toISOString().split('T')[0]; |
There was a problem hiding this comment.
new Date().toISOString()는 UTC 기준 시간을 반환하므로, 한국 시간(KST) 기준으로 오전 9시 이전에 테스트를 실행하면 어제 날짜가 생성됩니다. 이로 인해 과거 날짜에 대한 잔여석 조회가 요청되어 API가 실패할 수 있으므로, KST 기준의 오늘 날짜를 생성하도록 수정하는 것이 좋습니다.
| const today = new Date().toISOString().split('T')[0]; | |
| const today = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().split('T')[0]; |
| group('챗봇 메시지 전송', () => { | ||
| const res = http.post(`${BASE_URL}/api/v1/chat/messages`, payload, { | ||
| headers: HEADERS_AUTH, | ||
| timeout: '65s', // LLM 응답 최대 대기 | ||
| }); |
There was a problem hiding this comment.
k6의 http 요청 옵션에서 timeout은 밀리초(ms) 단위의 숫자(number) 타입을 기대합니다. '65s'와 같은 문자열을 지정하면 올바르게 인식되지 않고 기본값(60초)으로 동작하거나 예외가 발생할 수 있습니다. LLM 응답 대기를 위해 의도한 65초를 밀리초 단위 숫자로 명시해 주세요.
| group('챗봇 메시지 전송', () => { | |
| const res = http.post(`${BASE_URL}/api/v1/chat/messages`, payload, { | |
| headers: HEADERS_AUTH, | |
| timeout: '65s', // LLM 응답 최대 대기 | |
| }); | |
| group('챗봇 메시지 전송', () => { | |
| const res = http.post(BASE_URL + '/api/v1/chat/messages', payload, { | |
| headers: HEADERS_AUTH, | |
| timeout: 65000, // LLM 응답 최대 대기 (65초) | |
| }); |
| group(`근처 매장 조회 (PostGIS nearby) - ${loc.name}`, () => { | ||
| const res = http.get( | ||
| `${BASE_URL}/api/v1/stores/nearby?latitude=${lat}&longitude=${lng}&page=0&size=10`, | ||
| { headers: HEADERS_JSON, timeout: '15s' }, | ||
| ); |
There was a problem hiding this comment.
k6의 http 요청 옵션에서 timeout은 밀리초(ms) 단위의 숫자(number) 타입을 기대합니다. '15s'와 같은 문자열은 올바르게 인식되지 않으므로, 의도한 15초를 밀리초 단위 숫자인 15000으로 수정해야 합니다.
| group(`근처 매장 조회 (PostGIS nearby) - ${loc.name}`, () => { | |
| const res = http.get( | |
| `${BASE_URL}/api/v1/stores/nearby?latitude=${lat}&longitude=${lng}&page=0&size=10`, | |
| { headers: HEADERS_JSON, timeout: '15s' }, | |
| ); | |
| group('근처 매장 조회 (PostGIS nearby) - ' + loc.name, () => { | |
| const res = http.get( | |
| BASE_URL + '/api/v1/stores/nearby?latitude=' + lat + '&longitude=' + lng + '&page=0&size=10', | |
| { headers: HEADERS_JSON, timeout: 15000 }, | |
| ); |
| `&minLng=${loc.minLng}&maxLng=${loc.maxLng}` + | ||
| `¢erLat=${lat}¢erLng=${lng}&limit=50`; | ||
|
|
||
| const res = http.get(url, { headers: HEADERS_JSON, timeout: '15s' }); |
There was a problem hiding this comment.
| // 오늘 포함 향후 7일 중 랜덤 날짜 — 캐시 히트 방지 + 실제 사용 패턴 재현 | ||
| const today = new Date(); | ||
| const dayOffset = Math.floor(Math.random() * 7); // 0~6일 후 | ||
| today.setDate(today.getDate() + dayOffset); | ||
| const date = today.toISOString().split('T')[0]; |
There was a problem hiding this comment.
new Date() 객체를 생성한 뒤 toISOString()을 호출하면 UTC 기준 시간이 사용됩니다. 한국 시간(KST) 기준으로 오전 9시 이전에 테스트를 실행하고 dayOffset이 0으로 결정되면, date가 어제 날짜로 설정되어 과거 날짜에 대한 잔여석 조회가 요청됩니다. 이는 API 실패나 잘못된 테스트 결과를 초래할 수 있으므로 KST 기준의 날짜를 계산하도록 수정하는 것이 좋습니다.
| // 오늘 포함 향후 7일 중 랜덤 날짜 — 캐시 히트 방지 + 실제 사용 패턴 재현 | |
| const today = new Date(); | |
| const dayOffset = Math.floor(Math.random() * 7); // 0~6일 후 | |
| today.setDate(today.getDate() + dayOffset); | |
| const date = today.toISOString().split('T')[0]; | |
| // 오늘 포함 향후 7일 중 랜덤 날짜 — 캐시 히트 방지 + 실제 사용 패턴 재현 | |
| const today = new Date(Date.now() + 9 * 60 * 60 * 1000); | |
| const dayOffset = Math.floor(Math.random() * 7); // 0~6일 후 | |
| today.setDate(today.getDate() + dayOffset); | |
| const date = today.toISOString().split('T')[0]; |
|
|
||
| export default function () { | ||
| const storeId = STORE_IDS[Math.floor(Math.random() * STORE_IDS.length)]; | ||
| const today = new Date().toISOString().split('T')[0]; |
There was a problem hiding this comment.
new Date().toISOString()는 UTC 기준 시간을 반환하므로, 한국 시간(KST) 기준으로 오전 9시 이전에 테스트를 실행하면 어제 날짜가 생성됩니다. 이로 인해 과거 날짜에 대한 잔여석 조회가 요청되어 API가 실패할 수 있으므로, KST 기준의 오늘 날짜를 생성하도록 수정하는 것이 좋습니다.
| const today = new Date().toISOString().split('T')[0]; | |
| const today = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().split('T')[0]; |
Enum 매핑 수정 Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Enum 매핑 수정 Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
- timeout 문자열을 ms 숫자로 수정 (65s→65000, 15s→15000) - UTC→KST 날짜 변환 적용 (01, 03, 07, 10번 스크립트) - res.json() try-catch 안전 처리 (02번 - 502/503 HTML 응답 대비) - category 필터 한글→영문 Enum 값으로 수정 (03번) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
k6 시나리오 테스트: 매장 탐색, 예약 동시성, 전체 플로우, 서킷브레이커
k6 단일 API 테스트: 매장 목록, PostGIS 지리 쿼리, 잔여석 조회, 쿠폰 선착순 발급 k6 내구성 테스트: 10VU x 30분 Soak 테스트 (메모리 누수, 커넥션풀 고갈 탐지) run.ps1: 테스트 번호로 실행하는 래퍼 스크립트 (01~08, 10)
Grafana 백엔드 모니터링 대시보드 JSON (수동 import용)
📢 기능 설명
필요시 실행결과 스크린샷 첨부
연결된 issue
연결된 issue를 자동을 닫기 위해 아래 {이슈넘버}를 입력해주세요.
close #87
✅ 체크리스트