feat(gift): 신청번호 생성기를 추가한다
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.gift.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequence
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequenceRepository
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class GiftApplicationNoGenerator(
|
||||||
|
private val repository: GiftApplicationNoSequenceRepository,
|
||||||
|
transactionManager: PlatformTransactionManager
|
||||||
|
) {
|
||||||
|
private val transactionTemplate = TransactionTemplate(transactionManager)
|
||||||
|
|
||||||
|
fun generate(now: LocalDateTime = LocalDateTime.now(SEOUL_ZONE)): String {
|
||||||
|
val sequenceDate = now.format(DATE_FORMATTER)
|
||||||
|
for (attempt in 1..MAX_RETRY_COUNT) {
|
||||||
|
try {
|
||||||
|
return transactionTemplate.execute {
|
||||||
|
val sequence = repository.findBySequenceDateForUpdate(sequenceDate)
|
||||||
|
?: repository.saveAndFlush(GiftApplicationNoSequence(sequenceDate = sequenceDate, lastSequence = 0))
|
||||||
|
sequence.lastSequence += 1
|
||||||
|
"G$sequenceDate${sequence.lastSequence.toString().padStart(6, '0')}"
|
||||||
|
} ?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
} catch (_: DataIntegrityViolationException) {
|
||||||
|
if (attempt == MAX_RETRY_COUNT) throw SodaException(messageKey = "gift.application_no.generate_failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SodaException(messageKey = "gift.application_no.generate_failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val SEOUL_ZONE = ZoneId.of("Asia/Seoul")
|
||||||
|
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd")
|
||||||
|
private const val MAX_RETRY_COUNT = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.gift.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequenceRepository
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.concurrent.Callable
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
@DataJpaTest(
|
||||||
|
properties = [
|
||||||
|
"spring.cache.type=none",
|
||||||
|
"spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@Import(QueryDslConfig::class)
|
||||||
|
@DisplayName("선물 신청번호 채번 테스트")
|
||||||
|
class GiftApplicationNoGeneratorTest @Autowired constructor(
|
||||||
|
private val repository: GiftApplicationNoSequenceRepository,
|
||||||
|
private val transactionManager: PlatformTransactionManager
|
||||||
|
) {
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
repository.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("같은 날짜 신청번호를 순서대로 발급한다")
|
||||||
|
fun shouldGenerateSequentialApplicationNoByDate() {
|
||||||
|
val generator = GiftApplicationNoGenerator(repository, transactionManager)
|
||||||
|
val now = LocalDateTime.of(2026, 9, 29, 12, 0)
|
||||||
|
|
||||||
|
val first = generator.generate(now)
|
||||||
|
val second = generator.generate(now)
|
||||||
|
|
||||||
|
assertEquals("G20260929000001", first)
|
||||||
|
assertEquals("G20260929000002", second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("병렬 발급 결과는 모두 중복되지 않는다")
|
||||||
|
fun shouldGenerateUniqueApplicationNoConcurrently() {
|
||||||
|
val generator = GiftApplicationNoGenerator(repository, transactionManager)
|
||||||
|
val now = LocalDateTime.of(2026, 9, 29, 12, 0)
|
||||||
|
val executor = Executors.newFixedThreadPool(4)
|
||||||
|
|
||||||
|
val applicationNos = try {
|
||||||
|
executor.invokeAll(
|
||||||
|
(1..10).map {
|
||||||
|
Callable { generator.generate(now) }
|
||||||
|
}
|
||||||
|
).map { it.get() }
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(10, applicationNos.toSet().size)
|
||||||
|
assertEquals("G20260929000001", applicationNos.minOrNull())
|
||||||
|
assertEquals("G20260929000010", applicationNos.maxOrNull())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user