refactor: 초기 더미 데이터 생성 처리 로직 수정

This commit is contained in:
이상진 2025-10-04 17:46:18 +09:00
parent 9bc5b50a8f
commit f04d521029

View File

@ -8,11 +8,13 @@ import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import com.sangdol.roomescape.payment.infrastructure.common.* import com.sangdol.roomescape.payment.infrastructure.common.*
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.store.infrastructure.persistence.StoreEntity
import com.sangdol.roomescape.supports.AdminFixture import com.sangdol.roomescape.supports.AdminFixture
import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.FunSpecSpringbootTest
import com.sangdol.roomescape.supports.randomPhoneNumber import com.sangdol.roomescape.supports.randomPhoneNumber
import com.sangdol.roomescape.supports.randomString import com.sangdol.roomescape.supports.randomString
import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.user.business.SIGNUP import com.sangdol.roomescape.user.business.SIGNUP
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus
@ -21,6 +23,7 @@ import io.kotest.core.test.TestCaseOrder
import jakarta.persistence.EntityManager import jakarta.persistence.EntityManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
@ -151,22 +154,22 @@ class DefaultDataInitializer : AbstractDataInitializer() {
AdminPermissionLevel.READ_SUMMARY to 3 AdminPermissionLevel.READ_SUMMARY to 3
) )
val storeIds: List<Long> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) { val stores: List<StoreEntity> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery( entityManager.createQuery(
"SELECT s.id FROM StoreEntity s", "SELECT s FROM StoreEntity s",
Long::class.java StoreEntity::class.java
).resultList ).resultList
}!!.map { it as Long } }!!
transactionExecutionUtil.withNewTransaction(isReadOnly = false) { transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
storeIds.forEach { storeId -> stores.forEach { store ->
// StoreManager 1명 생성 // StoreManager 1명 생성
val storeManager = AdminFixture.create( val storeManager = AdminFixture.create(
account = "$storeId", account = store.name,
name = randomKoreanName(), name = randomKoreanName(),
phone = randomPhoneNumber(), phone = randomPhoneNumber(),
type = AdminType.STORE, type = AdminType.STORE,
storeId = storeId, storeId = store.id,
permissionLevel = AdminPermissionLevel.FULL_ACCESS permissionLevel = AdminPermissionLevel.FULL_ACCESS
).apply { ).apply {
this.createdBy = superHQAdmin.id this.createdBy = superHQAdmin.id
@ -178,11 +181,11 @@ class DefaultDataInitializer : AbstractDataInitializer() {
storeAdminCountsByPermissionLevel.forEach { (permissionLevel, count) -> storeAdminCountsByPermissionLevel.forEach { (permissionLevel, count) ->
repeat(count) { index -> repeat(count) { index ->
AdminFixture.create( AdminFixture.create(
account = randomString(), account = "${store.name}-${permissionLevel.ordinal}${index}",
name = randomKoreanName(), name = randomKoreanName(),
phone = randomPhoneNumber(), phone = randomPhoneNumber(),
type = AdminType.STORE, type = AdminType.STORE,
storeId = storeId, storeId = store.id,
permissionLevel = permissionLevel permissionLevel = permissionLevel
).apply { ).apply {
this.createdBy = storeManager.id this.createdBy = storeManager.id
@ -217,7 +220,7 @@ class DefaultDataInitializer : AbstractDataInitializer() {
val batchArgs = mutableListOf<Array<Any>>() val batchArgs = mutableListOf<Array<Any>>()
repeat(500) { i -> repeat(500) { i ->
val randomDay = if (i <= 9) (1..30).random() else (1..365 * 2).random() val randomDay = if (i <= 9) (7..30).random() else (30..365 * 2).random()
val randomCreatedAt: LocalDateTime = LocalDateTime.now().minusDays(randomDay.toLong()) val randomCreatedAt: LocalDateTime = LocalDateTime.now().minusDays(randomDay.toLong())
val randomThemeName = val randomThemeName =
(1..7).random().let { repeat -> (1..repeat).joinToString("") { randomKoreanName() } } (1..7).random().let { repeat -> (1..repeat).joinToString("") { randomKoreanName() } }
@ -417,25 +420,49 @@ class UserDataInitializer : AbstractDataInitializer() {
class ScheduleDataInitializer : AbstractDataInitializer() { class ScheduleDataInitializer : AbstractDataInitializer() {
init { init {
context("일정 초기 데이터 생성") { context("일정 초기 데이터 생성") {
test("테마 생성일 기준으로 다음 3일차, 매일 5개의 일정을 모든 매장에 생성") { test("테마 생성일 기준으로 다음 3일차, 매일 최대 10개의 일정을 모든 매장에 생성") {
val stores: List<Pair<Long, Long>> = getStoreWithManagers() val stores: List<Pair<Long, Long>> = getStoreWithManagers()
val themes: List<Triple<Long, Short, LocalDateTime>> = getThemes() val themes: List<ThemeEntity> = getThemes()
val maxAvailableMinutes = themes.maxOf { it.second.toInt() } val maxScheduleCountPerDay = 10
val scheduleCountPerDay = 5
val startTime = LocalTime.of(10, 0) val startTime = LocalTime.of(10, 0)
var lastTime = startTime
val times = mutableListOf<LocalTime>()
repeat(scheduleCountPerDay) { val themeWithTimes: Map<ThemeEntity, List<LocalTime>> = themes.associateWith { theme ->
val times = mutableListOf<LocalTime>()
val themeAvailableMinutes = theme.availableMinutes
var lastTime = startTime
while (times.size <= maxScheduleCountPerDay && lastTime.hour in (10..23)) {
times.add(lastTime) times.add(lastTime)
lastTime = lastTime.plusMinutes(maxAvailableMinutes.toLong() + 10L) lastTime = lastTime.plusMinutes(themeAvailableMinutes + 10L)
}
times
} }
coroutineScope { coroutineScope {
themes.forEach { theme -> stores.map { store ->
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
processTheme(theme, stores, times) processTheme(store, themeWithTimes)
}
}
}
}
test("내일 ~ 일주일 뒤 까지의 일정 생성") {
// val stores: List<Pair<Long, Long>> = getStoreWithManagers()
// val availableThemes: List<ThemeEntity> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
// entityManager.createQuery(
// "SELECT t FROM ThemeEntity t WHERE t.isActive = true AND t.createdAt >", ThemeEntity::class.java
// ).resultList
// }!!.take(10)
coroutineScope {
val jobs = (1..100).map { i ->
launch(Dispatchers.IO) {
val threadName = Thread.currentThread().name
println("[$i] 시작: $threadName")
delay(1)
println("[$i] 완료: $threadName")
} }
} }
} }
@ -444,9 +471,8 @@ class ScheduleDataInitializer : AbstractDataInitializer() {
} }
private suspend fun processTheme( private suspend fun processTheme(
theme: Triple<Long, Short, LocalDateTime>, store: Pair<Long, Long>,
stores: List<Pair<Long, Long>>, themeWithTimes: Map<ThemeEntity, List<LocalTime>>
times: List<LocalTime>
) { ) {
val sql = """ val sql = """
INSERT INTO schedule ( INSERT INTO schedule (
@ -457,24 +483,22 @@ class ScheduleDataInitializer : AbstractDataInitializer() {
val batchArgs = mutableListOf<Array<Any>>() val batchArgs = mutableListOf<Array<Any>>()
val now = LocalDateTime.now() val status = ScheduleStatus.RESERVED.name
stores.forEach { (storeId, adminId) -> themeWithTimes.forEach { (theme, times) ->
(1..3).forEach { dayOffset -> val themeCreatedAt = theme.createdAt
val date = theme.third.toLocalDate().plusDays(dayOffset.toLong()) (1..3).forEach {
val date = themeCreatedAt.toLocalDate().plusDays(it.toLong())
times.forEach { time -> times.forEach { time ->
val scheduledAt = LocalDateTime.of(date, time) val storeId = store.first
val status = val storeAdminId = store.second
if (scheduledAt.isAfter(now)) ScheduleStatus.AVAILABLE.name else ScheduleStatus.RESERVED.name
batchArgs.add( batchArgs.add(
arrayOf( arrayOf(
idGenerator.create(), storeId, theme.first, date, time, idGenerator.create(), storeId, theme.id, date, time,
status, adminId, adminId, Timestamp.valueOf(now), Timestamp.valueOf(now) status, storeAdminId, storeAdminId, themeCreatedAt.plusHours(1), themeCreatedAt.plusHours(1)
) )
) )
if (batchArgs.size >= 500) { if (batchArgs.size >= 300) {
executeBatch(sql, batchArgs).also { batchArgs.clear() } executeBatch(sql, batchArgs).also { batchArgs.clear() }
} }
} }
@ -500,17 +524,13 @@ class ScheduleDataInitializer : AbstractDataInitializer() {
} }
} }
private fun getThemes(): List<Triple<Long, Short, LocalDateTime>> { private fun getThemes(): List<ThemeEntity> {
return transactionExecutionUtil.withNewTransaction(isReadOnly = true) { return transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery( entityManager.createQuery(
"SELECT t._id, t.availableMinutes, t.createdAt FROM ThemeEntity t", "SELECT t FROM ThemeEntity t",
List::class.java ThemeEntity::class.java
) ).resultList
.resultList }!!
}!!.map {
val array = it as List<*>
Triple(array[0] as Long, array[1] as Short, array[2] as LocalDateTime)
}
} }
} }
@ -528,10 +548,10 @@ class ReservationDataInitializer : AbstractDataInitializer() {
init { init {
context("예약 초기 데이터 생성") { context("예약 초기 데이터 생성") {
test("${ScheduleStatus.RESERVED}인 모든 일정에 예약을 1개씩 배정한다.") { test("${ScheduleStatus.RESERVED}인 모든 일정에 예약을 1개씩 배정한다.") {
val chunkSize = 10_000 val chunkSize = 500
val chunkedSchedules: List<List<ScheduleWithThemeParticipants>> = entityManager.createQuery( val chunkedSchedules: List<List<ScheduleWithThemeParticipants>> = entityManager.createQuery(
"SELECT new com.sangdol.data.ScheduleWithThemeParticipants(s._id, t.minParticipants, t.maxParticipants) FROM ScheduleEntity s JOIN ThemeEntity t ON s.themeId = t.id WHERE s.status = :status", "SELECT new com.sangdol.roomescape.data.ScheduleWithThemeParticipants(s._id, t.minParticipants, t.maxParticipants) FROM ScheduleEntity s JOIN ThemeEntity t ON s.themeId = t.id WHERE s.status = :status",
ScheduleWithThemeParticipants::class.java ScheduleWithThemeParticipants::class.java
).setParameter("status", ScheduleStatus.RESERVED).resultList.chunked(chunkSize) ).setParameter("status", ScheduleStatus.RESERVED).resultList.chunked(chunkSize)
@ -587,10 +607,6 @@ class ReservationDataInitializer : AbstractDataInitializer() {
user.id, user.id,
) )
) )
if (batchArgs.size >= 1_000) {
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
} }
if (batchArgs.isNotEmpty()) executeBatch(sql, batchArgs).also { batchArgs.clear() } if (batchArgs.isNotEmpty()) executeBatch(sql, batchArgs).also { batchArgs.clear() }
@ -671,7 +687,7 @@ class PaymentDataInitializer : AbstractDataInitializer() {
} }
coroutineScope { coroutineScope {
allReservations.chunked(10_000).forEach { reservations -> allReservations.chunked(500).forEach { reservations ->
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
processPaymentAndDefaultDetail(reservations) processPaymentAndDefaultDetail(reservations)
} }
@ -681,12 +697,12 @@ class PaymentDataInitializer : AbstractDataInitializer() {
test("기존 결제 데이터에 상세 정보(계좌이체, 카드, 간편결제) 데이터를 생성한다.") { test("기존 결제 데이터에 상세 정보(계좌이체, 카드, 간편결제) 데이터를 생성한다.") {
val allPayments: List<PaymentWithMethods> = entityManager.createQuery( val allPayments: List<PaymentWithMethods> = entityManager.createQuery(
"SELECT new com.sangdol.data.PaymentWithMethods(pd._id, p.totalAmount, p.method) FROM PaymentEntity p JOIN PaymentDetailEntity pd ON p._id = pd.paymentId", "SELECT new com.sangdol.roomescape.data.PaymentWithMethods(pd._id, p.totalAmount, p.method) FROM PaymentEntity p JOIN PaymentDetailEntity pd ON p._id = pd.paymentId",
PaymentWithMethods::class.java PaymentWithMethods::class.java
).resultList ).resultList
coroutineScope { coroutineScope {
allPayments.chunked(10_000).forEach { payments -> allPayments.chunked(500).forEach { payments ->
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
processPaymentDetail(payments) processPaymentDetail(payments)
} }
@ -731,9 +747,6 @@ class PaymentDataInitializer : AbstractDataInitializer() {
approvedAtCache, approvedAtCache,
) )
) )
if (paymentBatchArgs.size >= 1_000) {
executeBatch(paymentSql, paymentBatchArgs).also { paymentBatchArgs.clear() }
}
val suppliedAmount: Int = (totalPrice * 0.9).toInt() val suppliedAmount: Int = (totalPrice * 0.9).toInt()
val vat: Int = (totalPrice - suppliedAmount) val vat: Int = (totalPrice - suppliedAmount)
@ -746,10 +759,6 @@ class PaymentDataInitializer : AbstractDataInitializer() {
vat vat
) )
) )
if (detailBatchArgs.size >= 1_000) {
executeBatch(paymentDetailSql, detailBatchArgs).also { detailBatchArgs.clear() }
}
} }
if (paymentBatchArgs.isNotEmpty()) { if (paymentBatchArgs.isNotEmpty()) {
@ -780,9 +789,6 @@ class PaymentDataInitializer : AbstractDataInitializer() {
settlementStatus settlementStatus
) )
) )
if (transferBatchArgs.size >= 1_000) {
executeBatch(paymentBankTransferDetailSql, transferBatchArgs).also { transferBatchArgs.clear() }
}
} }
PaymentMethod.EASY_PAY -> { PaymentMethod.EASY_PAY -> {
@ -803,10 +809,6 @@ class PaymentDataInitializer : AbstractDataInitializer() {
) )
) )
if (cardBatchArgs.size >= 1_000) {
executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() }
}
} else { } else {
easypayPrepaidBatchArgs.add( easypayPrepaidBatchArgs.add(
arrayOf( arrayOf(
@ -816,10 +818,6 @@ class PaymentDataInitializer : AbstractDataInitializer() {
randomDiscountAmount, randomDiscountAmount,
) )
) )
if (easypayPrepaidBatchArgs.size >= 1_000) {
executeBatch(paymentEasypayPrepaidDetailSql, easypayPrepaidBatchArgs).also { easypayPrepaidBatchArgs.clear() }
}
} }
} }
@ -839,10 +837,6 @@ class PaymentDataInitializer : AbstractDataInitializer() {
0, 0,
) )
) )
if (cardBatchArgs.size >= 1_000) {
executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() }
}
} }
else -> return@forEach else -> return@forEach