1+ import http from 'k6/http' ;
2+ import { check , sleep } from 'k6' ;
3+ import { Rate , Trend } from 'k6/metrics' ;
4+
5+ // 커스텀 메트릭
6+ const errorRate = new Rate ( 'errors' ) ;
7+ const searchDuration = new Trend ( 'search_duration' ) ;
8+
9+ // 테스트 설정
10+ export const options = {
11+ stages : [
12+ { duration : '30s' , target : 10 } , // 워밍업: 10명
13+ { duration : '1m' , target : 30 } , // 30명으로 증가
14+ { duration : '2m' , target : 30 } , // 30명 유지
15+ { duration : '30s' , target : 0 } , // 종료
16+ ] ,
17+ thresholds : {
18+ 'http_req_duration' : [ 'p(95)<1000' , 'p(99)<2000' ] ,
19+ 'errors' : [ 'rate<0.05' ] ,
20+ } ,
21+ summaryTrendStats : [ 'avg' , 'min' , 'med' , 'max' , 'p(90)' , 'p(95)' , 'p(99)' ] ,
22+ } ;
23+
24+ const BASE_URL = 'http://host.docker.internal:8080' ; // Docker에서 로컬 접근
25+
26+ // 다양한 검색 시나리오
27+ const scenarios = [
28+ { name : '키워드_일반' , params : `keyword=${ encodeURIComponent ( '아이폰' ) } ` } ,
29+ { name : '키워드_희소' , params : `keyword=${ encodeURIComponent ( 'MacBook Pro M3' ) } ` } ,
30+ { name : '카테고리' , params : 'category=1' } ,
31+ { name : '지역' , params : `location=${ encodeURIComponent ( '서울' ) } ` } ,
32+ { name : '복합검색' , params : `keyword=${ encodeURIComponent ( '아이폰' ) } &category=1&location=${ encodeURIComponent ( '서울' ) } ` } ,
33+ { name : '페이징' , params : 'page=5&size=20' } ,
34+ ] ;
35+
36+ export default function ( ) {
37+ // 랜덤하게 시나리오 선택
38+ const scenario = scenarios [ Math . floor ( Math . random ( ) * scenarios . length ) ] ;
39+ const url = `${ BASE_URL } /api/v1/products?${ scenario . params } ` ;
40+
41+ const startTime = new Date ( ) ;
42+ const response = http . get ( url , {
43+ tags : { scenario : scenario . name } ,
44+ timeout : '10s' ,
45+ } ) ;
46+ const duration = new Date ( ) - startTime ;
47+
48+ // 400 에러 상세 정보 출력
49+ if ( response . status === 400 ) {
50+ console . log ( `\n❌ 400 에러 상세 정보:` ) ;
51+ console . log ( `시나리오: ${ scenario . name } ` ) ;
52+ console . log ( `URL: ${ url } ` ) ;
53+ console . log ( `응답 본문: ${ response . body } ` ) ;
54+ console . log ( `응답 헤더: ${ JSON . stringify ( response . headers ) } \n` ) ;
55+ }
56+
57+ // 응답 검증
58+ const success = check ( response , {
59+ '상태 200' : ( r ) => r . status === 200 ,
60+ '응답시간 < 2초' : ( r ) => r . timings . duration < 2000 ,
61+ '데이터 존재' : ( r ) => {
62+ try {
63+ const body = JSON . parse ( r . body ) ;
64+ return body . data && body . data . content !== undefined ;
65+ } catch {
66+ return false ;
67+ }
68+ } ,
69+ } ) ;
70+
71+ if ( ! success ) {
72+ console . log ( `❌ 실패: ${ scenario . name } - Status: ${ response . status } ` ) ;
73+ }
74+
75+ // 메트릭 기록
76+ errorRate . add ( ! success ) ;
77+ searchDuration . add ( duration ) ;
78+
79+ // 사용자 행동 시뮬레이션
80+ sleep ( Math . random ( ) * 2 + 1 ) ; // 1~3초 대기
81+ }
82+
83+ // 테스트 종료 후 요약
84+ export function handleSummary ( data ) {
85+ const timestamp = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' ) . slice ( 0 , - 5 ) ;
86+
87+ const duration = data . metrics . http_req_duration ?. values || { } ;
88+ const reqs = data . metrics . http_reqs ?. values || { } ;
89+ const errors = data . metrics . errors ?. values || { } ;
90+
91+ console . log ( '\n=== 현재 상태 성능 테스트 결과 ===' ) ;
92+ console . log ( `평균 응답시간: ${ duration . avg ?. toFixed ( 2 ) || 'N/A' } ms` ) ;
93+ console . log ( `P95 응답시간: ${ duration [ 'p(95)' ] ?. toFixed ( 2 ) || 'N/A' } ms` ) ;
94+ console . log ( `P99 응답시간: ${ duration [ 'p(99)' ] ?. toFixed ( 2 ) || 'N/A' } ms` ) ;
95+ console . log ( `최대 응답시간: ${ duration . max ?. toFixed ( 2 ) || 'N/A' } ms` ) ;
96+ console . log ( `총 요청 수: ${ reqs . count || 0 } ` ) ;
97+ console . log ( `에러율: ${ errors . rate ? ( errors . rate * 100 ) . toFixed ( 2 ) : '0.00' } %` ) ;
98+
99+ return {
100+ 'stdout' : JSON . stringify ( data , null , 2 ) ,
101+ [ `results/getProducts-test-${ timestamp } .json` ] : JSON . stringify ( data , null , 2 ) ,
102+ 'results/getProducts-test-latest.json' : JSON . stringify ( data , null , 2 ) , // 항상 최신 결과
103+ } ;
104+ }
0 commit comments