Skip to content

Commit f3a01bd

Browse files
authored
feat: 성능테스트용 데이터 세팅
* feat: passwordEncoder 프로필별 설정 분리 * feat: Perf Data Initializer 클래스 껍데기 구현 * refactor: securityConfig passwordEncoder Bcrypt 레벨설정 - yml 설정값 활용 * feat: 테스트용 데이터 초기화도구 구현
1 parent cb29c2c commit f3a01bd

13 files changed

Lines changed: 1032 additions & 2 deletions

backend/src/main/java/com/back/domain/seat/entity/Seat.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ public static Seat createSeat(Event event, String seatCode, SeatGrade grade, int
9999
return seat;
100100
}
101101

102+
/**
103+
* 테스트 데이터 생성용 좌석 생성 (상태 전이 없이 바로 SOLD)
104+
* 실제 비즈니스 로직을 거치지 않고 테스트 데이터를 생성하기 위한 전용 메서드
105+
*/
106+
public static Seat soldForPerf(Event event, String seatCode, SeatGrade grade, int price) {
107+
Seat seat = new Seat();
108+
seat.event = event;
109+
seat.seatCode = seatCode;
110+
seat.grade = grade;
111+
seat.price = price;
112+
seat.seatStatus = SeatStatus.SOLD;
113+
return seat;
114+
}
115+
116+
/**
117+
* 테스트 데이터 생성용 좌석 상태 직접 설정
118+
* 상태 전이 검증 없이 바로 원하는 상태로 변경
119+
*/
120+
public void setSeatStatusForPerf(SeatStatus status) {
121+
this.seatStatus = status;
122+
}
123+
102124
public void update(String seatCode, SeatGrade grade, int price, SeatStatus seatStatus) {
103125
this.seatCode = seatCode;
104126
this.grade = grade;

backend/src/main/java/com/back/domain/seat/repository/SeatRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import com.back.domain.seat.entity.Seat;
1212
import com.back.domain.seat.entity.SeatGrade;
13+
import com.back.domain.seat.entity.SeatStatus;
1314

1415
public interface SeatRepository extends JpaRepository<Seat, Long> {
1516

@@ -23,6 +24,9 @@ public interface SeatRepository extends JpaRepository<Seat, Long> {
2324

2425
Optional<Seat> findByEventIdAndId(Long eventId, Long seatId);
2526

27+
// 특정 이벤트의 특정 상태 좌석 조회 (성능 최적화)
28+
List<Seat> findByEventIdAndSeatStatus(Long eventId, SeatStatus seatStatus);
29+
2630
@Modifying
2731
@Query("DELETE FROM Seat s WHERE s.event.id = :eventId")
2832
void deleteByEventId(@Param("eventId") Long eventId);

backend/src/main/java/com/back/domain/ticket/entity/Ticket.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ public static Ticket issue(User owner, Seat seat, Event event, String verificati
7474
return ticket;
7575
}
7676

77+
/**
78+
* 테스트 데이터 생성용 티켓 생성 (상태 전이 없이 바로 ISSUED)
79+
* 실제 비즈니스 로직을 거치지 않고 테스트 데이터를 생성하기 위한 전용 메서드
80+
*/
81+
public static Ticket issuedForPerf(User owner, Seat seat, Event event) {
82+
Ticket ticket = new Ticket();
83+
ticket.owner = owner;
84+
ticket.seat = seat;
85+
ticket.event = event;
86+
ticket.ticketStatus = TicketStatus.ISSUED;
87+
ticket.issuedAt = LocalDateTime.now();
88+
return ticket;
89+
}
90+
7791
// DRAFT → PAID
7892
public void markPaid() {
7993
if (this.ticketStatus != TicketStatus.DRAFT) {

backend/src/main/java/com/back/global/config/SecurityConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.global.config;
22

3+
import org.springframework.beans.factory.annotation.Value;
34
import org.springframework.context.annotation.Bean;
45
import org.springframework.context.annotation.Configuration;
56
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@@ -97,7 +98,9 @@ public UrlBasedCorsConfigurationSource corsConfigurationSource() {
9798
}
9899

99100
@Bean
100-
public PasswordEncoder passwordEncoder() {
101-
return new BCryptPasswordEncoder(12);
101+
public PasswordEncoder passwordEncoder(
102+
@Value("${security.password.bcrypt-strength}") int strength
103+
) {
104+
return new BCryptPasswordEncoder(strength);
102105
}
103106
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.back.global.init.perf;
2+
3+
import org.springframework.boot.ApplicationArguments;
4+
import org.springframework.boot.ApplicationRunner;
5+
import org.springframework.context.annotation.Profile;
6+
import org.springframework.stereotype.Component;
7+
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
11+
/**
12+
* 부하테스트용 데이터 초기화 중앙 통제
13+
*
14+
* 실행 예시:
15+
* ./gradlew bootRun --args='--users=1000 --events=10 --prereg-ratio=0.8 --queue-ratio=0.7 --ticket-ratio=0.3'
16+
*/
17+
@Component
18+
@Profile("perf")
19+
@RequiredArgsConstructor
20+
@Slf4j
21+
public class PerfBootstrapRunner implements ApplicationRunner {
22+
23+
private final PerfUserDataInitializer userInit;
24+
private final PerfEventDataInitializer eventInit;
25+
private final PerfSeatDataInitializer seatInit;
26+
private final PerfPreregisterDataInitializer preregisterInit;
27+
private final PerfQueueDataInitializer queueInit;
28+
private final PerfTicketDataInitializer ticketInit;
29+
30+
@Override
31+
public void run(ApplicationArguments args) {
32+
// 파라미터 파싱 (기본값 포함)
33+
int userCount = getIntArg(args, "users", 500);
34+
int eventCount = getIntArg(args, "events", 50);
35+
double preregRatio = getDoubleArg(args, "prereg-ratio", 1);
36+
double queueRatio = getDoubleArg(args, "queue-ratio", 1);
37+
double ticketRatio = getDoubleArg(args, "ticket-ratio", 1);
38+
39+
log.info("""
40+
41+
=====================================
42+
🚀 부하테스트 데이터 초기화 시작
43+
=====================================
44+
📊 설정값:
45+
- 사용자 수: {}명
46+
- 이벤트 수: {}개
47+
- 사전등록 비율: {}%
48+
- 대기열 진입 비율: {}%
49+
- 티켓 발급 비율: {}%
50+
=====================================
51+
""",
52+
userCount,
53+
eventCount,
54+
(int)(preregRatio * 100),
55+
(int)(queueRatio * 100),
56+
(int)(ticketRatio * 100)
57+
);
58+
59+
// 순차 실행 (의존성 순서)
60+
log.info("1️⃣ User 데이터 생성 중...");
61+
userInit.init(userCount);
62+
63+
log.info("2️⃣ Event 데이터 생성 중...");
64+
eventInit.init(eventCount);
65+
66+
log.info("3️⃣ Seat 데이터 생성 중...");
67+
seatInit.init();
68+
69+
log.info("4️⃣ PreRegister 데이터 생성 중...");
70+
preregisterInit.init(preregRatio);
71+
72+
log.info("5️⃣ QueueEntry 데이터 생성 중...");
73+
queueInit.init(queueRatio);
74+
75+
log.info("6️⃣ Ticket 데이터 생성 중...");
76+
ticketInit.init(ticketRatio);
77+
78+
log.info("""
79+
80+
=====================================
81+
✅ 부하테스트 데이터 초기화 완료
82+
=====================================
83+
""");
84+
}
85+
86+
private int getIntArg(ApplicationArguments args, String key, int defaultValue) {
87+
if (args.containsOption(key)) {
88+
try {
89+
return Integer.parseInt(args.getOptionValues(key).get(0));
90+
} catch (NumberFormatException e) {
91+
log.warn("잘못된 숫자 형식: --{}={}, 기본값 {} 사용", key, args.getOptionValues(key).get(0),
92+
defaultValue);
93+
return defaultValue;
94+
}
95+
}
96+
return defaultValue;
97+
}
98+
99+
private double getDoubleArg(ApplicationArguments args, String key, double defaultValue) {
100+
if (args.containsOption(key)) {
101+
try {
102+
return Double.parseDouble(args.getOptionValues(key).get(0));
103+
} catch (NumberFormatException e) {
104+
log.warn("잘못된 숫자 형식: --{}={}, 기본값 {} 사용", key, args.getOptionValues(key).get(0),
105+
defaultValue);
106+
return defaultValue;
107+
}
108+
}
109+
return defaultValue;
110+
}
111+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package com.back.global.init.perf;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
7+
import org.springframework.context.annotation.Profile;
8+
import org.springframework.stereotype.Component;
9+
10+
import com.back.domain.event.entity.Event;
11+
import com.back.domain.event.entity.EventCategory;
12+
import com.back.domain.event.entity.EventStatus;
13+
import com.back.domain.event.repository.EventRepository;
14+
15+
import lombok.RequiredArgsConstructor;
16+
import lombok.extern.slf4j.Slf4j;
17+
18+
@Component
19+
@Slf4j
20+
@RequiredArgsConstructor
21+
@Profile("perf")
22+
public class PerfEventDataInitializer {
23+
24+
private final EventRepository eventRepository;
25+
26+
public void init(int eventCount) {
27+
if (eventRepository.count() > 0) {
28+
log.info("Event 데이터가 이미 존재합니다. 초기화를 건너뜁니다.");
29+
return;
30+
}
31+
32+
log.info("Event 초기 데이터 생성 중: {}개", eventCount);
33+
34+
LocalDateTime now = LocalDateTime.now();
35+
List<Event> events = new ArrayList<>();
36+
37+
// 필수 이벤트 3개 (각 상태별 최소 1개)
38+
// 1. 사전등록 진행중인 이벤트 (부하 테스트 메인 타겟)
39+
events.add(Event.builder()
40+
.title("아이유 2025 HEREH WORLD TOUR - 서울")
41+
.category(EventCategory.CONCERT)
42+
.description("아이유의 월드투어 서울 공연입니다. 최고의 무대를 만나보세요!")
43+
.place("잠실종합운동장 올림픽 주경기장")
44+
.imageUrl("https://example.com/iu-concert.jpg")
45+
.minPrice(88000)
46+
.maxPrice(165000)
47+
.preOpenAt(now.minusDays(2))
48+
.preCloseAt(now.plusDays(5))
49+
.ticketOpenAt(now.plusDays(7))
50+
.ticketCloseAt(now.plusDays(30))
51+
.maxTicketAmount(500)
52+
.status(EventStatus.PRE_OPEN)
53+
.build());
54+
55+
// 2. 대기열 준비중인 이벤트
56+
events.add(Event.builder()
57+
.title("세븐틴 BE THE SUN - 부산")
58+
.category(EventCategory.CONCERT)
59+
.description("세븐틴의 2025년 콘서트 투어입니다.")
60+
.place("부산 아시아드 주경기장")
61+
.imageUrl("https://example.com/seventeen.jpg")
62+
.minPrice(99000)
63+
.maxPrice(154000)
64+
.preOpenAt(now.minusDays(10))
65+
.preCloseAt(now.minusDays(3))
66+
.ticketOpenAt(now.plusMinutes(30))
67+
.ticketCloseAt(now.plusDays(20))
68+
.maxTicketAmount(500)
69+
.status(EventStatus.QUEUE_READY)
70+
.build());
71+
72+
// 3. 티켓팅 진행중인 이벤트 (부하 테스트 메인 타겟 - 티켓팅 경쟁)
73+
events.add(Event.builder()
74+
.title("뉴진스 2025 콘서트 - 서울")
75+
.category(EventCategory.CONCERT)
76+
.description("뉴진스의 2025년 서울 콘서트입니다. 최고의 무대를 만나보세요!")
77+
.place("고척스카이돔")
78+
.imageUrl("https://example.com/newjeans-concert.jpg")
79+
.minPrice(99000)
80+
.maxPrice(165000)
81+
.preOpenAt(now.minusDays(5))
82+
.preCloseAt(now.minusDays(2))
83+
.ticketOpenAt(now.minusHours(1))
84+
.ticketCloseAt(now.plusDays(3))
85+
.maxTicketAmount(500)
86+
.status(EventStatus.OPEN)
87+
.build());
88+
89+
// 4. 티켓 완판 이벤트 (부하 테스트 타겟 - 티켓 조회/관리)
90+
if (eventCount >= 4) {
91+
events.add(Event.builder()
92+
.title("르세라핌 2025 콘서트 - 서울")
93+
.category(EventCategory.CONCERT)
94+
.description("르세라핌의 2025년 서울 콘서트입니다. (완판)")
95+
.place("KSPO돔")
96+
.imageUrl("https://example.com/lesserafim-concert.jpg")
97+
.minPrice(99000)
98+
.maxPrice(165000)
99+
.preOpenAt(now.minusDays(30))
100+
.preCloseAt(now.minusDays(20))
101+
.ticketOpenAt(now.minusDays(15))
102+
.ticketCloseAt(now.minusDays(10))
103+
.maxTicketAmount(500)
104+
.status(EventStatus.CLOSED)
105+
.build());
106+
}
107+
108+
// 추가 이벤트 생성 (Event #5+, 최소 4개 요청 시)
109+
int additionalCount = Math.max(0, eventCount - 4);
110+
111+
for (int i = 0; i < additionalCount; i++) {
112+
if (i % 3 == 0) {
113+
events.add(createConcertEvent("테스트 콘서트 " + (i + 5), now, 15000, 85000, 10000));
114+
} else if (i % 3 == 1) {
115+
events.add(createPopupEvent("테스트 팝업 " + (i + 5), now, 0, 250000, 15000));
116+
} else if (i % 3 == 2) {
117+
events.add(createDropEvent("테스트 드롭 " + (i + 5), now, 159000, 159000, 2000));
118+
}
119+
}
120+
121+
eventRepository.saveAll(events);
122+
123+
log.info("✅ Event 데이터 생성 완료: {}개 (PRE_OPEN:1, QUEUE_READY:1, OPEN:1, CLOSED:1, READY:{})",
124+
events.size(), Math.max(0, events.size() - 4));
125+
}
126+
127+
private Event createConcertEvent(String title, LocalDateTime baseTime,
128+
int minPrice, int maxPrice, int maxTickets) {
129+
return Event.builder()
130+
.title(title)
131+
.category(EventCategory.CONCERT)
132+
.description(title + " - 부하테스트용 이벤트")
133+
.place("공연장")
134+
.imageUrl("https://example.com/" + title.hashCode() + ".jpg")
135+
.minPrice(minPrice)
136+
.maxPrice(maxPrice)
137+
.preOpenAt(baseTime.plusDays(2))
138+
.preCloseAt(baseTime.plusDays(9))
139+
.ticketOpenAt(baseTime.plusDays(12))
140+
.ticketCloseAt(baseTime.plusDays(30))
141+
.maxTicketAmount(maxTickets)
142+
.status(EventStatus.READY)
143+
.build();
144+
}
145+
146+
private Event createPopupEvent(String title, LocalDateTime baseTime,
147+
int minPrice, int maxPrice, int maxTickets) {
148+
return Event.builder()
149+
.title(title)
150+
.category(EventCategory.POPUP)
151+
.description(title + " - 부하테스트용 이벤트")
152+
.place("팝업 스토어")
153+
.imageUrl("https://example.com/" + title.hashCode() + ".jpg")
154+
.minPrice(minPrice)
155+
.maxPrice(maxPrice)
156+
.preOpenAt(baseTime.plusDays(1))
157+
.preCloseAt(baseTime.plusDays(5))
158+
.ticketOpenAt(baseTime.plusDays(7))
159+
.ticketCloseAt(baseTime.plusDays(21))
160+
.maxTicketAmount(maxTickets)
161+
.status(EventStatus.READY)
162+
.build();
163+
}
164+
165+
private Event createDropEvent(String title, LocalDateTime baseTime,
166+
int minPrice, int maxPrice, int maxTickets) {
167+
return Event.builder()
168+
.title(title)
169+
.category(EventCategory.DROP)
170+
.description(title + " - 부하테스트용 이벤트")
171+
.place("온라인")
172+
.imageUrl("https://example.com/" + title.hashCode() + ".jpg")
173+
.minPrice(minPrice)
174+
.maxPrice(maxPrice)
175+
.preOpenAt(baseTime.plusDays(1))
176+
.preCloseAt(baseTime.plusDays(3))
177+
.ticketOpenAt(baseTime.plusDays(5))
178+
.ticketCloseAt(baseTime.plusDays(10))
179+
.maxTicketAmount(maxTickets)
180+
.status(EventStatus.READY)
181+
.build();
182+
}
183+
}

0 commit comments

Comments
 (0)