From 472f5695ebfc0a4bf2108db6709dc8f05d756011 Mon Sep 17 00:00:00 2001 From: kanghana1 Date: Sat, 3 Oct 2026 17:24:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20Terraform=EC=97=90=20loadtest=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8C=8C=EC=9D=BC=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 부하테스트 시 EC2를 헤더 기반 인증(loadtest 프로파일)으로 띄우기 위해 SPRING_PROFILES_ACTIVE를 변수화하여 .env로 주입. - variables.tf: spring_profiles_active (기본 prod) - main.tf: env_content에 SPRING_PROFILES_ACTIVE 추가 - tfvars.example: prod,loadtest 토글 안내 --- terraform/main.tf | 1 + terraform/terraform.tfvars.example | 3 +++ terraform/variables.tf | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/terraform/main.tf b/terraform/main.tf index 926e928..a14eb3e 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -9,6 +9,7 @@ locals { # EC2에 전달할 .env 내용 env_content = <<-EOT DOCKER_USERNAME=${var.docker_username} + SPRING_PROFILES_ACTIVE=${var.spring_profiles_active} MYSQL_ROOT_PASSWORD=${var.mysql_root_password} DB_NAME=${var.db_name} DB_USERNAME=${var.db_username} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example index 2ae0621..7ec3104 100644 --- a/terraform/terraform.tfvars.example +++ b/terraform/terraform.tfvars.example @@ -7,5 +7,8 @@ db_username = "course" mysql_root_password = "change-me-root-password" db_password = "change-me-app-password" +# Spring 프로파일 (평소 prod / 부하테스트 시 prod,loadtest 로 변경하면 헤더 기반 userId 활성화) +# spring_profiles_active = "prod,loadtest" + # SSH 허용 IP를 직접 지정하려면 주석 해제 (비우면 apply 시 현재 공인 IP 자동 감지) # ssh_ingress_cidr = "1.2.3.4/32" diff --git a/terraform/variables.tf b/terraform/variables.tf index a6ad36a..7f1e870 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -42,6 +42,12 @@ variable "docker_username" { type = string } +variable "spring_profiles_active" { + description = "Spring 프로파일 (평소 prod / 부하테스트 시 prod,loadtest)" + type = string + default = "prod" +} + variable "db_name" { description = "MySQL 데이터베이스명" type = string From d2e2e283d2c519e3a4e8d935bdf69259aa2489f8 Mon Sep 17 00:00:00 2001 From: kanghana1 Date: Sat, 3 Oct 2026 17:24:11 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20k6=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=EC=9A=94=EC=95=BD(handleSummary)=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B2=B0=EA=B3=BC=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 터미널에 전체 요약(p95/처리량) + 핵심 지표(성공률/정원 정합성) 강조 출력 - 실행 결과를 loadtest/results/*.json 으로 저장 (단일 vs 다중 비교용) - results 덤프는 .gitignore 처리 (필요한 것만 수동 add) --- .gitignore | 4 +++- loadtest/capacity-contention.js | 27 +++++++++++++++++++++++++++ loadtest/results/.gitkeep | 0 loadtest/throughput-baseline.js | 29 +++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 loadtest/results/.gitkeep diff --git a/.gitignore b/.gitignore index abf3587..34c86d5 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,6 @@ docker-compose.yml !*.tfvars.example crash.log override.tf -override.tf.json \ No newline at end of file +override.tf.json +### k6 결과 덤프 ### +loadtest/results/*.json diff --git a/loadtest/capacity-contention.js b/loadtest/capacity-contention.js index 679e57e..c457fca 100644 --- a/loadtest/capacity-contention.js +++ b/loadtest/capacity-contention.js @@ -10,6 +10,7 @@ import http from 'k6/http'; import { sleep } from 'k6'; import { Counter } from 'k6/metrics'; +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js'; const BASE_URL = __ENV.BASE_URL || 'http://localhost'; const HOT_SUBJECTS = parseInt(__ENV.HOT_SUBJECTS || '1'); // 경쟁 대상 과목 수 (1 또는 2) @@ -69,3 +70,29 @@ function formHeaders(userId) { return { headers: { userId: String(userId), 'Content-Type': 'application/x-www-form-urlencoded' } }; } function safeJson(res) { try { return res.json(); } catch (e) { return null; } } + +// 터미널엔 전체 요약 + 정합성 지표 강조, 파일엔 JSON 저장(비교용) +export function handleSummary(data) { + const ts = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19); + return { + stdout: textSummary(data, { indent: ' ', enableColors: true }) + highlight(data), + [`loadtest/results/capacity-contention-${ts}.json`]: JSON.stringify(data, null, 2), + }; +} +function highlight(data) { + const m = data.metrics; + const cnt = (k) => (m[k] ? m[k].values.count : 0); + const hot = parseInt(__ENV.HOT_SUBJECTS || '1'); + const p95 = m.http_req_duration ? m.http_req_duration.values['p(95)'] : 0; + const failed = m.http_req_failed ? (m.http_req_failed.values.rate * 100).toFixed(2) : '0.00'; + return [ + '\n========== 정합성 검증 (경합) ==========', + `신청 성공(정원 내) : ${cnt('enroll_success')} (기대: 30 x ${hot} = ${30 * hot})`, + `정원 초과 거절 : ${cnt('capacity_full')}`, + `기타 거절 : ${cnt('other_reject')}`, + `응답시간 p95 : ${p95.toFixed(0)} ms`, + `HTTP 실패율 : ${failed} %`, + '>>> 각 과목 registeredNum <= 30 인지 서버에서 추가 확인', + '=======================================\n', + ].join('\n'); +} diff --git a/loadtest/results/.gitkeep b/loadtest/results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/throughput-baseline.js b/loadtest/throughput-baseline.js index a438a44..2fb367c 100644 --- a/loadtest/throughput-baseline.js +++ b/loadtest/throughput-baseline.js @@ -8,6 +8,7 @@ import http from 'k6/http'; import { sleep } from 'k6'; import { Counter } from 'k6/metrics'; +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js'; const BASE_URL = __ENV.BASE_URL || 'http://localhost'; const SUBJECT_COUNT = parseInt(__ENV.SUBJECT_COUNT || '30'); // 시드된 과목 수 @@ -75,3 +76,31 @@ function formHeaders(userId) { } function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function safeJson(res) { try { return res.json(); } catch (e) { return null; } } + +// 터미널엔 전체 요약 + 핵심 지표 강조, 파일엔 JSON 저장(비교용) +export function handleSummary(data) { + const ts = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19); + return { + stdout: textSummary(data, { indent: ' ', enableColors: true }) + highlight(data), + [`loadtest/results/throughput-baseline-${ts}.json`]: JSON.stringify(data, null, 2), + }; +} +function highlight(data) { + const m = data.metrics; + const cnt = (k) => (m[k] ? m[k].values.count : 0); + const succ = cnt('enroll_success'); + const total = succ + cnt('enroll_rejected'); + const rate = total > 0 ? ((succ / total) * 100).toFixed(1) : '0.0'; + const p95 = m.http_req_duration ? m.http_req_duration.values['p(95)'] : 0; + const p99 = m.http_req_duration ? m.http_req_duration.values['p(99)'] : 0; + const rps = m.http_reqs ? m.http_reqs.values.rate : 0; + const failed = m.http_req_failed ? (m.http_req_failed.values.rate * 100).toFixed(2) : '0.00'; + return [ + '\n========== 핵심 지표 (처리량 baseline) ==========', + `신청 성공 / 시도 : ${succ} / ${total} (성공률 ${rate}%)`, + `처리량(http_reqs) : ${rps.toFixed(1)} req/s`, + `응답시간 p95 / p99 : ${p95.toFixed(0)} / ${p99.toFixed(0)} ms`, + `HTTP 실패율 : ${failed} %`, + '================================================\n', + ].join('\n'); +}