Skip to content

Commit afa84c2

Browse files
authored
feat: order도메인 성능/부하테스트 시나리오 작성
* test: 좌석 선택-주문생성 테스트 시나리오작성 * test: 좌석 선택-주문생성 한번 시행하는 테스트 시나리오작성
1 parent 3f4f6fe commit afa84c2

3 files changed

Lines changed: 346 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import http from "k6/http";
2+
import { check } from "k6";
3+
4+
/**
5+
* POST /api/v1/order
6+
* 주문 생성 시나리오 (인증 필요)
7+
*
8+
* @param {string} baseUrl - API 기본 주소
9+
* @param {string} jwt - 사용자 JWT 토큰
10+
* @param {string} testId - 테스트 실행 ID (로그/메트릭 태깅용)
11+
* @param {number} eventId - 이벤트 ID
12+
* @param {number} seatId - 좌석 ID
13+
* @param {number} amount - 주문 총액
14+
*/
15+
export function createOrder(baseUrl, jwt, testId, eventId, seatId, amount) {
16+
const url = `${baseUrl}/api/v1/order`;
17+
18+
const payload = JSON.stringify({
19+
amount: amount,
20+
eventId: eventId,
21+
seatId: seatId,
22+
});
23+
24+
const params = {
25+
headers: {
26+
Authorization: `Bearer ${jwt}`,
27+
Accept: "application/json",
28+
"Content-Type": "application/json",
29+
},
30+
tags: {
31+
api: "createOrder",
32+
test_id: testId,
33+
event_id: eventId,
34+
seat_id: seatId,
35+
},
36+
};
37+
38+
const res = http.post(url, payload, params);
39+
40+
let json;
41+
try {
42+
json = res.json();
43+
} catch {
44+
console.error("❌ JSON parse error:", res.body);
45+
return res;
46+
}
47+
48+
// 서버 응답 구조: { message: string, data: OrderResponseDto }
49+
const data = json?.data ?? null;
50+
51+
check(res, {
52+
"status 200": (r) => r.status === 200,
53+
"data exists": () => data !== null,
54+
"has orderId": () => typeof data?.orderId === "number",
55+
"has orderKey": () => typeof data?.orderKey === "string",
56+
"has ticketId": () => typeof data?.ticketId === "number",
57+
"has amount": () => typeof data?.amount === "number",
58+
"amount matches": () => data?.amount === amount,
59+
});
60+
61+
if (res.status !== 200) {
62+
console.error(`❌ createOrder failed [${res.status}]:`, JSON.stringify({
63+
status: res.status,
64+
message: json?.message,
65+
eventId,
66+
seatId,
67+
amount,
68+
}));
69+
} else {
70+
// 성공 시 주문 정보 로깅 (디버깅용)
71+
if (__ENV.DEBUG === "true") {
72+
console.log(`✅ Order created: orderId=${data?.orderId}, orderKey=${data?.orderKey}, ticketId=${data?.ticketId}, amount=${data?.amount}`);
73+
}
74+
}
75+
76+
return res;
77+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { selectSeat } from "../scenarios/selectSeat.js";
2+
import { createOrder } from "../scenarios/createOrder.js";
3+
import { generateJWT } from "../util/jwt.js";
4+
import { sleep } from "k6";
5+
6+
export const options = {
7+
stages: [
8+
{ duration: "10s", target: parseInt(__ENV.RAMP_UP_VUS || "50", 10) },
9+
{ duration: "1m", target: parseInt(__ENV.RAMP_UP_VUS || "50", 10) },
10+
{ duration: "10s", target: parseInt(__ENV.PEAK_VUS || "100", 10) },
11+
{ duration: "1m", target: parseInt(__ENV.PEAK_VUS || "100", 10) },
12+
{ duration: "1m", target: 0 },
13+
],
14+
};
15+
16+
/**
17+
* 좌석 선택 + 주문 생성 통합 부하 테스트
18+
*
19+
* 목적:
20+
* - 실제 티켓팅 플로우를 시뮬레이션 (좌석 선택 → 주문 생성)
21+
* - 두 API의 연계 동작 성능 측정
22+
* - 전체 트랜잭션 처리 시간 확인
23+
*
24+
* 테스트 데이터 구조:
25+
* - Event #3 (뉴진스 2025 콘서트 - 서울, OPEN 상태)
26+
* - 총 좌석: 500석
27+
* - VIP (A1~A50, 50석): 165,000원
28+
* - R (C1~C100, 100석): 145,000원
29+
* - S (B1~B150, 150석): 129,000원
30+
* - A (D1~D200, 200석): 99,000원
31+
*
32+
* 시나리오:
33+
* - VU별로 고유한 좌석 할당 (경합 없음 - Baseline)
34+
* - VU 1 → 좌석 1 (VIP), VU 2 → 좌석 2 (VIP), ...
35+
* - VU 51 → 좌석 51 (R), VU 152 → 좌석 152 (S), ...
36+
* - 각 VU는 다음 플로우를 수행:
37+
* 1. 좌석 선택 (selectSeat)
38+
* 2. 선택 성공 시 주문 생성 (createOrder)
39+
* 3. 주문 생성 성공 시 완료
40+
*
41+
* 특징:
42+
* - Non-Competitive 테스트 (좌석 중복 없음)
43+
* - 실제 사용자 플로우 재현
44+
* - 두 API의 순차 호출 성능 측정
45+
* - DB 트랜잭션 연계 처리 확인
46+
*
47+
* 주의:
48+
* - PEAK_VUS는 500 이하로 설정 권장 (좌석이 500개만 존재)
49+
* - 500 초과 시 좌석이 순환되어 중복 가능
50+
* - selectSeat 실패 시 createOrder를 호출하지 않음
51+
*/
52+
export function setup() {
53+
const secret = __ENV.JWT_SECRET;
54+
if (!secret) {
55+
throw new Error("JWT_SECRET 환경변수가 필요합니다.");
56+
}
57+
58+
const maxVus = Math.max(
59+
parseInt(__ENV.RAMP_UP_VUS || "50", 10),
60+
parseInt(__ENV.PEAK_VUS || "100", 10)
61+
);
62+
63+
// VU당 JWT 토큰 생성
64+
const tokens = Array.from({ length: maxVus }, (_, i) => {
65+
const userId = i + 1;
66+
return generateJWT(
67+
{
68+
id: userId,
69+
email: `test${userId}@test.com`,
70+
nickname: `PerfUser${userId}`
71+
},
72+
secret
73+
);
74+
});
75+
76+
console.log(`Testing with ${maxVus} VUs`);
77+
console.log(`좌석 선택 + 주문 생성 통합 테스트:`);
78+
console.log(` - Event #3 (뉴진스 2025 콘서트 - 서울)`);
79+
console.log(` - 총 500석 (VIP 50, R 100, S 150, A 200)`);
80+
console.log(` - VU별 고유 좌석 할당 (경합 없음)`);
81+
82+
if (maxVus > 500) {
83+
console.warn(`⚠️ PEAK_VUS(${maxVus})가 좌석 수(500)보다 큽니다.`);
84+
console.warn(` 좌석 ID가 순환됩니다.`);
85+
}
86+
87+
return {
88+
tokens,
89+
testId: new Date().toISOString().replace(/[:.]/g, "-"),
90+
};
91+
}
92+
93+
/**
94+
* Event #3의 좌석 ID로 가격 계산
95+
* - 좌석 1~50: VIP 165,000원
96+
* - 좌석 51~150: R 145,000원
97+
* - 좌석 151~300: S 129,000원
98+
* - 좌석 301~500: A 99,000원
99+
*/
100+
function getSeatPrice(seatId) {
101+
if (seatId >= 1 && seatId <= 50) {
102+
return 165000; // VIP (A1~A50)
103+
} else if (seatId >= 51 && seatId <= 150) {
104+
return 145000; // R (C1~C100)
105+
} else if (seatId >= 151 && seatId <= 300) {
106+
return 129000; // S (B1~B150)
107+
} else if (seatId >= 301 && seatId <= 500) {
108+
return 99000; // A (D1~D200)
109+
}
110+
// 기본값 (범위 밖이면 최저가)
111+
return 99000;
112+
}
113+
114+
export default function (data) {
115+
const baseUrl = __ENV.BASE_URL || "http://host.docker.internal:8080";
116+
117+
// VU별 JWT 토큰 사용
118+
const jwt = data.tokens[(__VU - 1) % data.tokens.length];
119+
120+
// Event #3 (OPEN 상태, 500석)
121+
const eventId = 3;
122+
123+
// VU별 고유 좌석 할당 (경합 없음)
124+
// VU 1 → 좌석 1, VU 2 → 좌석 2, ..., VU 500 → 좌석 500
125+
const totalSeats = 500;
126+
const seatId = ((__VU - 1) % totalSeats) + 1;
127+
128+
// 좌석 ID에 따른 가격 계산
129+
const amount = getSeatPrice(seatId);
130+
131+
// 1. 좌석 선택
132+
const selectRes = selectSeat(baseUrl, jwt, data.testId, eventId, seatId);
133+
134+
// 좌석 선택 성공 시에만 주문 생성
135+
if (selectRes.status === 200) {
136+
// 사용자가 좌석 선택 후 주문 버튼을 누르기까지의 시간 (0.3~1.0초)
137+
sleep(Math.random() * 0.7 + 0.3);
138+
139+
// 2. 주문 생성
140+
createOrder(baseUrl, jwt, data.testId, eventId, seatId, amount);
141+
} else {
142+
// 좌석 선택 실패 시 로깅
143+
if (__ENV.DEBUG === "true") {
144+
console.log(`⚠️ Seat selection failed for seatId=${seatId}, skipping order creation`);
145+
}
146+
}
147+
148+
// 사용자가 다음 행동을 하기까지의 대기 시간 (0.5~2.0초)
149+
sleep(Math.random() * 1.5 + 0.5);
150+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { selectSeat } from "../scenarios/selectSeat.js";
2+
import { createOrder } from "../scenarios/createOrder.js";
3+
import { generateJWT } from "../util/jwt.js";
4+
5+
export const options = {
6+
// ✅ 각 VU가 1번만 실행하도록 설정
7+
iterations: parseInt(__ENV.VUS || "100", 10),
8+
vus: parseInt(__ENV.VUS || "100", 10),
9+
};
10+
11+
/**
12+
* 좌석 선택 + 주문 생성 통합 부하 테스트 (1회 실행 버전)
13+
*
14+
* 목적:
15+
* - 실제 티켓팅 플로우를 시뮬레이션 (좌석 선택 → 주문 생성)
16+
* - 백엔드 멱등성 수정 전 임시 테스트용
17+
* - 각 VU가 정확히 1번만 실행
18+
*
19+
* 테스트 데이터 구조:
20+
* - Event #3 (뉴진스 2025 콘서트 - 서울, OPEN 상태)
21+
* - 총 좌석: 500석
22+
* - VIP (A1~A50, 50석): 165,000원
23+
* - R (C1~C100, 100석): 145,000원
24+
* - S (B1~B150, 150석): 129,000원
25+
* - A (D1~D200, 200석): 99,000원
26+
*
27+
* 시나리오:
28+
* - VU별로 고유한 좌석 할당 (경합 없음)
29+
* - 각 VU는 1번만 실행 (반복 없음)
30+
* - VU 1 → 좌석 1 (VIP), VU 2 → 좌석 2 (VIP), ...
31+
*
32+
* 주의:
33+
* - VUS는 500 이하로 설정 권장 (좌석이 500개만 존재)
34+
* - 백엔드의 멱등성 수정 후에는 selectSeatAndOrder.test.js 사용
35+
*/
36+
export function setup() {
37+
const secret = __ENV.JWT_SECRET;
38+
if (!secret) {
39+
throw new Error("JWT_SECRET 환경변수가 필요합니다.");
40+
}
41+
42+
const vus = parseInt(__ENV.VUS || "100", 10);
43+
44+
// VU당 JWT 토큰 생성
45+
const tokens = Array.from({ length: vus }, (_, i) => {
46+
const userId = i + 1;
47+
return generateJWT(
48+
{
49+
id: userId,
50+
email: `test${userId}@test.com`,
51+
nickname: `PerfUser${userId}`
52+
},
53+
secret
54+
);
55+
});
56+
57+
console.log(`Testing with ${vus} VUs (1 iteration each)`);
58+
console.log(`좌석 선택 + 주문 생성 통합 테스트 (1회 실행):`);
59+
console.log(` - Event #3 (뉴진스 2025 콘서트 - 서울)`);
60+
console.log(` - 총 500석 (VIP 50, R 100, S 150, A 200)`);
61+
console.log(` - VU별 고유 좌석 할당 (경합 없음)`);
62+
console.log(` - ⚠️ 백엔드 멱등성 수정 전 임시 버전`);
63+
64+
if (vus > 500) {
65+
console.warn(`⚠️ VUS(${vus})가 좌석 수(500)보다 큽니다.`);
66+
console.warn(` 좌석 ID가 순환됩니다.`);
67+
}
68+
69+
return {
70+
tokens,
71+
testId: new Date().toISOString().replace(/[:.]/g, "-"),
72+
};
73+
}
74+
75+
/**
76+
* Event #3의 좌석 ID로 가격 계산
77+
*/
78+
function getSeatPrice(seatId) {
79+
if (seatId >= 1 && seatId <= 50) {
80+
return 165000; // VIP (A1~A50)
81+
} else if (seatId >= 51 && seatId <= 150) {
82+
return 145000; // R (C1~C100)
83+
} else if (seatId >= 151 && seatId <= 300) {
84+
return 129000; // S (B1~B150)
85+
} else if (seatId >= 301 && seatId <= 500) {
86+
return 99000; // A (D1~D200)
87+
}
88+
return 99000;
89+
}
90+
91+
export default function (data) {
92+
const baseUrl = __ENV.BASE_URL || "http://host.docker.internal:8080";
93+
94+
// VU별 JWT 토큰 사용
95+
const jwt = data.tokens[(__VU - 1) % data.tokens.length];
96+
97+
// Event #3 (OPEN 상태, 500석)
98+
const eventId = 3;
99+
100+
// VU별 고유 좌석 할당
101+
const totalSeats = 500;
102+
const seatId = ((__VU - 1) % totalSeats) + 1;
103+
104+
// 좌석 ID에 따른 가격 계산
105+
const amount = getSeatPrice(seatId);
106+
107+
// 1. 좌석 선택
108+
const selectRes = selectSeat(baseUrl, jwt, data.testId, eventId, seatId);
109+
110+
// 좌석 선택 성공 시에만 주문 생성
111+
if (selectRes.status === 200) {
112+
// 2. 주문 생성
113+
createOrder(baseUrl, jwt, data.testId, eventId, seatId, amount);
114+
} else {
115+
if (__ENV.DEBUG === "true") {
116+
console.log(`⚠️ Seat selection failed for seatId=${seatId}, skipping order creation`);
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)