refactor: 모든 LocalDateTime, OffsetDateTime 타입 Instant 전환

This commit is contained in:
이상진 2025-10-05 22:03:41 +09:00
parent 2fa4874ad7
commit e93d8de6cc
14 changed files with 43 additions and 46 deletions

View File

@ -8,7 +8,7 @@ import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedBy import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime import java.time.Instant
@MappedSuperclass @MappedSuperclass
@EntityListeners(AuditingEntityListener::class) @EntityListeners(AuditingEntityListener::class)
@ -17,7 +17,7 @@ abstract class AuditingBaseEntity(
) : PersistableBaseEntity(id) { ) : PersistableBaseEntity(id) {
@Column(updatable = false) @Column(updatable = false)
@CreatedDate @CreatedDate
lateinit var createdAt: LocalDateTime lateinit var createdAt: Instant
@Column(updatable = false) @Column(updatable = false)
@CreatedBy @CreatedBy
@ -25,7 +25,7 @@ abstract class AuditingBaseEntity(
@Column @Column
@LastModifiedDate @LastModifiedDate
lateinit var updatedAt: LocalDateTime lateinit var updatedAt: Instant
@Column @Column
@LastModifiedBy @LastModifiedBy

View File

@ -5,7 +5,7 @@ import com.sangdol.roomescape.auth.web.PrincipalType
import jakarta.persistence.* import jakarta.persistence.*
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime import java.time.Instant
@Entity @Entity
@Table(name = "login_history") @Table(name = "login_history")
@ -24,5 +24,5 @@ class LoginHistoryEntity(
@Column(updatable = false) @Column(updatable = false)
@CreatedDate @CreatedDate
var createdAt: LocalDateTime? = null, var createdAt: Instant? = null,
) : PersistableBaseEntity(id) ) : PersistableBaseEntity(id)

View File

@ -1,6 +1,6 @@
package com.sangdol.roomescape.common.types package com.sangdol.roomescape.common.types
import java.time.LocalDateTime import java.time.Instant
data class Auditor( data class Auditor(
val id: Long, val id: Long,
@ -12,8 +12,8 @@ data class Auditor(
} }
data class AuditingInfo( data class AuditingInfo(
val createdAt: LocalDateTime, val createdAt: Instant,
val createdBy: Auditor, val createdBy: Auditor,
val updatedAt: LocalDateTime, val updatedAt: Instant,
val updatedBy: Auditor, val updatedBy: Auditor,
) )

View File

@ -19,7 +19,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime import java.time.Instant
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -145,7 +145,7 @@ class ReservationService(
reservationId = reservation.id, reservationId = reservation.id,
canceledBy = user.id, canceledBy = user.id,
cancelReason = cancelReason, cancelReason = cancelReason,
canceledAt = LocalDateTime.now(), canceledAt = Instant.now(),
status = CanceledReservationStatus.COMPLETED status = CanceledReservationStatus.COMPLETED
).also { ).also {
canceledReservationRepository.save(it) canceledReservationRepository.save(it)

View File

@ -9,7 +9,7 @@ import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime import java.time.Instant
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -26,7 +26,7 @@ class IncompletedReservationScheduler(
fun processExpiredHoldSchedule() { fun processExpiredHoldSchedule() {
log.info { "[IncompletedReservationScheduler] 만료 시간이 지난 ${ScheduleStatus.HOLD} 상태의 일정 재활성화 시작" } log.info { "[IncompletedReservationScheduler] 만료 시간이 지난 ${ScheduleStatus.HOLD} 상태의 일정 재활성화 시작" }
scheduleRepository.releaseExpiredHolds(LocalDateTime.now()).also { scheduleRepository.releaseExpiredHolds(Instant.now()).also {
log.info { "[IncompletedReservationScheduler] ${it}개의 일정 재활성화 완료" } log.info { "[IncompletedReservationScheduler] ${it}개의 일정 재활성화 완료" }
} }
} }
@ -36,7 +36,7 @@ class IncompletedReservationScheduler(
fun processExpiredReservation() { fun processExpiredReservation() {
log.info { "[IncompletedReservationScheduler] 결제되지 않은 예약 만료 처리 시작 " } log.info { "[IncompletedReservationScheduler] 결제되지 않은 예약 만료 처리 시작 " }
reservationRepository.expirePendingReservations(LocalDateTime.now()).also { reservationRepository.expirePendingReservations(Instant.now()).also {
log.info { "[IncompletedReservationScheduler] ${it}개의 예약 및 일정 처리 완료" } log.info { "[IncompletedReservationScheduler] ${it}개의 예약 및 일정 처리 완료" }
} }
} }

View File

@ -5,7 +5,7 @@ import jakarta.persistence.Entity
import jakarta.persistence.EnumType import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated import jakarta.persistence.Enumerated
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.Instant
@Entity @Entity
@Table(name = "canceled_reservation") @Table(name = "canceled_reservation")
@ -15,7 +15,7 @@ class CanceledReservationEntity(
val reservationId: Long, val reservationId: Long,
val canceledBy: Long, val canceledBy: Long,
val cancelReason: String, val cancelReason: String,
val canceledAt: LocalDateTime, val canceledAt: Instant,
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
val status: CanceledReservationStatus, val status: CanceledReservationStatus,

View File

@ -4,7 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param import org.springframework.data.repository.query.Param
import java.time.LocalDateTime import java.time.Instant
interface ReservationRepository : JpaRepository<ReservationEntity, Long> { interface ReservationRepository : JpaRepository<ReservationEntity, Long> {
@ -24,5 +24,5 @@ interface ReservationRepository : JpaRepository<ReservationEntity, Long> {
WHERE WHERE
r.status = 'PENDING' AND r.created_at <= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE) r.status = 'PENDING' AND r.created_at <= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE)
""", nativeQuery = true) """, nativeQuery = true)
fun expirePendingReservations(@Param("now") now: LocalDateTime): Int fun expirePendingReservations(@Param("now") now: Instant): Int
} }

View File

@ -1,13 +1,13 @@
package com.sangdol.roomescape.reservation.web package com.sangdol.roomescape.reservation.web
import jakarta.validation.constraints.NotEmpty
import com.sangdol.roomescape.payment.web.PaymentWithDetailResponse import com.sangdol.roomescape.payment.web.PaymentWithDetailResponse
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationEntity import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationEntity
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
import com.sangdol.roomescape.schedule.web.ScheduleOverviewResponse import com.sangdol.roomescape.schedule.web.ScheduleOverviewResponse
import com.sangdol.roomescape.user.web.UserContactResponse import com.sangdol.roomescape.user.web.UserContactResponse
import jakarta.validation.constraints.NotEmpty
import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
data class PendingReservationCreateRequest( data class PendingReservationCreateRequest(
@ -79,7 +79,7 @@ data class ReservationDetailResponse(
val id: Long, val id: Long,
val reserver: ReserverInfo, val reserver: ReserverInfo,
val user: UserContactResponse, val user: UserContactResponse,
val applicationDateTime: LocalDateTime, val applicationDateTime: Instant,
val payment: PaymentWithDetailResponse?, val payment: PaymentWithDetailResponse?,
) )

View File

@ -3,8 +3,8 @@ package com.sangdol.roomescape.schedule.infrastructure.persistence
import com.sangdol.common.persistence.AuditingBaseEntity import com.sangdol.common.persistence.AuditingBaseEntity
import jakarta.persistence.* import jakarta.persistence.*
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
@Entity @Entity
@ -20,7 +20,7 @@ class ScheduleEntity(
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
var status: ScheduleStatus, var status: ScheduleStatus,
var holdExpiredAt: LocalDateTime? = null var holdExpiredAt: Instant? = null
) : AuditingBaseEntity(id) { ) : AuditingBaseEntity(id) {
fun modifyIfNotNull( fun modifyIfNotNull(
time: LocalTime?, time: LocalTime?,

View File

@ -7,8 +7,8 @@ import org.springframework.data.jpa.repository.Lock
import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param import org.springframework.data.repository.query.Param
import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> { interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
@ -108,7 +108,7 @@ interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
s.status = :changeStatus, s.status = :changeStatus,
s.holdExpiredAt = CASE s.holdExpiredAt = CASE
WHEN :changeStatus = com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus.HOLD WHEN :changeStatus = com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus.HOLD
THEN CURRENT_TIMESTAMP + 5 MINUTE THEN :expiredAt
ELSE NULL ELSE NULL
END END
WHERE WHERE
@ -117,7 +117,7 @@ interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
s.status = :currentStatus s.status = :currentStatus
""" """
) )
fun changeStatus(id: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus): Int fun changeStatus(id: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus, expiredAt: Instant = Instant.now().plusSeconds(5 * 60)): Int
@Modifying @Modifying
@Query( @Query(
@ -137,5 +137,5 @@ interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
) )
""" """
) )
fun releaseExpiredHolds(@Param("now") now: LocalDateTime): Int fun releaseExpiredHolds(@Param("now") now: Instant): Int
} }

View File

@ -22,19 +22,16 @@ import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus
import com.sangdol.roomescape.user.web.UserContactResponse import com.sangdol.roomescape.user.web.UserContactResponse
import io.kotest.core.test.TestCaseOrder import io.kotest.core.test.TestCaseOrder
import jakarta.persistence.EntityManager import jakarta.persistence.EntityManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ActiveProfiles
import java.sql.Timestamp import java.sql.Timestamp
import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.OffsetDateTime import java.time.ZoneId
@ActiveProfiles("test", "data") @ActiveProfiles("test", "data")
abstract class AbstractDataInitializer( abstract class AbstractDataInitializer(
@ -327,7 +324,7 @@ class UserDataInitializer : AbstractDataInitializer() {
} while (true) } while (true)
user.phone = newPhone user.phone = newPhone
user.updatedAt = LocalDateTime.now() user.updatedAt = Instant.now()
entityManager.merge(user) entityManager.merge(user)
} }
} }
@ -488,14 +485,17 @@ class ScheduleDataInitializer : AbstractDataInitializer() {
themeWithTimes.forEach { (theme, times) -> themeWithTimes.forEach { (theme, times) ->
val themeCreatedAt = theme.createdAt val themeCreatedAt = theme.createdAt
(1..3).forEach { (1..3).forEach {
val date = themeCreatedAt.toLocalDate().plusDays(it.toLong()) val themeCreatedDateTime = themeCreatedAt.atZone(ZoneId.systemDefault())
val themeCreatedDate = themeCreatedDateTime.toLocalDate().plusDays(it.toLong())
val themeCreatedTime = themeCreatedDateTime.toLocalTime()
times.forEach { time -> times.forEach { time ->
val storeId = store.first val storeId = store.first
val storeAdminId = store.second val storeAdminId = store.second
batchArgs.add( batchArgs.add(
arrayOf( arrayOf(
idGenerator.create(), storeId, theme.id, date, time, idGenerator.create(), storeId, theme.id, themeCreatedDate, time,
status, storeAdminId, storeAdminId, themeCreatedAt.plusHours(1), themeCreatedAt.plusHours(1) status, storeAdminId, storeAdminId, themeCreatedTime.plusHours(1), themeCreatedTime.plusHours(1)
) )
) )

View File

@ -195,8 +195,7 @@ private fun randomLocalDateTime(): String {
return LocalDateTime.of(year, month, day, hour, minute, second) return LocalDateTime.of(year, month, day, hour, minute, second)
.atZone(ZoneId.systemDefault()) .atZone(ZoneId.systemDefault())
.toOffsetDateTime() .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"))
} }
private fun generateBusinessRegNum(): String { private fun generateBusinessRegNum(): String {

View File

@ -13,7 +13,8 @@ import io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.JdbcTemplate
import java.time.LocalDateTime import java.time.Instant
import java.time.temporal.ChronoUnit
/** /**
* @see com.sangdol.roomescape.reservation.business.scheduler.IncompletedReservationScheduler * @see com.sangdol.roomescape.reservation.business.scheduler.IncompletedReservationScheduler
@ -31,7 +32,7 @@ class IncompletedReservationSchedulerTest(
test("예약이 없고, hold_expired_at 시간이 지난 ${ScheduleStatus.HOLD} 일정을 ${ScheduleStatus.AVAILABLE} 상태로 바꾼다.") { test("예약이 없고, hold_expired_at 시간이 지난 ${ScheduleStatus.HOLD} 일정을 ${ScheduleStatus.AVAILABLE} 상태로 바꾼다.") {
val schedule: ScheduleEntity = dummyInitializer.createSchedule().apply { val schedule: ScheduleEntity = dummyInitializer.createSchedule().apply {
this.status = ScheduleStatus.HOLD this.status = ScheduleStatus.HOLD
this.holdExpiredAt = LocalDateTime.now().minusSeconds(1) this.holdExpiredAt = Instant.now().minusSeconds(1)
}.also { }.also {
scheduleRepository.saveAndFlush(it) scheduleRepository.saveAndFlush(it)
} }
@ -52,16 +53,13 @@ class IncompletedReservationSchedulerTest(
jdbcTemplate.execute("UPDATE reservation SET created_at = DATE_SUB(NOW(), INTERVAL 5 MINUTE) WHERE id = ${it.id}") jdbcTemplate.execute("UPDATE reservation SET created_at = DATE_SUB(NOW(), INTERVAL 5 MINUTE) WHERE id = ${it.id}")
} }
val now = LocalDateTime.now()
transactionExecutionUtil.withNewTransaction(isReadOnly = false) { transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
incompletedReservationScheduler.processExpiredReservation() incompletedReservationScheduler.processExpiredReservation()
} }
assertSoftly(reservationRepository.findByIdOrNull(reservation.id)!!) { assertSoftly(reservationRepository.findByIdOrNull(reservation.id)!!) {
this.status shouldBe ReservationStatus.EXPIRED this.status shouldBe ReservationStatus.EXPIRED
this.updatedAt.hour shouldBe now.hour this.updatedAt.truncatedTo(ChronoUnit.MINUTES) shouldBe Instant.now().truncatedTo(ChronoUnit.MINUTES)
this.updatedAt.minute shouldBe now.minute
} }
assertSoftly(scheduleRepository.findByIdOrNull(reservation.scheduleId)!!) { assertSoftly(scheduleRepository.findByIdOrNull(reservation.scheduleId)!!) {

View File

@ -20,7 +20,7 @@ import kotlinx.coroutines.withContext
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDateTime import java.time.Instant
class ReservationConcurrencyTest( class ReservationConcurrencyTest(
private val transactionManager: PlatformTransactionManager, private val transactionManager: PlatformTransactionManager,
@ -35,7 +35,7 @@ class ReservationConcurrencyTest(
val user = testAuthUtil.defaultUserLogin().first val user = testAuthUtil.defaultUserLogin().first
val schedule = dummyInitializer.createSchedule().also { val schedule = dummyInitializer.createSchedule().also {
it.status = ScheduleStatus.HOLD it.status = ScheduleStatus.HOLD
it.holdExpiredAt = LocalDateTime.now().minusMinutes(1) it.holdExpiredAt = Instant.now().minusSeconds(1 * 60)
scheduleRepository.save(it) scheduleRepository.save(it)
} }
lateinit var response: PendingReservationCreateResponse lateinit var response: PendingReservationCreateResponse