From 44c556776d0af38c72ebaf26d37755e8c9961e6e Mon Sep 17 00:00:00 2001 From: pricelees Date: Mon, 6 Oct 2025 16:49:58 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=9D=BC=EC=A0=95=20HOLD=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 충돌 방지를 위해 조회시 Lock 추가 - 해당 일정의 시작 시간이 현재 시간 이후인지 검증 로직 추가 --- .../schedule/business/ScheduleService.kt | 32 ++++++++++++------- .../schedule/business/ScheduleValidator.kt | 9 ++++++ .../roomescape/schedule/ScheduleApiTest.kt | 18 ++++++++++- 3 files changed, 47 insertions(+), 12 deletions(-) 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 95be6d49..2dbade7b 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,19 +71,18 @@ class ScheduleService( @Transactional fun holdSchedule(id: Long) { log.info { "[holdSchedule] 일정 Holding 시작: id=$id" } - val result: Int = scheduleRepository.changeStatus( - id = id, - currentStatus = ScheduleStatus.AVAILABLE, + + val schedule = findForUpdateOrThrow(id).also { + scheduleValidator.validateCanHold(it) + } + + scheduleRepository.changeStatus( + id = schedule.id, + currentStatus = schedule.status, changeStatus = ScheduleStatus.HOLD ).also { - log.info { "[holdSchedule] $it 개의 row 변경 완료" } + log.info { "[holdSchedule] 일정 Holding 완료: id=$id" } } - - if (result == 0) { - throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE) - } - - log.info { "[holdSchedule] 일정 Holding 완료: id=$id" } } // ======================================== @@ -222,7 +221,18 @@ class ScheduleService( return scheduleRepository.findByIdOrNull(id) ?.also { log.info { "[findOrThrow] 일정 조회 완료: id=$id" } } ?: run { - log.warn { "[updateSchedule] 일정 조회 실패. id=$id" } + log.warn { "[findOrThrow] 일정 조회 실패. id=$id" } + throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_FOUND) + } + } + + private fun findForUpdateOrThrow(id: Long): ScheduleEntity { + log.info { "[findForUpdateOrThrow] 일정 LOCK + 조회 시작: id=$id" } + + return scheduleRepository.findByIdForUpdate(id) + ?.also { log.info { "[findForUpdateOrThrow] 일정 조회 완료: id=$id" } } + ?: run { + log.warn { "[findForUpdateOrThrow] 일정 조회 실패. id=$id" } throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_FOUND) } } diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleValidator.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleValidator.kt index 1ce56192..5e0f5e1d 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleValidator.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleValidator.kt @@ -22,6 +22,15 @@ private val log: KLogger = KotlinLogging.logger {} class ScheduleValidator( private val scheduleRepository: ScheduleRepository ) { + fun validateCanHold(schedule: ScheduleEntity) { + if (schedule.status != ScheduleStatus.AVAILABLE) { + log.info { "[validateCanHold] HOLD 실패: id=${schedule.id}, status=${schedule.status}" } + throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE) + } + + validateNotInPast(schedule.date, schedule.time) + } + fun validateCanDelete(schedule: ScheduleEntity) { val status: ScheduleStatus = schedule.status diff --git a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt index 2ad2c5a6..94f8d4e8 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleApiTest.kt @@ -155,12 +155,28 @@ class ScheduleApiTest( } } + test("해당 일정의 시작 시간이 현재 시간 이전이면 실패한다.") { + val schedule = dummyInitializer.createSchedule( + request = ScheduleFixture.createRequest.copy( + date = KoreaDate.today(), + time = KoreaTime.now().minusMinutes(1) + ) + ) + + runExceptionTest( + token = testAuthUtil.defaultUserLogin().second, + method = HttpMethod.POST, + endpoint = "/schedules/${schedule.id}/hold", + expectedErrorCode = ScheduleErrorCode.PAST_DATE_TIME + ) + } + test("일정이 없으면 실패한다.") { runExceptionTest( token = testAuthUtil.defaultUserLogin().second, method = HttpMethod.POST, endpoint = "/schedules/${INVALID_PK}/hold", - expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE + expectedErrorCode = ScheduleErrorCode.SCHEDULE_NOT_FOUND ) } }