Skip to content

Commit 17a83ce

Browse files
okjunghyeonSalinatedCoffeesojunsikTheGreatKang
authored
develop -> main (모니터링 관련) (#186)
* chore: 로컬 환경에서의 모니터링 서비스 설정 추가 (#184) * 모니터링 ec2 구축 및 프로메테우스 그라파나 구동 (#185) * develop -> main ( 강의, 수강신청 ) (#180) * [fix] 대기열 인원 수 확인 로직 변경 테스트 반영 (#179) * feat: Redis 기반 동시성 제어 및 큐 기반 수강신청·취소 비동기 처리 구현 (#176) * test: 현재 동시성 문제가 발생하는지 확인하기 위한 테스트 추가 - 동시에 100명이 신청했을 경우 최대 수강 인원이 30명인 강좌에 몇명이 등록되는지 확인 - 수강신청 내역이 30개가 아닌 100개가 생성되고있음 - 강의 정보에 현재 수강인원이 제대로 갱신되고 있지 않음 - 30명이 채워져야하지만 11, 12명 등 이상한 값으로 채워지고 있음 # Conflicts: # backend/src/main/java/com/WEB4_5_GPT_BE/unihub/domain/enrollment/repository/EnrollmentRepository.java * feat: 수강신청 로그를 추가하여 수강인원이 제대로 증가하지 않는 문제점 파악 - 동시에 수강신청을 보내고 인원을 변경하기 때문에 해당 강좌의 현재수강신청 인원 수가 제대로 증가하지 않고 오히려 적게 나타나는 문제였음 ``` 수강 신청이 완료되었습니다. 학생: 25020004, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020010, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020002, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020005, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020006, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020003, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020001, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020009, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020008, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020007, 강좌: 0 현재 수강인원 변경이 완료되었습니다. 학생: 25020001, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020006, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020004, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020005, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020002, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020007, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020010, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020003, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020008, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020009, 강좌: 1 ``` * refactor : 변경된 회원 도메인에 맞게 테스트 코드 수정 * feat: Redisson을 통한 수강신청 동시성 제어 구현 - 코드별 주석으로 상세 설명 * feat: 동시성 테스트는 프로젝트 테스트에서 진행하지 않도록 @Profile("!test") 수정 - 현재 동시성 로직은 별도의 트랜잭션을 생성하기 때문에 test 코드 상에 transational 을 붙여줄 수 없음 - test 코드에서 @transational 을 붙이지 않으면 테스트 과정에서 수정된 데이터가 롤백되지 못해 다른 테스트에 영향을 줄 수 있음 - 또한 동시성 관련 테스트는 생성해야 하는 회원 데이터 등이 많아 테스트에 부하가 많이 걸림 - 프로젝트 테스트에서 동시성을 검증하지 않고 별도의 툴 (jmeter) 을 통해 외부에서 테스트 할 예정 * feat: 동시성 테스트용 데이터 (4학년 학생 100명 및 신청용 강좌) 생성 # Conflicts: # backend/src/main/java/com/WEB4_5_GPT_BE/unihub/global/init/InitDevData.java * feat: 동시성 문제 해결을 위해 redis의 원자성이 보장되는 AtomicLong 적용 및 Queue를 통한 수강신청 비동기 처리 구현 - Redis RAtomicLong 을 이용해 capacity 및 enrolled 카운터를 원자적으로 관리하고, 수강신청 시 increment하여 동시성 보장 - 수강신청 요청 시 DB 트랜잭션 대신에 Redis 카운터만 업데이트하여 빠른 응답을 제공 - EnrollmentCommand DTO 를 통해 수강신청 정보를 큐(RBlockingQueue<EnrollmentCommand>)에 저장 - EnrollmentCommandConsumer 가 큐에서 명령을 순차적으로 꺼내 EnrollmentCommandHandler 에게 위임 * feat: Redisson을 활용하여 강의 생성 시 강의 수강 인원 및 수용 인원 정보를 Redis에 저장하는 기능 추가 * docs: k6를 활용한 수강신청 동시성 테스트 (100명 동시 신청 시 30명 정상 등록 완료) 파일 추가 - local 테스트 * test: 변경된 로직에 맞게 테스트 코드 수정 * refactor: 잘못 적용되어있는 ConcurrencyGuard 제거 * feat: EnrollmentDuplicateChecker를 구현하여 중복 queue 삽입 (동일 학생이 연속으로 신청하는 경우 (따닥)) 방지 - k6 테스트 스크립트 추가 (한 학생이 동일강좌에 5번 동시에 신청) * docs : 주석 추가 * refactor: EnrollmentAlreadyQueuedException 예외처리를 공용으로 사용할 수 있도록 수정 * feat: 강의 취소의 경우에도 순차적으로 queue를 통해 처리될 수 있도록 수강 신청 로직과 동일하게 구현 * docs: 30명 동시 수강신청 및 동시 수강 취소 k6 테스트 스크립트 추가 * refactor : 수강신청 api 주석 수정 및 수강신청 queue 처리 후 flag를 제거하도록 추가 * refactor : 테스트용 대기열 설정 제거 * test : 테스트 코드 수정 * refactor: 패키지 이동 * feat: 강의 등록, 수정 시 redis에 저장된 Enrolled와 Capacity를 업데이트하도록 추가 * feat: 강의 삭제 시 redis에 저장된 Enrolled와 Capacity를 삭제하도록 구현 * feat: redis counter 관련 로직을 별도의 service로 분리 * feat: 프로젝트 재부팅 시 redis에 저장된 counter 값을 현재 강의 정보와 동기화 하도록 구현 * test: 변경된 로직에 맞게 테스트 코드 수정 * feat: initData 과정에서 강의 생성 시 counter를 redis에 저장하도록 추가 * refactor: 필요없는 주석 제거 * merge: [BUGFIX] 강의 생성시 대학 정보 획득 방식 변경 (#178) * fix: 강의 생성 및 수정 시 인증된 유저 정보도 가져오도록 변경 * fix: 강의 생성 및 수정 시 요청 본문 대신 인증된 유저 정보로부터 대학 정보를 가져오도록 변경 * test: 변경 사항을 테스트에 반영 * fix: 강의 요청 DTO에서 대학 이름을 필수에서 선택으로 변경 * [fix] 대기열 인원 수 확인 로직 변경 테스트 반영 (#179) * feat: Redis 기반 동시성 제어 및 큐 기반 수강신청·취소 비동기 처리 구현 (#176) * test: 현재 동시성 문제가 발생하는지 확인하기 위한 테스트 추가 - 동시에 100명이 신청했을 경우 최대 수강 인원이 30명인 강좌에 몇명이 등록되는지 확인 - 수강신청 내역이 30개가 아닌 100개가 생성되고있음 - 강의 정보에 현재 수강인원이 제대로 갱신되고 있지 않음 - 30명이 채워져야하지만 11, 12명 등 이상한 값으로 채워지고 있음 # Conflicts: # backend/src/main/java/com/WEB4_5_GPT_BE/unihub/domain/enrollment/repository/EnrollmentRepository.java * feat: 수강신청 로그를 추가하여 수강인원이 제대로 증가하지 않는 문제점 파악 - 동시에 수강신청을 보내고 인원을 변경하기 때문에 해당 강좌의 현재수강신청 인원 수가 제대로 증가하지 않고 오히려 적게 나타나는 문제였음 ``` 수강 신청이 완료되었습니다. 학생: 25020004, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020010, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020002, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020005, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020006, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020003, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020001, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020009, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020008, 강좌: 0 수강 신청이 완료되었습니다. 학생: 25020007, 강좌: 0 현재 수강인원 변경이 완료되었습니다. 학생: 25020001, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020006, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020004, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020005, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020002, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020007, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020010, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020003, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020008, 강좌: 1 현재 수강인원 변경이 완료되었습니다. 학생: 25020009, 강좌: 1 ``` * refactor : 변경된 회원 도메인에 맞게 테스트 코드 수정 * feat: Redisson을 통한 수강신청 동시성 제어 구현 - 코드별 주석으로 상세 설명 * feat: 동시성 테스트는 프로젝트 테스트에서 진행하지 않도록 @Profile("!test") 수정 - 현재 동시성 로직은 별도의 트랜잭션을 생성하기 때문에 test 코드 상에 transational 을 붙여줄 수 없음 - test 코드에서 @transational 을 붙이지 않으면 테스트 과정에서 수정된 데이터가 롤백되지 못해 다른 테스트에 영향을 줄 수 있음 - 또한 동시성 관련 테스트는 생성해야 하는 회원 데이터 등이 많아 테스트에 부하가 많이 걸림 - 프로젝트 테스트에서 동시성을 검증하지 않고 별도의 툴 (jmeter) 을 통해 외부에서 테스트 할 예정 * feat: 동시성 테스트용 데이터 (4학년 학생 100명 및 신청용 강좌) 생성 # Conflicts: # backend/src/main/java/com/WEB4_5_GPT_BE/unihub/global/init/InitDevData.java * feat: 동시성 문제 해결을 위해 redis의 원자성이 보장되는 AtomicLong 적용 및 Queue를 통한 수강신청 비동기 처리 구현 - Redis RAtomicLong 을 이용해 capacity 및 enrolled 카운터를 원자적으로 관리하고, 수강신청 시 increment하여 동시성 보장 - 수강신청 요청 시 DB 트랜잭션 대신에 Redis 카운터만 업데이트하여 빠른 응답을 제공 - EnrollmentCommand DTO 를 통해 수강신청 정보를 큐(RBlockingQueue<EnrollmentCommand>)에 저장 - EnrollmentCommandConsumer 가 큐에서 명령을 순차적으로 꺼내 EnrollmentCommandHandler 에게 위임 * feat: Redisson을 활용하여 강의 생성 시 강의 수강 인원 및 수용 인원 정보를 Redis에 저장하는 기능 추가 * docs: k6를 활용한 수강신청 동시성 테스트 (100명 동시 신청 시 30명 정상 등록 완료) 파일 추가 - local 테스트 * test: 변경된 로직에 맞게 테스트 코드 수정 * refactor: 잘못 적용되어있는 ConcurrencyGuard 제거 * feat: EnrollmentDuplicateChecker를 구현하여 중복 queue 삽입 (동일 학생이 연속으로 신청하는 경우 (따닥)) 방지 - k6 테스트 스크립트 추가 (한 학생이 동일강좌에 5번 동시에 신청) * docs : 주석 추가 * refactor: EnrollmentAlreadyQueuedException 예외처리를 공용으로 사용할 수 있도록 수정 * feat: 강의 취소의 경우에도 순차적으로 queue를 통해 처리될 수 있도록 수강 신청 로직과 동일하게 구현 * docs: 30명 동시 수강신청 및 동시 수강 취소 k6 테스트 스크립트 추가 * refactor : 수강신청 api 주석 수정 및 수강신청 queue 처리 후 flag를 제거하도록 추가 * refactor : 테스트용 대기열 설정 제거 * test : 테스트 코드 수정 * refactor: 패키지 이동 * feat: 강의 등록, 수정 시 redis에 저장된 Enrolled와 Capacity를 업데이트하도록 추가 * feat: 강의 삭제 시 redis에 저장된 Enrolled와 Capacity를 삭제하도록 구현 * feat: redis counter 관련 로직을 별도의 service로 분리 * feat: 프로젝트 재부팅 시 redis에 저장된 counter 값을 현재 강의 정보와 동기화 하도록 구현 * test: 변경된 로직에 맞게 테스트 코드 수정 * feat: initData 과정에서 강의 생성 시 counter를 redis에 저장하도록 추가 * refactor: 필요없는 주석 제거 * fix: 강의 생성 및 수정 시 인증된 유저 정보도 가져오도록 변경 * fix: 강의 생성 및 수정 시 요청 본문 대신 인증된 유저 정보로부터 대학 정보를 가져오도록 변경 * merge: Rebase 박주원/fix-177 onto develop * fix: 강의 요청 DTO에서 대학 이름을 필수에서 선택으로 변경 * test: 변경 사항을 테스트에 반영 --------- Co-authored-by: TheGreatKang <77500386+TheGreatKang@users.noreply.github.com> Co-authored-by: OJH <ok6737@naver.com> --------- Co-authored-by: TheGreatKang <77500386+TheGreatKang@users.noreply.github.com> Co-authored-by: OJH <ok6737@naver.com> Co-authored-by: SalinatedCoffee <74612242+SalinatedCoffee@users.noreply.github.com> * docs : 기존 ec2 t3.micro 에서 t3.small로 업그레이드 * docs : 모니터링용 ec2 생성 * feat : prod에 테스트용 100명 데이터 생성 * refactor : 동시성 테스트를 위해 세션 검증로직 임시 주석처리 * docs : 로컬 및 prod 환경 테스트 코드 추가 * refactor: DuplicateChecker를 제거하고 학생 id, 강의 id로 된 분산락을 획득하여 연속 접근을 방지하도록 로직을 수정 * test: DuplicateChecker를 제거 * feat : 스프링 종료 시 redisson도 정상종료 되도록 추가 * docs : 수강신청 날짜 변경 * refactor: test에서도 동작하도록 redisson 설정 수정 * feat : 수강신청 검증 로직이 별도의 트랜잭션을 탈수있도록 EnrollmentValidator 클래스로 분리 * teat: 변경된 로직에 맞게 테스트 수정 * refactor: 대기열 최대 인원 100명으로 증가 - 수강신청 시 세션검증 로직 주석 해제 * docs: prod 테스트 코드 추가 - 100명 동시 신청 * docs: 프로메테우스 설정 추가 * docs: 프로메테우스 설정 추가 및 k6 코드 추가 * refactor : 발표자료에 시각자료로 사용하기 위해 코드 수정 및 리팩토링 * docs : prometheus main ec2 private ip를 참조하도록 수정 * docs : 테스트용 브랜치 제거 --------- Co-authored-by: sojunsik <113578269+sojunsik@users.noreply.github.com> Co-authored-by: TheGreatKang <77500386+TheGreatKang@users.noreply.github.com> Co-authored-by: SalinatedCoffee <74612242+SalinatedCoffee@users.noreply.github.com> --------- Co-authored-by: SalinatedCoffee <74612242+SalinatedCoffee@users.noreply.github.com> Co-authored-by: sojunsik <113578269+sojunsik@users.noreply.github.com> Co-authored-by: TheGreatKang <77500386+TheGreatKang@users.noreply.github.com>
1 parent fa11139 commit 17a83ce

38 files changed

Lines changed: 4884 additions & 680 deletions

backend/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ dependencies {
9090

9191
// Redisson (redis를 통한 분산락)
9292
implementation("org.redisson:redisson-spring-boot-starter:3.46.0")
93+
94+
// prometheus (모니터링툴)
95+
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
96+
9397
}
9498

9599

backend/k6/cancel-cap30-vus100.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import http from "k6/http";
2+
import {SharedArray} from "k6/data";
3+
import {Counter} from "k6/metrics";
4+
5+
// —— 테스트 파라미터 ——
6+
const VUS = 100;
7+
const COURSE_ID = 11;
8+
const BASE = "http://host.docker.internal:8080";
9+
10+
// students.json 로드 (100개 계정)
11+
const users = new SharedArray("students", () =>
12+
JSON.parse(open("students.json"))
13+
);
14+
15+
// 성공/실패 카운터
16+
export let cancelSuccess = new Counter("cancel_success");
17+
export let cancelFail = new Counter("cancel_fail");
18+
export let loginFail = new Counter("login_fail");
19+
20+
export let options = {
21+
scenarios: {
22+
cancelTest: {
23+
executor: "per-vu-iterations",
24+
vus: VUS,
25+
iterations: 1,
26+
maxDuration: "30s",
27+
},
28+
},
29+
thresholds: {
30+
// 100건 중 100건 2xx 기대
31+
"cancel_success": ["count == 30"],
32+
"cancel_fail": ["count == 0"],
33+
},
34+
};
35+
36+
export default function () {
37+
// 각 VU마다 한 번씩 실행
38+
const user = users[(__VU - 1) % users.length];
39+
40+
// 1) 로그인
41+
let loginRes = http.post(
42+
`${BASE}/api/members/login`,
43+
JSON.stringify({email: user.email, password: user.password}),
44+
{headers: {"Content-Type": "application/json"}}
45+
);
46+
if (loginRes.status !== 200) {
47+
loginFail.add(1);
48+
return;
49+
}
50+
const token = loginRes.json("data.accessToken");
51+
52+
// 2) 수강 취소 요청
53+
let res = http.del(
54+
`${BASE}/api/enrollments/${COURSE_ID}`,
55+
null,
56+
{
57+
headers: {
58+
"Authorization": `Bearer ${token}`,
59+
},
60+
}
61+
);
62+
if (res.status >= 200 && res.status < 300) {
63+
cancelSuccess.add(1);
64+
} else {
65+
cancelFail.add(1);
66+
}
67+
}
68+
// 실행 방법
69+
// 1. Docker 환경에서 실행
70+
// 2. c: 경로에 js 파일과 students.json 파일을 저장
71+
// 3. 해당 경로로 이동하여 powershell에서 아래 명령어 실행
72+
// docker run --rm --name k6_1 --network common -v ${pwd}:/scripts -w /scripts grafana/k6:latest run cancel-cap30-vus100.js

backend/k6/enroll-cap30-vus100.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Counter} from "k6/metrics";
66
// —— 테스트 파라미터 ——
77
const VUS = 100;
88
const COURSE_ID = 11;
9-
const BASE = __ENV.BASE || "http://host.docker.internal:8080";
9+
const BASE = "http://host.docker.internal:8080";
1010

1111
// students.json 로드 (100개 계정)
1212
const users = new SharedArray("students", () =>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import http from "k6/http";
2+
import {check} from "k6";
3+
import {SharedArray} from "k6/data";
4+
import {Counter} from "k6/metrics";
5+
6+
// —— 테스트 파라미터 ——
7+
const VUS = 30; // 30명의 학생만
8+
const COURSE_ID = 11;
9+
const BASE = "https://api.un1hub.site";
10+
11+
// students.json 로드 (100개 계정)
12+
const allUsers = new SharedArray("students", () =>
13+
JSON.parse(open("students.json"))
14+
);
15+
16+
// 앞에서부터 30명만 사용
17+
const users = allUsers.slice(0, VUS);
18+
19+
// 성공/실패 카운터
20+
export let enrollSuccess = new Counter("enroll_success");
21+
export let enrollFail = new Counter("enroll_fail");
22+
23+
export let options = {
24+
duration: `180s`,
25+
scenarios: {
26+
enrollmentTest: {
27+
executor: "per-vu-iterations",
28+
vus: VUS,
29+
iterations: 1,
30+
},
31+
},
32+
thresholds: {
33+
// 30명 중 30명은 성공, 0명은 실패 기대
34+
"enroll_success": ["count == 30"],
35+
"enroll_fail": ["count == 0"],
36+
},
37+
};
38+
39+
export default function () {
40+
// VU 번호에 맞춰 0..29 인덱스 사용자 선택
41+
const user = users[__VU - 1];
42+
43+
// 1) 로그인
44+
let loginRes = http.post(
45+
`${BASE}/api/members/login`,
46+
JSON.stringify({email: user.email, password: user.password}),
47+
{headers: {"Content-Type": "application/json"}}
48+
);
49+
check(loginRes, {"login 200": r => r.status === 200});
50+
if (loginRes.status !== 200) {
51+
enrollFail.add(1);
52+
return;
53+
}
54+
const token = loginRes.json("data.accessToken");
55+
56+
// 2) 비동기 수강신청 요청
57+
let res = http.post(
58+
`${BASE}/api/enrollments`,
59+
JSON.stringify({courseId: COURSE_ID}),
60+
{
61+
headers: {
62+
"Content-Type": "application/json",
63+
Authorization: `Bearer ${token}`,
64+
},
65+
}
66+
);
67+
68+
if (res.status >= 200 && res.status < 300) {
69+
enrollSuccess.add(1);
70+
} else {
71+
enrollFail.add(1);
72+
}
73+
}
74+
// cd /home/ec2-user/scripts
75+
/**
76+
docker run --rm \
77+
--name k6_prometheus \
78+
-v "$(pwd)":/scripts \
79+
-w /scripts \
80+
-p 6565:6565 \
81+
-e K6_PROMETHEUS_HOST=0.0.0.0 \
82+
-e K6_PROMETHEUS_PORT=6565 \
83+
grafana/k6:latest run \
84+
--out experimental-prometheus-rw=0.0.0.0:6565 \
85+
30_student_enrollment.js
86+
*/
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import http from "k6/http";
2+
import {check} from "k6";
3+
import {SharedArray} from "k6/data";
4+
import {Counter} from "k6/metrics";
5+
6+
// —— 테스트 파라미터 ——
7+
const VUS = 30; // 30명의 학생
8+
const COURSE_ID = 11;
9+
const BASE = "https://api.un1hub.site";
10+
11+
// students.json 로드 (100개 계정)
12+
const allUsers = new SharedArray("students", () =>
13+
JSON.parse(open("students.json"))
14+
);
15+
16+
// 앞에서부터 30명만 사용
17+
const users = allUsers.slice(0, VUS);
18+
19+
// 성공/실패 카운터
20+
export let cancelSuccess = new Counter("cancel_success");
21+
export let cancelFail = new Counter("cancel_fail");
22+
23+
export let options = {
24+
scenarios: {
25+
cancellationTest: {
26+
executor: "per-vu-iterations",
27+
vus: VUS,
28+
iterations: 1,
29+
maxDuration: "30s",
30+
},
31+
},
32+
thresholds: {
33+
// 30명 중 30건 성공, 0건 실패 기대
34+
"cancel_success": ["count == 30"],
35+
"cancel_fail": ["count == 0"],
36+
},
37+
};
38+
39+
export default function () {
40+
// VU 번호에 맞춰 0..29 인덱스 사용자 선택
41+
const user = users[__VU - 1];
42+
43+
// 1) 로그인
44+
let loginRes = http.post(
45+
`${BASE}/api/members/login`,
46+
JSON.stringify({email: user.email, password: user.password}),
47+
{headers: {"Content-Type": "application/json"}}
48+
);
49+
check(loginRes, {"login 200": r => r.status === 200});
50+
if (loginRes.status !== 200) {
51+
cancelFail.add(1);
52+
return;
53+
}
54+
const token = loginRes.json("data.accessToken");
55+
56+
// 2) 수강 취소 요청
57+
let res = http.del(
58+
`${BASE}/api/enrollments/${COURSE_ID}`,
59+
null,
60+
{
61+
headers: {
62+
Authorization: `Bearer ${token}`,
63+
},
64+
}
65+
);
66+
67+
if (res.status >= 200 && res.status < 300) {
68+
cancelSuccess.add(1);
69+
} else {
70+
cancelFail.add(1);
71+
}
72+
}
73+
74+
// cd /home/ec2-user/scripts
75+
/**
76+
docker run --rm \
77+
--name k6_prometheus \
78+
-v "$(pwd)":/scripts \
79+
-w /scripts \
80+
-p 6565:6565 \
81+
-e K6_PROMETHEUS_HOST=0.0.0.0 \
82+
-e K6_PROMETHEUS_PORT=6565 \
83+
grafana/k6:latest run \
84+
--out experimental-prometheus-rw=0.0.0.0:6565 \
85+
30_student_enrollmentCancel.js
86+
*/
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import http from "k6/http";
2+
import {SharedArray} from "k6/data";
3+
import {Counter} from "k6/metrics";
4+
5+
// —— 테스트 파라미터 ——
6+
const VUS = 100;
7+
const COURSE_ID = 11;
8+
const BASE = "https://api.un1hub.site";
9+
10+
// students.json 로드 (100개 계정)
11+
const users = new SharedArray("students", () =>
12+
JSON.parse(open("students.json"))
13+
);
14+
15+
// 성공/실패 카운터
16+
export let cancelSuccess = new Counter("cancel_success");
17+
export let cancelFail = new Counter("cancel_fail");
18+
export let loginFail = new Counter("login_fail");
19+
20+
export let options = {
21+
scenarios: {
22+
cancelTest: {
23+
executor: "per-vu-iterations",
24+
vus: VUS,
25+
iterations: 1,
26+
maxDuration: "30s",
27+
},
28+
},
29+
thresholds: {
30+
// 100건 중 100건 2xx 기대
31+
"cancel_success": ["count == 30"],
32+
"cancel_fail": ["count == 0"],
33+
},
34+
};
35+
36+
export default function () {
37+
// 각 VU마다 한 번씩 실행
38+
const user = users[(__VU - 1) % users.length];
39+
40+
// 1) 로그인
41+
let loginRes = http.post(
42+
`${BASE}/api/members/login`,
43+
JSON.stringify({email: user.email, password: user.password}),
44+
{headers: {"Content-Type": "application/json"}}
45+
);
46+
if (loginRes.status !== 200) {
47+
loginFail.add(1);
48+
return;
49+
}
50+
const token = loginRes.json("data.accessToken");
51+
52+
// 2) 수강 취소 요청
53+
let res = http.del(
54+
`${BASE}/api/enrollments/${COURSE_ID}`,
55+
null,
56+
{
57+
headers: {
58+
"Authorization": `Bearer ${token}`,
59+
},
60+
}
61+
);
62+
if (res.status >= 200 && res.status < 300) {
63+
cancelSuccess.add(1);
64+
} else {
65+
cancelFail.add(1);
66+
}
67+
}
68+
69+
// cd /home/ec2-user/scripts
70+
/**
71+
docker run --rm \
72+
--name k6_prometheus \
73+
--network common \
74+
-v "$(pwd)":/scripts \
75+
-w /scripts \
76+
-p 6565:6565 \
77+
-e K6_PROMETHEUS_HOST=0.0.0.0 \
78+
-e K6_PROMETHEUS_PORT=6565 \
79+
grafana/k6:latest run \
80+
--out experimental-prometheus-rw=0.0.0.0:6565 \
81+
cancel-cap30-vus100.js
82+
*/

0 commit comments

Comments
 (0)