From a50cbbe43ef852563cb9c89dc6e44147a37d0b29 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 3 Oct 2025 16:00:37 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20schedule/hold=20API=EC=97=90?= =?UTF-8?q?=EC=84=9C=EC=9D=98=20=EB=A1=9C=EA=B9=85=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/business/ScheduleService.kt | 5 +- .../schedule/ScheduleConcurrencyTest.kt | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt index 91b4e2dc..73549c1b 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt @@ -71,10 +71,11 @@ class ScheduleService( id = id, currentStatus = ScheduleStatus.AVAILABLE, changeStatus = ScheduleStatus.HOLD - ) + ).also { + log.info { "[ScheduleService.holdSchedule] $it 개의 row 변경 완료" } + } if (result == 0) { - log.info { "[ScheduleService.holdSchedule] Holding된 일정 없음: id=${id}, count = " } throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE) } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt new file mode 100644 index 00000000..c237d11e --- /dev/null +++ b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt @@ -0,0 +1,59 @@ +package com.sangdol.roomescape.schedule + +import com.sangdol.common.types.web.HttpStatus +import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus +import com.sangdol.roomescape.supports.FunSpecSpringbootTest +import com.sangdol.roomescape.supports.runTest +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import org.springframework.data.repository.findByIdOrNull +import java.util.concurrent.CountDownLatch + +class ScheduleConcurrencyTest( + private val scheduleRepository: ScheduleRepository +) : FunSpecSpringbootTest() { + init { + test("하나의 ${ScheduleStatus.AVAILABLE}인 일정에 대한 동시 HOLD 변경 요청이 들어오면, 가장 먼저 들어온 요청만 성공한다.") { + val user = testAuthUtil.defaultUserLogin() + val schedule = dummyInitializer.createSchedule() + + withContext(Dispatchers.IO) { + val jobSize = 64 + val latch = CountDownLatch(jobSize) + + (1..jobSize).map { + async { + latch.countDown() + latch.await() + + runTest( + token = user.second, + on = { + post("/schedules/${schedule.id}/hold") + }, + expect = { + } + ).extract().statusCode() + } + }.awaitAll().also { + it.count { statusCode -> statusCode == HttpStatus.OK.value() } shouldBe 1 + it.count { statusCode -> statusCode == ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE.httpStatus.value() } shouldBe (jobSize - 1) + } + } + + assertSoftly(scheduleRepository.findByIdOrNull(schedule.id)!!) { + this.status shouldBe ScheduleStatus.HOLD + this.holdExpiredAt.shouldNotBeNull() + } + } + } +}