-
Notifications
You must be signed in to change notification settings - Fork 1
Test/#184 loadtest #185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Test/#184 loadtest #185
Changes from 6 commits
766a1a4
9441468
49d277a
a7891f4
134f94a
ccd472e
f110cc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| // ์ง์์ ์ ์ถ ๋ชฐ๋ฆผ(write burst) ๋ถํ ํ ์คํธ | ||
| // | ||
| // ApplicationServiceImpl.create() ๋ ๋จ์ผ @Transactional ์์์ NCP ์ ๋ก๋์ DB ์ฐ๊ธฐ๋ฅผ | ||
| // ํจ๊ป ์ํํ๋ค. ์ปค๋ฅ์ ์ ์ ์๊ฐ์ด ์ ๋ก๋ ์๊ฐ์ ๋ฌถ์ด๋ฏ๋ก ์ฒ๋ฆฌ๋ ์ํ์ด | ||
| // Hikari pool / ํธ๋์ญ์ ์๊ฐ | ||
| // ์ผ๋ก ๊ณ ์ ๋๋ค. ์ด ์คํฌ๋ฆฝํธ๋ ๊ทธ ์ํ์ ์ฐพ๋๋ค. | ||
| // | ||
| // ์คํ | ||
| // k6 run -e BASE_URL=https://stg.recruit-withus.co.kr -e SLUG=fdHhU7Mle \ | ||
| // -e PROFILE=smoke k6/application-submit-burst.js | ||
| // | ||
| // PROFILE | ||
| // smoke : VU 2, 30s โ ํ์ด๋ก๋๊ฐ ๊ณต๊ณ ์ค์ ๊ณผ ๋ง๋์ง ํ์ธ | ||
| // ramp : VU 0โ150 โ ์ฒ๋ฆฌ๋์ด ๊บพ์ด๋ ์ง์ ๊ณผ ์ฒซ 5xx ์์ | ||
| // soak : VU ๊ณ ์ , 5m โ ์ง์ ๋ถํ ์์ ์ฑ | ||
| // | ||
| // FILE_KB | ||
| // 0 ์ด๋ฉด ํ์ผํ ์ง๋ฌธ์ ํ์ด๋ก๋์์ ์์ ๋นผ๊ณ ๋ณด๋ธ๋ค(์ ๋ก๋ ์์). | ||
| // 0 ๋ณด๋ค ํฌ๋ฉด ๊ทธ ํฌ๊ธฐ์ ๋๋ฏธ ํ์ผ์ ์ฒจ๋ถํ๋ค. | ||
| // ๋ ๊ฐ์ ๋น๊ตํ๋ฉด NCP ์ ๋ก๋๊ฐ ํธ๋์ญ์ ์์ ์ฐจ์งํ๋ ๋น์ค์ด ๋๋ฌ๋๋ค. | ||
| // | ||
| // ์ฃผ์ | ||
| // - ์ค์ ์ง์์๊ฐ ์์ฑ๋๊ณ NCP ์ ํ์ผ์ด ์์ธ๋ค. ๋ผ์ด๋๋ง๋ค ์ ๋ฆฌํด์ผ | ||
| // ํ ์ด๋ธ ํฌ๊ธฐ๊ฐ ๋ฌ๋ผ์ง์ง ์์ ๋ผ์ด๋ ๊ฐ ๋น๊ต๊ฐ ์ ํจํ๋ค. | ||
| // - ๋ฉ์ผ์ mail.provider=noop ์ผ๋ก ๋ง์๋ ์ํ์์ ๋๋ฆด ๊ฒ. | ||
|
|
||
| import http from 'k6/http'; | ||
| import { check, sleep, fail } from 'k6'; | ||
| import { Counter, Rate, Trend } from 'k6/metrics'; | ||
|
|
||
| const BASE_URL = __ENV.BASE_URL; | ||
| const SLUG = __ENV.SLUG; | ||
| const PROFILE = __ENV.PROFILE || 'smoke'; | ||
| const FILE_KB = Number(__ENV.FILE_KB || '500'); | ||
| const SLEEP_SECONDS = Number(__ENV.SLEEP_SECONDS || '0'); | ||
|
|
||
| if (!BASE_URL) fail('BASE_URL is required. e.g. -e BASE_URL=https://stg.recruit-withus.co.kr'); | ||
| if (!SLUG) fail('SLUG is required. e.g. -e SLUG=fdHhU7Mle'); | ||
|
|
||
| const PROFILES = { | ||
| smoke: { vus: Number(__ENV.VUS || '2'), duration: __ENV.DURATION || '30s' }, | ||
| ramp: { | ||
| stages: [ | ||
| { duration: '30s', target: 10 }, | ||
| { duration: '1m', target: 30 }, | ||
| { duration: '1m', target: 60 }, | ||
| { duration: '1m', target: 100 }, | ||
| { duration: '1m', target: 150 }, | ||
| { duration: '30s', target: 0 }, | ||
| ], | ||
| }, | ||
| soak: { vus: Number(__ENV.VUS || '20'), duration: __ENV.DURATION || '5m' }, | ||
| }; | ||
|
|
||
| if (!PROFILES[PROFILE]) fail(`Unknown PROFILE: ${PROFILE}. use smoke|ramp|soak`); | ||
|
|
||
| export const options = { | ||
| ...PROFILES[PROFILE], | ||
| // ํ๊ณ๋ฅผ ์ฐพ๋ ๊ฒ ๋ชฉ์ ์ด๋ฏ๋ก ์คํจํด๋ ์ค๋จํ์ง ์๋๋ค. | ||
| thresholds: { | ||
| submit_failed: ['rate<0.05'], | ||
| http_req_duration: ['p(95)<10000'], | ||
| }, | ||
| }; | ||
|
|
||
| const submitFailed = new Rate('submit_failed'); | ||
| const submitDuration = new Trend('submit_duration', true); | ||
| const submitOk = new Counter('submit_ok'); | ||
| const submit5xx = new Counter('submit_5xx'); | ||
| const submit4xx = new Counter('submit_4xx'); | ||
| const submitPoolExhausted = new Counter('submit_pool_exhausted'); | ||
|
|
||
| // VU ๋น ํ ๋ฒ๋ง ๋ง๋ ๋ค. ๋งค ๋ฐ๋ณต ์์ฑํ๋ฉด ํด๋ผ์ด์ธํธ CPU ๊ฐ ๋ณ๋ชฉ์ด ๋๋ค. | ||
| const FILLER = FILE_KB > 0 | ||
| ? 'k6-loadtest-filler-'.repeat(Math.ceil((FILE_KB * 1024) / 19)).slice(0, FILE_KB * 1024) | ||
| : ''; | ||
|
|
||
| export function setup() { | ||
| const res = http.get(`${BASE_URL}/api/v1/recruitments/slug/${SLUG}`); | ||
| if (res.status !== 200) { | ||
| fail(`Failed to load recruitment. status=${res.status} body=${String(res.body).slice(0, 300)}`); | ||
| } | ||
|
|
||
| const d = res.json().result; | ||
| if (!d) fail('Recruitment detail is empty.'); | ||
|
|
||
| const questions = (d.applicationQuestions || []).map((q) => ({ | ||
| questionId: q.questionId, | ||
| type: q.type, | ||
| })); | ||
|
|
||
| // "2026.12.24" + "00:30" -> "2026-12-24T00:30:00" | ||
| const availableTimes = (d.availableTimeRanges || []).map((r) => { | ||
| const date = String(r.date).replace(/\./g, '-'); | ||
| const time = String(r.startTime).length === 5 ? `${r.startTime}:00` : r.startTime; | ||
| return `${date}T${time}`; | ||
| }); | ||
|
|
||
| const positions = (d.positions || []).map((p) => p.id ?? p.organizationRoleId); | ||
|
|
||
| const setupData = { | ||
| recruitmentId: d.recruitmentId, | ||
| positionId: positions.length > 0 ? positions[0] : null, | ||
| questions, | ||
| availableTimes, | ||
| needImage: d.needImage, | ||
| needGender: d.needGender, | ||
| needAddress: d.needAddress, | ||
| needSchool: d.needSchool, | ||
| needBirthDate: d.needBirthDate, | ||
| needMajor: d.needMajor, | ||
| needAcademicStatus: d.needAcademicStatus, | ||
| }; | ||
|
|
||
| console.log( | ||
| `[setup] recruitmentId=${setupData.recruitmentId} ` + | ||
| `questions=${questions.length}(file=${questions.filter((q) => q.type === 'FILE').length}) ` + | ||
| `availableTimes=${availableTimes.length} needImage=${d.needImage} ` + | ||
| `deadline=${d.documentDeadline} FILE_KB=${FILE_KB} PROFILE=${PROFILE}` | ||
| ); | ||
|
|
||
| return setupData; | ||
| } | ||
|
|
||
| export default function (data) { | ||
| const suffix = `${__VU}-${__ITER}-${Date.now()}`; | ||
| const attachFile = FILE_KB > 0; | ||
|
|
||
| // ApplicationValidator.validateFileAnswers ๋ answers ์ค FILE ์ง๋ฌธ ์์ | ||
| // ์ค์ ํ์ผ ๊ฐ์๊ฐ ์ ํํ ์ผ์นํด์ผ ํต๊ณผํ๋ค. FILE_KB=0 ์ด๋ฉด FILE ์ง๋ฌธ์ | ||
| // answers ์์ ์ ์ธํด ํ์ผ ์์ด ๋ณด๋ธ๋ค. | ||
| const answers = []; | ||
| let fileName = null; | ||
|
|
||
| for (const q of data.questions) { | ||
| if (q.type === 'FILE') { | ||
| if (!attachFile) continue; | ||
| fileName = `loadtest-${suffix}.pdf`; | ||
| answers.push({ questionId: q.questionId, answerText: null, fileName }); | ||
| } else { | ||
| answers.push({ | ||
| questionId: q.questionId, | ||
| answerText: `[k6] VU=${__VU} ITER=${__ITER} ์๋ ์์ฑ ๋ต๋ณ์ ๋๋ค.`, | ||
| fileName: null, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| const request = { | ||
| name: `๋ถํํ ์คํธ${__VU}-${__ITER}`, | ||
| email: `loadtest+${suffix}@example.com`, | ||
| phoneNumber: `010${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`, | ||
| recruitmentId: data.recruitmentId, | ||
| positionId: data.positionId, | ||
| answers, | ||
| availableTimes: data.availableTimes, | ||
| gender: data.needGender ? 'MALE' : null, | ||
| university: data.needSchool ? '์๋ช ๋ํ๊ต' : null, | ||
| major: data.needMajor ? '์ปดํจํฐ๊ณผํ๊ณผ' : null, | ||
| academicStatus: data.needAcademicStatus ? 'ENROLLED' : null, | ||
| birthDate: data.needBirthDate ? '2000-01-01' : null, | ||
| address: data.needAddress ? '์์ธ์ ๋๋ด๊ตฌ 56๋ก 501' : null, | ||
| }; | ||
|
|
||
| const payload = { | ||
| request: http.file(JSON.stringify(request), 'request.json', 'application/json'), | ||
| }; | ||
|
|
||
| if (data.needImage) { | ||
| payload.profileImage = http.file(FILLER || 'x', `loadtest-${suffix}.jpg`, 'image/jpeg'); | ||
| } | ||
|
|
||
| if (attachFile) { | ||
| payload.files = http.file(FILLER, fileName, 'application/pdf'); | ||
| } | ||
|
Comment on lines
+127
to
+175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ฏ Functional Correctness | ๐ Major | โก Quick win FILE ๋ต๋ณ ์์ ์ ๋ก๋ ํ์ผ ์๋ฅผ ๋์ผํ๊ฒ ๋ง๋์ญ์์ค. ํ์ฌ
๐ค Prompt for AI Agents |
||
|
|
||
| const res = http.post(`${BASE_URL}/api/v1/applications`, payload, { | ||
| tags: { name: 'POST /api/v1/applications' }, | ||
| timeout: '60s', | ||
| }); | ||
|
|
||
| submitDuration.add(res.timings.duration); | ||
|
|
||
| const ok = check(res, { 'submit 200': (r) => r.status === 200 }); | ||
| submitFailed.add(!ok); | ||
|
|
||
| if (ok) { | ||
| submitOk.add(1); | ||
| return; | ||
| } | ||
|
|
||
| const body = String(res.body || ''); | ||
|
|
||
| if (res.status >= 500 || res.status === 0) { | ||
| submit5xx.add(1); | ||
| // ์ปค๋ฅ์ ํ ๊ณ ๊ฐ์ ๋ฐ๋ก ์ผ๋ค. ์ด๊ฒ ์ง๋ฐฐ์ ์ด๋ฉด ํธ๋์ญ์ ๊ธธ์ด๊ฐ ๋ณ๋ชฉ์ด๋ค. | ||
| if (/SQLTransientConnection|Connection is not available|HikariPool/i.test(body)) { | ||
| submitPoolExhausted.add(1); | ||
| } | ||
| if (__ITER % 50 === 0) { | ||
| console.error(`5xx status=${res.status} body=${body.slice(0, 200)}`); | ||
| } | ||
| } else { | ||
| submit4xx.add(1); | ||
| // 400 ์ด๋ฉด ํ์ด๋ก๋๊ฐ ๊ณต๊ณ ์ค์ ๊ณผ ์ ๋ง๋ ๊ฒ์ด๋ฏ๋ก ์ฆ์ ๋๋ฌ๋์ผ ํ๋ค. | ||
| if (__ITER === 0) { | ||
| console.error(`${res.status} status=${res.status} body=${body.slice(0, 500)}`); | ||
| } | ||
| } | ||
|
|
||
| if (SLEEP_SECONDS > 0) sleep(SLEEP_SECONDS); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||
| package KUSITMS.WITHUS.global.infra.email.sender; | ||||||
|
|
||||||
| import KUSITMS.WITHUS.global.infra.email.MailProperties; | ||||||
| import lombok.RequiredArgsConstructor; | ||||||
| import lombok.extern.slf4j.Slf4j; | ||||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||||
| import org.springframework.context.annotation.Profile; | ||||||
| import org.springframework.core.io.InputStreamSource; | ||||||
| import org.springframework.stereotype.Component; | ||||||
| import org.springframework.transaction.support.TransactionSynchronization; | ||||||
| import org.springframework.transaction.support.TransactionSynchronizationManager; | ||||||
|
|
||||||
| import java.util.List; | ||||||
|
|
||||||
| /** | ||||||
| * ์ค์ ๋ฐ์ก ์์ด ๋ฐ์ก ์ง์ฐ๋ง ์ฌํํ๋ ๊ตฌํ์ฒด. | ||||||
| * ๋ถํ ํ ์คํธ์์ SendGrid/Gmail ์ผ์ผ ํ๋๋ฅผ ์๋ชจํ์ง ์๊ธฐ ์ํด ์ฌ์ฉํ๋ค. | ||||||
| * | ||||||
| * <p>{@link SmtpMailSender}, {@link SendGridMailSender} ์ ๋์ผํ๊ฒ ์ปค๋ฐ ์ดํ์ ๋์ํ๋ค. | ||||||
| * ํธ๋์ญ์ ์์์ ์ง์ฐ์ ์ฃผ๋ฉด DB ์ปค๋ฅ์ ์ ์ ์๊ฐ์ด ํจ๊ป ๋์ด๋ ์ ํ ๋ค๋ฅธ ๊ฒ์ ์ธก์ ํ๊ฒ ๋๋ฏ๋ก | ||||||
| * afterCommit ๊ตฌ์กฐ๋ฅผ ๋ฐ๋์ ๋ง์ถฐ์ผ ํ๋ค. | ||||||
| */ | ||||||
| @Slf4j | ||||||
| @Component | ||||||
| @Profile("!test") | ||||||
| @ConditionalOnProperty(name = "mail.provider", havingValue = "noop") | ||||||
| @RequiredArgsConstructor | ||||||
| public class NoopMailSender implements MailSender { | ||||||
|
|
||||||
| private final MailProperties mailProperties; | ||||||
|
|
||||||
| @Override | ||||||
| public void send(String to, String subject, String text) { | ||||||
| simulateAfterCommit(to, subject); | ||||||
| } | ||||||
|
|
||||||
| @Override | ||||||
| public void sendWithAttachments( | ||||||
| String to, | ||||||
| String subject, | ||||||
| String html, | ||||||
| List<InputStreamSource> attachments | ||||||
| ) { | ||||||
| simulateAfterCommit(to, subject); | ||||||
| } | ||||||
|
|
||||||
| private void simulateAfterCommit(String to, String subject) { | ||||||
| if (!TransactionSynchronizationManager.isSynchronizationActive()) { | ||||||
| simulate(to, subject); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { | ||||||
| @Override | ||||||
| public void afterCommit() { | ||||||
| simulate(to, subject); | ||||||
| } | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| private void simulate(String to, String subject) { | ||||||
| long delayMs = mailProperties.getNoopDelayMs(); | ||||||
|
|
||||||
| if (delayMs > 0) { | ||||||
| try { | ||||||
| Thread.sleep(delayMs); | ||||||
| } catch (InterruptedException e) { | ||||||
| Thread.currentThread().interrupt(); | ||||||
| return; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| log.info("Email skipped by noop provider (simulated {}ms): [{}] subject: {}", delayMs, to, subject); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ Security & Privacy | ๐ Major | โก Quick win ์์ ์ ์ด๋ฉ์ผ์ INFO ๋ก๊ทธ์์ ์ ๊ฑฐํ์ญ์์ค.
์์ ์์ ์ ๋ชฉ์ ๋ก๊ทธ์์ ์ ๊ฑฐํ๊ฑฐ๋ ๋น์๋ณํํ์ญ์์ค. ์์ ์์- log.info("Email skipped by noop provider (simulated {}ms): [{}] subject: {}", delayMs, to, subject);
+ log.info("Email skipped by noop provider (simulated {}ms)", delayMs);๐ Committable suggestion
Suggested change
๐ค Prompt for AI Agents |
||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,10 +83,14 @@ private void sendMail(String to, String subject, String html, List<InputStreamSo | |
| .POST(HttpRequest.BodyPublishers.ofString(requestBody)) | ||
| .build(); | ||
|
|
||
| long startedAt = System.nanoTime(); | ||
| HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); | ||
| long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000L; | ||
|
|
||
| if (response.statusCode() != ACCEPTED) { | ||
| log.error( | ||
| "SendGrid rejected email: status={} to={} subject={} body={}", | ||
| "SendGrid rejected email in {}ms: status={} to={} subject={} body={}", | ||
| elapsedMs, | ||
|
Comment on lines
+86
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ Maintainability & Code Quality | ๐ก Minor | โก Quick win ์์ธ ๊ฒฝ๋ก์๋ SendGrid ์ ์ก ์๊ฐ์ ๊ธฐ๋กํ์ธ์.
๐ค Prompt for AI Agents |
||
| response.statusCode(), | ||
| to, | ||
| subject, | ||
|
|
@@ -96,7 +100,13 @@ private void sendMail(String to, String subject, String html, List<InputStreamSo | |
| } | ||
|
|
||
| String messageId = response.headers().firstValue("X-Message-Id").orElse("unknown"); | ||
| log.info("Email accepted by SendGrid: [{}] subject: {} messageId: {}", to, subject, messageId); | ||
| log.info( | ||
| "Email accepted by SendGrid in {}ms: [{}] subject: {} messageId: {}", | ||
| elapsedMs, | ||
| to, | ||
| subject, | ||
| messageId | ||
| ); | ||
| } catch (CustomException e) { | ||
| throw e; | ||
| } catch (IOException e) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.