diff --git a/src/main/kotlin/io/ticketaka/api/common/infrastructure/event/EventDispatcher.kt b/src/main/kotlin/io/ticketaka/api/common/infrastructure/event/EventDispatcher.kt index 9609f1c..2fc6947 100644 --- a/src/main/kotlin/io/ticketaka/api/common/infrastructure/event/EventDispatcher.kt +++ b/src/main/kotlin/io/ticketaka/api/common/infrastructure/event/EventDispatcher.kt @@ -1,16 +1,19 @@ package io.ticketaka.api.common.infrastructure.event import io.ticketaka.api.common.domain.DomainEvent +import io.ticketaka.api.concert.infrastructure.event.ReservationCreateEventConsumer import io.ticketaka.api.point.domain.PointChargeEvent import io.ticketaka.api.point.domain.PointRechargeEvent import io.ticketaka.api.point.infrastructure.event.PointChargeEventConsumer import io.ticketaka.api.point.infrastructure.event.PointRechargeEventConsumer +import io.ticketaka.api.reservation.domain.reservation.ReservationCreateEvent import org.springframework.stereotype.Component @Component class EventDispatcher( private val pointRechargeEventConsumer: PointRechargeEventConsumer, private val pointChargeEventConsumer: PointChargeEventConsumer, + private val reservationCreateEventConsumer: ReservationCreateEventConsumer, ) { fun dispatch(event: DomainEvent) { when (event) { @@ -20,6 +23,9 @@ class EventDispatcher( is PointChargeEvent -> { pointChargeEventConsumer.offer(event) } + is ReservationCreateEvent -> { + reservationCreateEventConsumer.offer(event) + } } } } diff --git a/src/main/kotlin/io/ticketaka/api/concert/application/ConcertSeatService.kt b/src/main/kotlin/io/ticketaka/api/concert/application/ConcertSeatService.kt index 2c558cf..9dc01a8 100644 --- a/src/main/kotlin/io/ticketaka/api/concert/application/ConcertSeatService.kt +++ b/src/main/kotlin/io/ticketaka/api/concert/application/ConcertSeatService.kt @@ -1,19 +1,13 @@ package io.ticketaka.api.concert.application -import io.ticketaka.api.common.exception.BadClientRequestException import io.ticketaka.api.concert.application.dto.SeatResult -import io.ticketaka.api.concert.domain.Concert import io.ticketaka.api.concert.domain.ConcertRepository -import io.ticketaka.api.concert.domain.Seat -import io.ticketaka.api.concert.domain.SeatRepository import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @Service class ConcertSeatService( private val concertCacheAsideQueryService: ConcertCacheAsideQueryService, - private val seatRepository: SeatRepository, private val concertRepository: ConcertRepository, ) { fun getSeatNumbers(date: LocalDate): List { @@ -21,44 +15,6 @@ class ConcertSeatService( return concertCacheAsideQueryService.getConcertSeatNumbers(concert.id).map { SeatResult(it.number, it.status) } } - @Transactional(readOnly = true) - fun getAvailableConcert(date: LocalDate): Concert { - val concert = - concertRepository.findByDate(date) - ?: throw BadClientRequestException("해당 날짜의 콘서트가 없습니다.") - return concert - } - - @Transactional(readOnly = true) - fun getAvailableSeats( - date: LocalDate, - seatNumbers: List, - ): Set { - val concert = - concertRepository.findByDate(date) - ?: throw BadClientRequestException("해당 날짜의 콘서트가 없습니다.") - val seats = seatRepository.findSeatsByConcertDateAndNumberInOrderByNumber(concert.date, seatNumbers) - seats.forEach { seat -> - if (seat.status != Seat.Status.AVAILABLE) { - throw BadClientRequestException("이미 예약된 좌석입니다.") - } - } - return seats - } - - @Transactional - fun reserveSeat( - concertId: Long, - seatNumbers: List, - ): Set { - val seats = - seatRepository.findSeatsByConcertIdAndNumberInOrderByNumberForUpdate(concertId, seatNumbers) - seats.forEach { seat -> - seat.reserve() - } - return seats - } - fun getDates(): List { return concertRepository.findAllDate().sorted() } diff --git a/src/main/kotlin/io/ticketaka/api/concert/domain/ConcertSeatUpdater.kt b/src/main/kotlin/io/ticketaka/api/concert/domain/ConcertSeatUpdater.kt new file mode 100644 index 0000000..7079841 --- /dev/null +++ b/src/main/kotlin/io/ticketaka/api/concert/domain/ConcertSeatUpdater.kt @@ -0,0 +1,17 @@ +package io.ticketaka.api.concert.domain + +import java.time.LocalDate + +interface ConcertSeatUpdater { + fun reserve( + concertId: Long, + date: LocalDate, + seatNumbers: List, + ): Set + + fun confirm( + concertId: Long, + date: String, + seatNumbers: List, + ) +} diff --git a/src/main/kotlin/io/ticketaka/api/concert/domain/SeatRepository.kt b/src/main/kotlin/io/ticketaka/api/concert/domain/SeatRepository.kt index a0ed147..122da93 100644 --- a/src/main/kotlin/io/ticketaka/api/concert/domain/SeatRepository.kt +++ b/src/main/kotlin/io/ticketaka/api/concert/domain/SeatRepository.kt @@ -12,6 +12,8 @@ interface SeatRepository { numbers: List, ): Set + fun findByIdsOrderByNumberForUpdate(ids: List): Set + fun findSeatsByConcertDateAndNumberInOrderByNumber( date: LocalDate, numbers: List, diff --git a/src/main/kotlin/io/ticketaka/api/concert/infrastructure/InMemoryCacheConcertSeatUpdater.kt b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/InMemoryCacheConcertSeatUpdater.kt new file mode 100644 index 0000000..81efd9e --- /dev/null +++ b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/InMemoryCacheConcertSeatUpdater.kt @@ -0,0 +1,42 @@ +package io.ticketaka.api.concert.infrastructure + +import io.ticketaka.api.common.exception.NotFoundException +import io.ticketaka.api.concert.domain.ConcertSeatUpdater +import io.ticketaka.api.concert.domain.Seat +import io.ticketaka.api.concert.domain.SeatRepository +import org.springframework.cache.caffeine.CaffeineCacheManager +import org.springframework.stereotype.Component +import java.time.LocalDate + +@Component +class InMemoryCacheConcertSeatUpdater( + private val caffeineCacheManager: CaffeineCacheManager, + private val seatRepository: SeatRepository, +) : ConcertSeatUpdater { + override fun reserve( + concertId: Long, + date: LocalDate, + seatNumbers: List, + ): Set { + val cache = caffeineCacheManager.getCache("seatNumbers") ?: throw NotFoundException("콘서트별 좌석 캐시가 존재하지 않습니다.") + synchronized(cache) { + var seats = cache.get(concertId) { setOf() } as Set + if (seats.isEmpty()) { + seats = seatRepository.findByConcertId(concertId) + } + seats.sortedBy { it.number } + .filter { seatNumbers.contains(it.number) } + .forEach { it.reserve() } + cache.put(concertId, seats) + return seats.filter { seatNumbers.contains(it.number) }.toSet() + } + } + + override fun confirm( + concertId: Long, + date: String, + seatNumbers: List, + ) { + println("Confirm concert seat") + } +} diff --git a/src/main/kotlin/io/ticketaka/api/concert/infrastructure/event/DBSeatStatusManger.kt b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/event/DBSeatStatusManger.kt new file mode 100644 index 0000000..24305c1 --- /dev/null +++ b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/event/DBSeatStatusManger.kt @@ -0,0 +1,16 @@ +package io.ticketaka.api.concert.infrastructure.event + +import io.ticketaka.api.concert.domain.SeatRepository +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class DBSeatStatusManger( + private val seatRepository: SeatRepository, +) { + @Transactional + fun reserve(seatIds: List) { + val seats = seatRepository.findByIdsOrderByNumberForUpdate(seatIds) + seats.forEach { it.reserve() } + } +} diff --git a/src/main/kotlin/io/ticketaka/api/concert/infrastructure/event/ReservationCreateEventConsumer.kt b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/event/ReservationCreateEventConsumer.kt new file mode 100644 index 0000000..9a94027 --- /dev/null +++ b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/event/ReservationCreateEventConsumer.kt @@ -0,0 +1,79 @@ +package io.ticketaka.api.concert.infrastructure.event + +import io.ticketaka.api.reservation.domain.reservation.Reservation +import io.ticketaka.api.reservation.domain.reservation.ReservationCreateEvent +import io.ticketaka.api.reservation.domain.reservation.ReservationRepository +import io.ticketaka.api.reservation.domain.reservation.ReservationSeatAllocator +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import org.springframework.util.StopWatch +import java.util.concurrent.LinkedBlockingDeque +import kotlin.concurrent.thread + +@Component +class ReservationCreateEventConsumer( + private val reservationRepository: ReservationRepository, + private val reservationSeatAllocator: ReservationSeatAllocator, + private val seatStatusManger: DBSeatStatusManger, +) { + private val logger = LoggerFactory.getLogger(javaClass) + private val eventQueue = LinkedBlockingDeque() + + init { + startEventConsumer() + } + + fun consume(events: MutableList) { + events.forEach { event -> + seatStatusManger.reserve(event.seatIds) + val reservation = + reservationRepository.save( + Reservation.createPendingReservation( + userId = event.userId, + concertId = event.concertId, + ), + ) + reservationSeatAllocator.allocate( + reservationId = reservation.id, + seatIds = event.seatIds, + ) + } + } + + fun offer(event: ReservationCreateEvent) { + eventQueue.add(event) + } + + private fun startEventConsumer() { + thread( + start = true, + isDaemon = true, + name = "reservationEventConsumer", + ) { + while (true) { + val stopWatch = StopWatch() + stopWatch.start() + var processingTime = 1000L + val currentThread = Thread.currentThread() + while (currentThread.state.name == Thread.State.WAITING.name) { + logger.info(currentThread.state.name) + Thread.sleep(processingTime) + } + if (eventQueue.isNotEmpty()) { + val events = mutableListOf() + var quantity = 1000 + while (eventQueue.isNotEmpty().and(quantity > 0)) { + quantity-- + eventQueue.poll()?.let { events.add(it) } + } + consume(events) + stopWatch.stop() + processingTime = stopWatch.totalTimeMillis + logger.debug("reservationEventConsumer consume ${events.size} events, cost ${processingTime}ms") + } else { + Thread.sleep(5000) + } + } + } + } +} diff --git a/src/main/kotlin/io/ticketaka/api/concert/infrastructure/jpa/JpaSeatRepository.kt b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/jpa/JpaSeatRepository.kt index 858ced3..dc77491 100644 --- a/src/main/kotlin/io/ticketaka/api/concert/infrastructure/jpa/JpaSeatRepository.kt +++ b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/jpa/JpaSeatRepository.kt @@ -14,6 +14,9 @@ interface JpaSeatRepository : JpaRepository { seatNumbers: List, ): List + @Lock(LockModeType.PESSIMISTIC_WRITE) + fun findByIdInOrderByNumber(ids: List): List + @Lock(LockModeType.PESSIMISTIC_WRITE) fun findSeatsByConcertDateAndNumberInOrderByNumber( date: LocalDate, diff --git a/src/main/kotlin/io/ticketaka/api/concert/infrastructure/persistence/SeatRepositoryComposition.kt b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/persistence/SeatRepositoryComposition.kt index c99c6c3..a3abc45 100644 --- a/src/main/kotlin/io/ticketaka/api/concert/infrastructure/persistence/SeatRepositoryComposition.kt +++ b/src/main/kotlin/io/ticketaka/api/concert/infrastructure/persistence/SeatRepositoryComposition.kt @@ -26,6 +26,10 @@ class SeatRepositoryComposition( return jpaSeatRepository.findSeatsByConcertDateAndNumberIn(date, numbers).toSet() } + override fun findByIdsOrderByNumberForUpdate(ids: List): Set { + return jpaSeatRepository.findByIdInOrderByNumber(ids).toSet() + } + override fun findSeatsByConcertDateAndNumberInOrderByNumber( date: LocalDate, numbers: List, diff --git a/src/main/kotlin/io/ticketaka/api/point/application/PaymentService.kt b/src/main/kotlin/io/ticketaka/api/point/application/PaymentService.kt index 110848f..2d275f4 100644 --- a/src/main/kotlin/io/ticketaka/api/point/application/PaymentService.kt +++ b/src/main/kotlin/io/ticketaka/api/point/application/PaymentService.kt @@ -3,35 +3,17 @@ package io.ticketaka.api.point.application import io.ticketaka.api.point.application.dto.PaymentCommand import io.ticketaka.api.point.domain.payment.Payment import io.ticketaka.api.point.domain.payment.PaymentRepository -import org.springframework.context.ApplicationEventPublisher -import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional @Service @Transactional(readOnly = true) class PaymentService( private val paymentRepository: PaymentRepository, - private val pointCacheAsideQueryService: PointCacheAsideQueryService, - private val applicationEventPublisher: ApplicationEventPublisher, ) { @Transactional fun paymentApproval(paymentCommand: PaymentCommand) { Thread.sleep((500..1000).random().toLong()) // PG 승인 요청 시간 대기 paymentRepository.save(Payment.newInstance(paymentCommand.amount, paymentCommand.userId, paymentCommand.pointId)) } - - @Async - @Transactional(propagation = Propagation.NESTED) - fun paymentApprovalAsync(paymentCommand: PaymentCommand) { - try { - Thread.sleep((500..1000).random().toLong()) // PG 승인 요청 시간 대기 - val payment = Payment.newInstance(paymentCommand.amount, paymentCommand.userId, paymentCommand.pointId) - paymentRepository.save(payment) - payment.pollAllEvents().forEach { applicationEventPublisher.publishEvent(it) } - } catch (e: Exception) { - e.printStackTrace() - } - } } diff --git a/src/main/kotlin/io/ticketaka/api/point/application/PointService.kt b/src/main/kotlin/io/ticketaka/api/point/application/PointService.kt index 912ce81..924c5bb 100644 --- a/src/main/kotlin/io/ticketaka/api/point/application/PointService.kt +++ b/src/main/kotlin/io/ticketaka/api/point/application/PointService.kt @@ -3,7 +3,7 @@ package io.ticketaka.api.point.application import io.ticketaka.api.common.exception.NotFoundException import io.ticketaka.api.point.application.dto.BalanceQueryModel import io.ticketaka.api.point.application.dto.RechargeCommand -import io.ticketaka.api.point.domain.PointBalanceCacheUpdater +import io.ticketaka.api.point.domain.PointBalanceUpdater import io.ticketaka.api.point.domain.PointRechargeEvent import io.ticketaka.api.point.domain.PointRepository import io.ticketaka.api.user.application.TokenUserCacheAsideQueryService @@ -15,7 +15,7 @@ import org.springframework.transaction.annotation.Transactional class PointService( private val tokenUserCacheAsideQueryService: TokenUserCacheAsideQueryService, private val pointCacheAsideQueryService: PointCacheAsideQueryService, - private val pointBalanceCacheUpdater: PointBalanceCacheUpdater, + private val pointBalanceUpdater: PointBalanceUpdater, private val applicationEventPublisher: ApplicationEventPublisher, private val pointRepository: PointRepository, ) { @@ -23,7 +23,7 @@ class PointService( fun recharge(rechargeCommand: RechargeCommand) { val user = tokenUserCacheAsideQueryService.getUser(rechargeCommand.userId) val point = pointCacheAsideQueryService.getPoint(user.pointId) - pointBalanceCacheUpdater.recharge(point.id, rechargeCommand.amount) + pointBalanceUpdater.recharge(point.id, rechargeCommand.amount) applicationEventPublisher.publishEvent(PointRechargeEvent(user.id, point.id, rechargeCommand.amount)) } diff --git a/src/main/kotlin/io/ticketaka/api/point/domain/PointBalanceCacheUpdater.kt b/src/main/kotlin/io/ticketaka/api/point/domain/PointBalanceUpdater.kt similarity index 85% rename from src/main/kotlin/io/ticketaka/api/point/domain/PointBalanceCacheUpdater.kt rename to src/main/kotlin/io/ticketaka/api/point/domain/PointBalanceUpdater.kt index ec10d96..61ddd56 100644 --- a/src/main/kotlin/io/ticketaka/api/point/domain/PointBalanceCacheUpdater.kt +++ b/src/main/kotlin/io/ticketaka/api/point/domain/PointBalanceUpdater.kt @@ -2,7 +2,7 @@ package io.ticketaka.api.point.domain import java.math.BigDecimal -interface PointBalanceCacheUpdater { +interface PointBalanceUpdater { fun recharge( pointId: Long, amount: BigDecimal, diff --git a/src/main/kotlin/io/ticketaka/api/point/infrastructure/InMemoryPointBalanceCacheUpdater.kt b/src/main/kotlin/io/ticketaka/api/point/infrastructure/InMemoryCachePointBalanceUpdater.kt similarity index 89% rename from src/main/kotlin/io/ticketaka/api/point/infrastructure/InMemoryPointBalanceCacheUpdater.kt rename to src/main/kotlin/io/ticketaka/api/point/infrastructure/InMemoryCachePointBalanceUpdater.kt index 11aa164..6e115c6 100644 --- a/src/main/kotlin/io/ticketaka/api/point/infrastructure/InMemoryPointBalanceCacheUpdater.kt +++ b/src/main/kotlin/io/ticketaka/api/point/infrastructure/InMemoryCachePointBalanceUpdater.kt @@ -2,15 +2,15 @@ package io.ticketaka.api.point.infrastructure import io.ticketaka.api.common.exception.NotFoundException import io.ticketaka.api.point.domain.Point -import io.ticketaka.api.point.domain.PointBalanceCacheUpdater +import io.ticketaka.api.point.domain.PointBalanceUpdater import org.springframework.cache.caffeine.CaffeineCacheManager import org.springframework.stereotype.Component import java.math.BigDecimal @Component -class InMemoryPointBalanceCacheUpdater( +class InMemoryCachePointBalanceUpdater( private val caffeineCacheManager: CaffeineCacheManager, -) : PointBalanceCacheUpdater { +) : PointBalanceUpdater { override fun recharge( pointId: Long, amount: BigDecimal, diff --git a/src/main/kotlin/io/ticketaka/api/point/infrastructure/event/PointRechargeEventConsumer.kt b/src/main/kotlin/io/ticketaka/api/point/infrastructure/event/PointRechargeEventConsumer.kt index 467abf8..3d6e924 100644 --- a/src/main/kotlin/io/ticketaka/api/point/infrastructure/event/PointRechargeEventConsumer.kt +++ b/src/main/kotlin/io/ticketaka/api/point/infrastructure/event/PointRechargeEventConsumer.kt @@ -4,7 +4,6 @@ import io.ticketaka.api.point.application.PointService import io.ticketaka.api.point.domain.PointHistory import io.ticketaka.api.point.domain.PointHistoryRepository import io.ticketaka.api.point.domain.PointRechargeEvent -import io.ticketaka.api.point.domain.PointRepository import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import org.springframework.util.StopWatch @@ -13,7 +12,6 @@ import kotlin.concurrent.thread @Component class PointRechargeEventConsumer( - private val pointRepository: PointRepository, private val pointHistoryRepository: PointHistoryRepository, private val pointService: PointService, ) { @@ -49,7 +47,7 @@ class PointRechargeEventConsumer( thread( start = true, isDaemon = true, - name = "PointRechargeEventConsumer", + name = "pointEventConsumer", ) { while (true) { val stopWatch = StopWatch() diff --git a/src/main/kotlin/io/ticketaka/api/reservation/application/ReservationService.kt b/src/main/kotlin/io/ticketaka/api/reservation/application/ReservationService.kt index c67761d..4522d79 100644 --- a/src/main/kotlin/io/ticketaka/api/reservation/application/ReservationService.kt +++ b/src/main/kotlin/io/ticketaka/api/reservation/application/ReservationService.kt @@ -1,35 +1,30 @@ package io.ticketaka.api.reservation.application +import io.ticketaka.api.common.domain.EventBroker import io.ticketaka.api.common.exception.NotFoundException -import io.ticketaka.api.concert.application.ConcertSeatService -import io.ticketaka.api.concert.infrastructure.cache.ConcertSeatCacheRefresher +import io.ticketaka.api.concert.application.ConcertCacheAsideQueryService +import io.ticketaka.api.concert.domain.ConcertSeatUpdater import io.ticketaka.api.reservation.application.dto.CreateReservationCommand -import io.ticketaka.api.reservation.domain.reservation.Reservation +import io.ticketaka.api.reservation.domain.reservation.ReservationCreateEvent import io.ticketaka.api.reservation.domain.reservation.ReservationRepository -import io.ticketaka.api.reservation.domain.reservation.ReservationSeatAllocator import io.ticketaka.api.user.application.TokenUserCacheAsideQueryService import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service -@Transactional(readOnly = true) class ReservationService( private val tokenUserCacheAsideQueryService: TokenUserCacheAsideQueryService, - private val concertSeatService: ConcertSeatService, + private val concertCacheAsideQueryService: ConcertCacheAsideQueryService, private val reservationRepository: ReservationRepository, - private val reservationSeatAllocator: ReservationSeatAllocator, - private val concertSeatCacheRefresher: ConcertSeatCacheRefresher, + private val concertSeatUpdater: ConcertSeatUpdater, + private val eventBroker: EventBroker, ) { - @Async - @Transactional fun createReservation(command: CreateReservationCommand) { val user = tokenUserCacheAsideQueryService.getUser(command.userId) - val concert = concertSeatService.getAvailableConcert(command.date) - val seats = concertSeatService.reserveSeat(concert.id, command.seatNumbers) - val reservation = reservationRepository.save(Reservation.createPendingReservation(user.id, concert.id)) - reservationSeatAllocator.allocate(reservation.id, seats.map { it.id }) - concertSeatCacheRefresher.refresh(concert.id) + val concert = concertCacheAsideQueryService.getConcert(command.date) + val seats = concertSeatUpdater.reserve(concert.id, command.date, command.seatNumbers) + eventBroker.produce(ReservationCreateEvent(user.id, concert.id, seats.map { it.id })) } @Async diff --git a/src/main/kotlin/io/ticketaka/api/reservation/application/dto/ConfirmReservationResult.kt b/src/main/kotlin/io/ticketaka/api/reservation/application/dto/ConfirmReservationResult.kt deleted file mode 100644 index 2baf22c..0000000 --- a/src/main/kotlin/io/ticketaka/api/reservation/application/dto/ConfirmReservationResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.ticketaka.api.reservation.application.dto - -import io.ticketaka.api.reservation.domain.reservation.Reservation - -data class ConfirmReservationResult( - val reservationTsid: String, - val status: Reservation.Status, -) diff --git a/src/main/kotlin/io/ticketaka/api/reservation/application/dto/CreateReservationResult.kt b/src/main/kotlin/io/ticketaka/api/reservation/application/dto/CreateReservationResult.kt deleted file mode 100644 index 9146673..0000000 --- a/src/main/kotlin/io/ticketaka/api/reservation/application/dto/CreateReservationResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.ticketaka.api.reservation.application.dto - -import io.ticketaka.api.reservation.domain.reservation.Reservation -import java.time.LocalDateTime - -data class CreateReservationResult( - val reservationTsid: String, - val status: Reservation.Status, - val expiration: LocalDateTime, -) diff --git a/src/main/kotlin/io/ticketaka/api/reservation/domain/reservation/ReservationCreateEvent.kt b/src/main/kotlin/io/ticketaka/api/reservation/domain/reservation/ReservationCreateEvent.kt new file mode 100644 index 0000000..8a98c1f --- /dev/null +++ b/src/main/kotlin/io/ticketaka/api/reservation/domain/reservation/ReservationCreateEvent.kt @@ -0,0 +1,15 @@ +package io.ticketaka.api.reservation.domain.reservation + +import io.ticketaka.api.common.domain.DomainEvent +import java.time.LocalDateTime + +data class ReservationCreateEvent( + val userId: Long, + val concertId: Long, + val seatIds: List, + val occurredOn: LocalDateTime = LocalDateTime.now(), +) : DomainEvent { + override fun occurredOn(): LocalDateTime { + return this.occurredOn + } +} diff --git a/src/main/kotlin/io/ticketaka/api/reservation/infrastructure/persistence/ReservationSeatAllocatorImpl.kt b/src/main/kotlin/io/ticketaka/api/reservation/infrastructure/persistence/ReservationSeatAllocatorImpl.kt index a6da97c..fbae650 100644 --- a/src/main/kotlin/io/ticketaka/api/reservation/infrastructure/persistence/ReservationSeatAllocatorImpl.kt +++ b/src/main/kotlin/io/ticketaka/api/reservation/infrastructure/persistence/ReservationSeatAllocatorImpl.kt @@ -4,11 +4,13 @@ import io.ticketaka.api.reservation.domain.reservation.ReservationSeat import io.ticketaka.api.reservation.domain.reservation.ReservationSeatAllocator import io.ticketaka.api.reservation.domain.reservation.ReservationSeatRepository import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional @Component class ReservationSeatAllocatorImpl( private val reservationSeatRepository: ReservationSeatRepository, ) : ReservationSeatAllocator { + @Transactional override fun allocate( reservationId: Long, seatIds: List, diff --git a/src/test/kotlin/io/ticketaka/api/concert/application/ConcertSeatServiceTest.kt b/src/test/kotlin/io/ticketaka/api/concert/application/ConcertSeatServiceTest.kt index cca83b4..a02dcc6 100644 --- a/src/test/kotlin/io/ticketaka/api/concert/application/ConcertSeatServiceTest.kt +++ b/src/test/kotlin/io/ticketaka/api/concert/application/ConcertSeatServiceTest.kt @@ -23,7 +23,7 @@ class ConcertSeatServiceTest { mock { on { findAllDate() } doReturn setOf(LocalDate.of(2024, 4, 1)) } - val concertSeatService = ConcertSeatService(mock(), mock(), mockConcertRepository) + val concertSeatService = ConcertSeatService(mock(), mockConcertRepository) // when val result = concertSeatService.getDates() @@ -39,7 +39,7 @@ class ConcertSeatServiceTest { mock { on { findAllDate() } doReturn emptySet() } - val concertSeatService = ConcertSeatService(mock(), mock(), mockConcertRepository) + val concertSeatService = ConcertSeatService(mock(), mockConcertRepository) // when val result = concertSeatService.getDates() @@ -60,7 +60,7 @@ class ConcertSeatServiceTest { on { getConcert(any()) } doReturn concert on { getConcertSeatNumbers(any()) } doReturn setOf(seat) } - val concertSeatService = ConcertSeatService(mockConcertCacheAsideQueryService, mock(), mock()) + val concertSeatService = ConcertSeatService(mockConcertCacheAsideQueryService, mock()) // when val result = concertSeatService.getSeatNumbers(date) @@ -79,7 +79,7 @@ class ConcertSeatServiceTest { on { getConcert(any()) } doReturn concert on { getConcertSeatNumbers(any()) } doReturn emptySet() } - val concertSeatService = ConcertSeatService(mockConcertCacheAsideQueryService, mock(), mock()) + val concertSeatService = ConcertSeatService(mockConcertCacheAsideQueryService, mock()) // when val result = concertSeatService.getSeatNumbers(date) diff --git a/src/test/kotlin/io/ticketaka/api/payment/application/PaymentServiceTest.kt b/src/test/kotlin/io/ticketaka/api/payment/application/PaymentServiceTest.kt index 35345aa..d7680cc 100644 --- a/src/test/kotlin/io/ticketaka/api/payment/application/PaymentServiceTest.kt +++ b/src/test/kotlin/io/ticketaka/api/payment/application/PaymentServiceTest.kt @@ -30,7 +30,7 @@ class PaymentServiceTest { on { save(any()) } doReturn Payment.newInstance(1000.toBigDecimal(), userId, pointId) } - val paymentService = PaymentService(mockPaymentRepository, mock(), mock()) + val paymentService = PaymentService(mockPaymentRepository) val paymentCommand = PaymentCommand( userId = userId, diff --git a/src/test/kotlin/io/ticketaka/api/point/application/PointServiceTest.kt b/src/test/kotlin/io/ticketaka/api/point/application/PointServiceTest.kt index 74ff21e..76144d5 100644 --- a/src/test/kotlin/io/ticketaka/api/point/application/PointServiceTest.kt +++ b/src/test/kotlin/io/ticketaka/api/point/application/PointServiceTest.kt @@ -3,7 +3,7 @@ package io.ticketaka.api.point.application import io.ticketaka.api.common.exception.BadClientRequestException import io.ticketaka.api.point.application.dto.RechargeCommand import io.ticketaka.api.point.domain.Point -import io.ticketaka.api.point.domain.PointBalanceCacheUpdater +import io.ticketaka.api.point.domain.PointBalanceUpdater import io.ticketaka.api.user.application.TokenUserCacheAsideQueryService import io.ticketaka.api.user.domain.User import org.junit.jupiter.api.Assertions.assertEquals @@ -41,15 +41,15 @@ class PointServiceTest { on { getPoint(any()) } doReturn point } - val pointBalanceCacheUpdater = - mock { + val pointBalanceUpdater = + mock { on { recharge(point.id, rechargeCommand.amount) } doAnswer { point.recharge(rechargeCommand.amount) } } val pointService = - PointService(tokenUserCacheAsideQueryService, pointCacheAsideQueryService, pointBalanceCacheUpdater, mock(), mock()) + PointService(tokenUserCacheAsideQueryService, pointCacheAsideQueryService, pointBalanceUpdater, mock(), mock()) // when pointService.recharge(rechargeCommand) @@ -79,15 +79,15 @@ class PointServiceTest { mock { on { getPoint(any()) } doReturn point } - val pointBalanceCacheUpdater = - mock { + val pointBalanceUpdater = + mock { on { recharge(point.id, rechargeCommand.amount) } doAnswer { point.recharge(rechargeCommand.amount) } } val pointService = - PointService(tokenUserCacheAsideQueryService, pointCacheAsideQueryService, pointBalanceCacheUpdater, mock(), mock()) + PointService(tokenUserCacheAsideQueryService, pointCacheAsideQueryService, pointBalanceUpdater, mock(), mock()) // when val exception = @@ -112,9 +112,9 @@ class PointServiceTest { mock { on { getPoint(any()) } doReturn point } - val pointBalanceCacheUpdater = mock() + val pointBalanceUpdater = mock() val pointService = - PointService(tokenUserCacheAsideQueryService, pointCacheAsideQueryService, pointBalanceCacheUpdater, mock(), mock()) + PointService(tokenUserCacheAsideQueryService, pointCacheAsideQueryService, pointBalanceUpdater, mock(), mock()) // when val balanceQueryModel = pointService.getBalance(user.id) diff --git a/src/test/kotlin/io/ticketaka/api/reservation/application/ReservationServiceTest.kt b/src/test/kotlin/io/ticketaka/api/reservation/application/ReservationServiceTest.kt index 4162f36..3842397 100644 --- a/src/test/kotlin/io/ticketaka/api/reservation/application/ReservationServiceTest.kt +++ b/src/test/kotlin/io/ticketaka/api/reservation/application/ReservationServiceTest.kt @@ -2,11 +2,10 @@ package io.ticketaka.api.reservation.application import io.ticketaka.api.common.exception.BadClientRequestException import io.ticketaka.api.common.exception.NotFoundException -import io.ticketaka.api.concert.application.ConcertSeatService +import io.ticketaka.api.concert.application.ConcertCacheAsideQueryService import io.ticketaka.api.concert.domain.Concert -import io.ticketaka.api.concert.domain.ConcertRepository +import io.ticketaka.api.concert.domain.ConcertSeatUpdater import io.ticketaka.api.concert.domain.Seat -import io.ticketaka.api.concert.domain.SeatRepository import io.ticketaka.api.point.domain.Point import io.ticketaka.api.reservation.application.dto.CreateReservationCommand import io.ticketaka.api.reservation.domain.reservation.Reservation @@ -42,23 +41,22 @@ class ReservationServiceTest { on { getUser(any()) } doReturn user } - val mockReservationRepository = - mock { - on { save(any()) } doReturn reservation + val mockConcertSeatCacheAsideQueryService = + mock { + on { getConcert(date) } doReturn concert } - val concertSeatService = - mock { - on { getAvailableConcert(date) } doReturn concert - on { reserveSeat(concert.id, seats.map { it.number }.toList()) } doReturn seats + val mockConcertUpdater = + mock { + on { reserve(concert.id, date, seats.map { it.number }.toList()) } doReturn seats } val reservationService = ReservationService( mockTokenUserCacheAsideQueryService, - concertSeatService, - mockReservationRepository, + mockConcertSeatCacheAsideQueryService, mock(), + mockConcertUpdater, mock(), ) @@ -80,7 +78,6 @@ class ReservationServiceTest { val seat = Seat.newInstance(seatNumber, 1000.toBigDecimal(), concert.id) seat.occupy() val seats = setOf(seat) - val mockReservationRepository = mock() val point = Point.newInstance(10000.toBigDecimal()) val user = User.newInstance(point.id) @@ -89,18 +86,22 @@ class ReservationServiceTest { mock { on { getUser(any()) } doReturn user } - val concertSeatService = - mock { - on { getAvailableConcert(date) } doReturn concert - on { reserveSeat(concert.id, seats.map { it.number }.toList()) } doThrow BadClientRequestException("이미 예약된 좌석입니다.") + val mockConcertSeatCacheAsideQueryService = + mock { + on { getConcert(date) } doReturn concert + } + + val mockConcertUpdater = + mock { + on { reserve(concert.id, date, seats.map { it.number }.toList()) } doThrow BadClientRequestException("이미 예약된 좌석입니다.") } val reservationService = ReservationService( mockTokenUserCacheAsideQueryService, - concertSeatService, - mockReservationRepository, + mockConcertSeatCacheAsideQueryService, mock(), + mockConcertUpdater, mock(), ) @@ -126,19 +127,19 @@ class ReservationServiceTest { seat.occupy() val notFoundConcertErrorMessage = "콘서트를 찾을 수 없습니다." - val mockConcertRepository = - mock { - on { findByDate(date) } doThrow NotFoundException(notFoundConcertErrorMessage) - } - val mockSeatRepository = mock() + val mockReservationRepository = mock() val mockTokenUserCacheAsideQueryService = mock() + val mockConcertSeatCacheAsideQueryService = + mock { + on { getConcert(date) } doThrow NotFoundException(notFoundConcertErrorMessage) + } val reservationService = ReservationService( mockTokenUserCacheAsideQueryService, - ConcertSeatService(mock(), mockSeatRepository, mockConcertRepository), + mockConcertSeatCacheAsideQueryService, mockReservationRepository, mock(), mock(),