Skip to content

Commit 3f4f6fe

Browse files
authored
feat: 알림 조회 api k6 테스트코드 작성, 알림 테스트 데이터 초기화 클래스 구현
* feat: 알림 조회 api k6 테스트코드 작성, 테스트 데이터 초기화 클래스 구현 * feat: 알림 전체 읽음처리 api 테스트 시나리오 작성
1 parent e9b9d49 commit 3f4f6fe

6 files changed

Lines changed: 557 additions & 1 deletion

File tree

backend/src/main/java/com/back/global/init/perf/PerfBootstrapRunner.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class PerfBootstrapRunner implements ApplicationRunner {
2626
private final PerfPreregisterDataInitializer preregisterInit;
2727
private final PerfQueueDataInitializer queueInit;
2828
private final PerfTicketDataInitializer ticketInit;
29+
private final PerfNotificationDataInitializer notificationInit;
2930

3031
@Override
3132
public void run(ApplicationArguments args) {
@@ -75,8 +76,11 @@ public void run(ApplicationArguments args) {
7576
log.info("6️⃣ Ticket 데이터 생성 중...");
7677
ticketInit.init(ticketRatio);
7778

79+
log.info("7️⃣ Notification 데이터 생성 중...");
80+
notificationInit.init();
81+
7882
log.info("""
79-
83+
8084
=====================================
8185
✅ 부하테스트 데이터 초기화 완료
8286
=====================================
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package com.back.global.init.perf;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.Random;
7+
8+
import org.springframework.context.annotation.Profile;
9+
import org.springframework.stereotype.Component;
10+
11+
import com.back.domain.event.entity.Event;
12+
import com.back.domain.event.repository.EventRepository;
13+
import com.back.domain.notification.entity.Notification;
14+
import com.back.domain.notification.enums.DomainName;
15+
import com.back.domain.notification.enums.NotificationTypeDetails;
16+
import com.back.domain.notification.enums.NotificationTypes;
17+
import com.back.domain.notification.repository.NotificationRepository;
18+
import com.back.domain.user.entity.User;
19+
import com.back.domain.user.repository.UserRepository;
20+
21+
import lombok.RequiredArgsConstructor;
22+
import lombok.extern.slf4j.Slf4j;
23+
24+
/**
25+
* 부하테스트용 알림 데이터 초기화
26+
* - 1~500번 유저에 대해 1~4번 이벤트의 다양한 알림 생성
27+
*/
28+
@Component
29+
@Slf4j
30+
@RequiredArgsConstructor
31+
@Profile("perf")
32+
public class PerfNotificationDataInitializer {
33+
34+
private final NotificationRepository notificationRepository;
35+
private final UserRepository userRepository;
36+
private final EventRepository eventRepository;
37+
private final Random random = new Random(42); // 재현 가능한 랜덤
38+
39+
public void init() {
40+
if (notificationRepository.count() > 0) {
41+
log.info("Notification 데이터가 이미 존재합니다. 초기화를 건너뜁니다.");
42+
return;
43+
}
44+
45+
List<User> users = userRepository.findAll();
46+
if (users.isEmpty()) {
47+
log.warn("User 데이터가 없습니다. PerfUserDataInitializer를 먼저 실행해주세요.");
48+
return;
49+
}
50+
51+
// Event #1~4 조회
52+
List<Event> events = eventRepository.findAllById(List.of(1L, 2L, 3L, 4L));
53+
if (events.isEmpty()) {
54+
log.warn("Event 데이터가 없습니다. PerfEventDataInitializer를 먼저 실행해주세요.");
55+
return;
56+
}
57+
58+
log.info("Notification 초기 데이터 생성 중: {}명의 사용자 × {}개의 이벤트", users.size(), events.size());
59+
60+
List<Notification> notifications = new ArrayList<>();
61+
62+
// 각 유저별로 1~4번 이벤트에 대한 알림 생성
63+
for (User user : users) {
64+
for (Event event : events) {
65+
// 각 이벤트당 1~3개의 랜덤 알림 생성
66+
int notificationCount = random.nextInt(3) + 1; // 1~3개
67+
68+
for (int i = 0; i < notificationCount; i++) {
69+
Notification notification = createRandomNotification(user, event);
70+
notifications.add(notification);
71+
}
72+
}
73+
}
74+
75+
notificationRepository.saveAll(notifications);
76+
77+
long unreadCount = notifications.stream().filter(n -> !n.isRead()).count();
78+
log.info("✅ Notification 데이터 생성 완료: 총 {}개 (읽음: {}, 안읽음: {})",
79+
notifications.size(),
80+
notifications.size() - unreadCount,
81+
unreadCount);
82+
}
83+
84+
/**
85+
* 랜덤한 타입의 알림 생성
86+
*/
87+
private Notification createRandomNotification(User user, Event event) {
88+
// 알림 타입 랜덤 선택
89+
NotificationType[] types = NotificationType.values();
90+
NotificationType selectedType = types[random.nextInt(types.length)];
91+
92+
// 읽음 상태 랜덤 설정 (70% 확률로 읽음 처리)
93+
boolean isRead = random.nextDouble() < 0.7;
94+
LocalDateTime readAt = isRead ? LocalDateTime.now().minusDays(random.nextInt(30)) : null;
95+
96+
Notification.NotificationBuilder builder = Notification.builder()
97+
.user(user)
98+
.type(selectedType.notificationType)
99+
.typeDetail(selectedType.typeDetail)
100+
.domainName(selectedType.domainName)
101+
.domainId(event.getId())
102+
.title(selectedType.getTitle())
103+
.message(selectedType.getMessage(event.getTitle()))
104+
.isRead(isRead)
105+
.readAt(readAt);
106+
107+
return builder.build();
108+
}
109+
110+
/**
111+
* 알림 타입 정의
112+
*/
113+
private enum NotificationType {
114+
TICKET_GET(
115+
NotificationTypes.TICKET,
116+
NotificationTypeDetails.TICKET_GET,
117+
DomainName.ORDERS,
118+
"티켓 수령 완료"
119+
) {
120+
@Override
121+
String getMessage(String eventName) {
122+
return String.format("[%s]\n티켓 1매가 발급되었습니다", eventName);
123+
}
124+
},
125+
126+
PAYMENT_SUCCESS(
127+
NotificationTypes.PAYMENT,
128+
NotificationTypeDetails.PAYMENT_SUCCESS,
129+
DomainName.ORDERS,
130+
"주문 및 결제 완료"
131+
) {
132+
@Override
133+
String getMessage(String eventName) {
134+
long amount = 50000 + new Random().nextInt(150000); // 50,000 ~ 200,000
135+
return String.format("[%s] 티켓 1매가 결제되었습니다\n결제금액: %,d원", eventName, amount);
136+
}
137+
},
138+
139+
PAYMENT_FAILED(
140+
NotificationTypes.PAYMENT,
141+
NotificationTypeDetails.PAYMENT_FAILED,
142+
DomainName.ORDERS,
143+
"결제 실패"
144+
) {
145+
@Override
146+
String getMessage(String eventName) {
147+
return String.format("[%s] 결제에 실패했습니다\n다시 시도해주세요", eventName);
148+
}
149+
},
150+
151+
PRE_REGISTER_DONE(
152+
NotificationTypes.PRE_REGISTER,
153+
NotificationTypeDetails.PRE_REGISTER_DONE,
154+
DomainName.PRE_REGISTER,
155+
"사전등록 완료"
156+
) {
157+
@Override
158+
String getMessage(String eventName) {
159+
return String.format("[%s]\n사전등록이 완료되었습니다.\n티켓팅 시작일에 알림을 보내드리겠습니다.", eventName);
160+
}
161+
},
162+
163+
TICKETING_POSSIBLE(
164+
NotificationTypes.QUEUE_ENTRIES,
165+
NotificationTypeDetails.TICKETING_POSSIBLE,
166+
DomainName.QUEUE_ENTRIES,
167+
"티켓팅 시작"
168+
) {
169+
@Override
170+
String getMessage(String eventName) {
171+
return String.format("[%s]\n입장 준비가 완료되었습니다.\n이제 티켓을 구매하실 수 있습니다.", eventName);
172+
}
173+
},
174+
175+
TICKETING_EXPIRED(
176+
NotificationTypes.QUEUE_ENTRIES,
177+
NotificationTypeDetails.TICKETING_EXPIRED,
178+
DomainName.QUEUE_ENTRIES,
179+
"티켓팅 만료"
180+
) {
181+
@Override
182+
String getMessage(String eventName) {
183+
return String.format("[%s]\n입장 시간이 만료되었습니다.", eventName);
184+
}
185+
};
186+
187+
final NotificationTypes notificationType;
188+
final NotificationTypeDetails typeDetail;
189+
final DomainName domainName;
190+
final String title;
191+
192+
NotificationType(
193+
NotificationTypes notificationType,
194+
NotificationTypeDetails typeDetail,
195+
DomainName domainName,
196+
String title
197+
) {
198+
this.notificationType = notificationType;
199+
this.typeDetail = typeDetail;
200+
this.domainName = domainName;
201+
this.title = title;
202+
}
203+
204+
String getTitle() {
205+
return title;
206+
}
207+
208+
abstract String getMessage(String eventName);
209+
}
210+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import http from "k6/http";
2+
import { check } from "k6";
3+
4+
/**
5+
* GET /api/v1/notifications
6+
* 알림 목록 조회 시나리오 (인증 필요)
7+
*
8+
* 테스트 데이터:
9+
* - 사용자별 알림이 생성되어 있어야 함
10+
* - 티켓 발급, 주문 성공/실패 등 다양한 타입의 알림
11+
*
12+
* @param {string} baseUrl - API 기본 주소
13+
* @param {string} jwt - 사용자 JWT 토큰
14+
* @param {string} testId - 테스트 실행 ID (로그/메트릭 태깅용)
15+
*/
16+
export function getNotifications(baseUrl, jwt, testId) {
17+
const url = `${baseUrl}/api/v1/notifications`;
18+
19+
const params = {
20+
headers: {
21+
Authorization: `Bearer ${jwt}`,
22+
Accept: "application/json",
23+
},
24+
tags: {
25+
api: "getNotifications",
26+
test_id: testId,
27+
},
28+
};
29+
30+
const res = http.get(url, params);
31+
32+
let json;
33+
try {
34+
json = res.json();
35+
} catch {
36+
console.error("❌ JSON parse error:", res.body);
37+
return res;
38+
}
39+
40+
// 서버 응답 구조: { message: string, data: List<NotificationResponseDto> }
41+
const data = json?.data ?? null;
42+
43+
check(res, {
44+
"status 200": (r) => r.status === 200,
45+
"data exists": () => data !== null,
46+
"data is array": () => Array.isArray(data),
47+
});
48+
49+
// 알림 목록 데이터 구조 검증
50+
if (res.status === 200 && Array.isArray(data) && data.length > 0) {
51+
const firstNotification = data[0];
52+
53+
check(res, {
54+
"has id": () => typeof firstNotification?.id === "number",
55+
"has type": () => typeof firstNotification?.type === "string",
56+
"has typeDetail": () => typeof firstNotification?.typeDetail === "string",
57+
"has title": () => typeof firstNotification?.title === "string",
58+
"has message": () => typeof firstNotification?.message === "string",
59+
"has isRead": () => typeof firstNotification?.isRead === "boolean",
60+
"has createdAt": () => typeof firstNotification?.createdAt === "string",
61+
});
62+
}
63+
64+
if (res.status !== 200) {
65+
console.error(`❌ getNotifications failed [${res.status}]:`, JSON.stringify({
66+
status: res.status,
67+
message: json?.message,
68+
}));
69+
} else {
70+
// 성공 시 알림 개수 로깅 (디버깅용)
71+
if (__ENV.DEBUG === "true") {
72+
const notificationCount = Array.isArray(data) ? data.length : 0;
73+
const unreadCount = Array.isArray(data)
74+
? data.filter(n => !n.isRead).length
75+
: 0;
76+
console.log(`✅ Notifications retrieved: total=${notificationCount}, unread=${unreadCount}`);
77+
}
78+
}
79+
80+
return res;
81+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import http from "k6/http";
2+
import { check } from "k6";
3+
4+
/**
5+
* PATCH /api/v1/notifications/read-all
6+
* 전체 알림 읽음 처리 시나리오 (인증 필요)
7+
*
8+
* 테스트 데이터:
9+
* - 사용자별 알림이 생성되어 있어야 함
10+
* - 각 사용자는 읽지 않은 알림을 가지고 있음 (약 30%)
11+
*
12+
* @param {string} baseUrl - API 기본 주소
13+
* @param {string} jwt - 사용자 JWT 토큰
14+
* @param {string} testId - 테스트 실행 ID (로그/메트릭 태깅용)
15+
*/
16+
export function markAllAsRead(baseUrl, jwt, testId) {
17+
const url = `${baseUrl}/api/v1/notifications/read-all`;
18+
19+
const params = {
20+
headers: {
21+
Authorization: `Bearer ${jwt}`,
22+
Accept: "application/json",
23+
},
24+
tags: {
25+
api: "markAllAsRead",
26+
test_id: testId,
27+
},
28+
};
29+
30+
const res = http.patch(url, null, params);
31+
32+
let json;
33+
try {
34+
json = res.json();
35+
} catch {
36+
console.error("❌ JSON parse error:", res.body);
37+
return res;
38+
}
39+
40+
// 서버 응답 구조: { message: string, data: null }
41+
const data = json?.data ?? null;
42+
43+
check(res, {
44+
"status 200": (r) => r.status === 200,
45+
"data is null": () => data === null,
46+
"has success message": () => typeof json?.message === "string",
47+
});
48+
49+
if (res.status !== 200) {
50+
console.error(`❌ markAllAsRead failed [${res.status}]:`, JSON.stringify({
51+
status: res.status,
52+
message: json?.message,
53+
}));
54+
} else {
55+
// 성공 시 메시지 로깅 (디버깅용)
56+
if (__ENV.DEBUG === "true") {
57+
console.log(`✅ All notifications marked as read: ${json?.message}`);
58+
}
59+
}
60+
61+
return res;
62+
}

0 commit comments

Comments
 (0)