diff --git a/load-tests/calendar.js b/load-tests/calendar.js new file mode 100644 index 0000000..78cffd3 --- /dev/null +++ b/load-tests/calendar.js @@ -0,0 +1,50 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { + BASE_URL, + VU_OPTIONS, + authHeaders, + createGuest, + getCalendarParams, +} from './common.js'; + +export const options = { + ...VU_OPTIONS, + duration: '1m', +}; + +export default function () { + const { res: guestRes } = createGuest(); + + const guestOk = check(guestRes, { + 'guest created': (r) => r.status === 201 || r.status === 200, + 'guest has token': (r) => !!r.json('data.auth.accessToken'), + }); + + if (!guestOk) { + return; + } + + const headers = authHeaders(guestRes.json('data.auth.accessToken')); + const { year, month, selectedDate } = getCalendarParams(); + + const statusRes = http.get(`${BASE_URL}/api/v1/users/status`, { headers }); + check(statusRes, { 'user status 200': (r) => r.status === 200 }); + + const calRes = http.get( + `${BASE_URL}/api/v1/calendars/main?year=${year}&month=${month}&selectedDate=${selectedDate}`, + { headers }, + ); + check(calRes, { 'calendar main 200': (r) => r.status === 200 }); + + const dateEventsRes = http.get( + `${BASE_URL}/api/v1/calendars/dates/${selectedDate}/events`, + { headers }, + ); + check(dateEventsRes, { 'date events 200': (r) => r.status === 200 }); + + const labelsRes = http.get(`${BASE_URL}/api/v1/labels`, { headers }); + check(labelsRes, { 'labels 200': (r) => r.status === 200 }); + + sleep(1); +} diff --git a/load-tests/common.js b/load-tests/common.js new file mode 100644 index 0000000..1b6e687 --- /dev/null +++ b/load-tests/common.js @@ -0,0 +1,40 @@ +import http from 'k6/http'; +import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; + +export const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; + +export const VU_OPTIONS = { + vus: 10, + duration: '30s', + thresholds: { + http_req_duration: ['p(95)<500'], + http_req_failed: ['rate<0.01'], + }, +}; + +export function createGuest() { + const guestId = uuidv4(); + const res = http.post( + `${BASE_URL}/api/v1/guests`, + JSON.stringify({ guestId }), + { headers: { 'Content-Type': 'application/json' } }, + ); + + return { res, guestId }; +} + +export function authHeaders(accessToken) { + return { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }; +} + +export function getCalendarParams() { + const now = new Date(); + return { + year: now.getFullYear(), + month: now.getMonth() + 1, + selectedDate: now.toISOString().slice(0, 10), + }; +} diff --git a/load-tests/guest.js b/load-tests/guest.js new file mode 100644 index 0000000..433c586 --- /dev/null +++ b/load-tests/guest.js @@ -0,0 +1,31 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { BASE_URL, VU_OPTIONS, createGuest } from './common.js'; + +export const options = VU_OPTIONS; + +export function setup() { + const res = http.get(`${BASE_URL}/health`); + if (res.status !== 200) { + throw new Error( + `백엔드에 연결할 수 없습니다 (${BASE_URL}/health → status=${res.status}). ` + + 'Spring Boot가 실행 중인지 확인하세요.', + ); + } +} + +export default function () { + const { res } = createGuest(); + + check(res, { + 'guest created or reconnected': (r) => { + if (r.status !== 201 && r.status !== 200) { + console.error(`guest 실패: status=${r.status}, body=${r.body}`); + } + return r.status === 201 || r.status === 200; + }, + 'guest response has access token': (r) => !!r.json('data.auth.accessToken'), + }); + + sleep(1); +} diff --git a/load-tests/health.js b/load-tests/health.js new file mode 100644 index 0000000..138bc53 --- /dev/null +++ b/load-tests/health.js @@ -0,0 +1,16 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { BASE_URL, VU_OPTIONS } from './common.js'; + +export const options = VU_OPTIONS; + +export default function () { + const res = http.get(`${BASE_URL}/health`); + + check(res, { + 'health status is 200': (r) => r.status === 200, + 'health overall UP': (r) => r.json('status') === 'UP', + }); + + sleep(1); +} diff --git a/src/main/java/com/tryna/domain/recommendation/constants/RecommendationRedisKey.java b/src/main/java/com/tryna/domain/recommendation/constants/RecommendationRedisKey.java index 7b653dc..3fc3632 100644 --- a/src/main/java/com/tryna/domain/recommendation/constants/RecommendationRedisKey.java +++ b/src/main/java/com/tryna/domain/recommendation/constants/RecommendationRedisKey.java @@ -2,7 +2,7 @@ public final class RecommendationRedisKey { private static final String LATEST_REVISION_PREFIX = - "tryna:recommendation:latest-revision:"; + "recommendation:latest-revision:"; private RecommendationRedisKey() { } diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 5e57f28..444f41b 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -5,6 +5,8 @@ spring: username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:postgres} driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 25 # 로컬 환경에 부 DB가 없으면 주 DB와 동일한 곳을 바라본다. # 부 DB 라우팅 동작 자체를 로컬에서 검증하고 싶다면 DB_REPLICA_URL을 별도로 지정한다. read: @@ -12,6 +14,8 @@ spring: username: ${DB_REPLICA_USERNAME:${DB_USERNAME:postgres}} password: ${DB_REPLICA_PASSWORD:${DB_PASSWORD:postgres}} driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 25 jpa: hibernate: # Flyway가 먼저 마이그레이션을 적용한 뒤 실행된다. 엔티티 변경 시 update가 로컬 스키마를