Skip to content

Commit

Permalink
Merge pull request #17 from mingj7235/feature/cache-for-read-concert
Browse files Browse the repository at this point in the history
Cache 도입 - 콘서트 조회, 콘서트 스케쥴 조회에 Cache 를 도입한다.
  • Loading branch information
mingj7235 authored Aug 3, 2024
2 parents 109eb26 + 06c2dfd commit 38964a5
Show file tree
Hide file tree
Showing 37 changed files with 900 additions and 1,399 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package com.hhplus.concert.business.application.dto

import com.hhplus.concert.common.type.QueueStatus
import java.time.LocalDateTime

class QueueServiceDto {
data class IssuedToken(
val token: String,
val createdAt: LocalDateTime,
)

data class Queue(
val queueId: Long,
val joinAt: LocalDateTime,
val status: QueueStatus,
val remainingWaitListCount: Int,
val remainingWaitListCount: Long,
val estimatedWaitTime: Long,
)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.hhplus.concert.business.application.service

import com.hhplus.concert.business.application.dto.ConcertServiceDto
import com.hhplus.concert.business.domain.manager.ConcertManager
import com.hhplus.concert.business.domain.manager.QueueManager
import com.hhplus.concert.business.domain.manager.concert.ConcertManager
import com.hhplus.concert.business.domain.manager.queue.QueueManager
import com.hhplus.concert.common.config.CacheConfig
import com.hhplus.concert.common.error.code.ErrorCode
import com.hhplus.concert.common.error.exception.BusinessException
import com.hhplus.concert.common.type.QueueStatus
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service

@Service
Expand All @@ -17,6 +19,12 @@ class ConcertService(
* 1. token 을 통해 queue 상태를 검증한다. (processing 상태여야한다.)
* 2. 현재 예약이 가능한 전체 concert 리스트를 dto 로 변환하여 리턴한다.
*/
@Cacheable(
cacheNames = [CacheConfig.FIVE_MIN_CACHE],
key = "'available-concert'",
condition = "#token != null",
sync = true,
)
fun getAvailableConcerts(token: String): List<ConcertServiceDto.Concert> {
validateQueueStatus(token)
return concertManager
Expand All @@ -35,6 +43,12 @@ class ConcertService(
* 2. 현재 예약 가능한 concert 인지 확인하고, 해당 concertSchedule 을 조회한다.
* 3. dto 형태로 변환하여 리턴한다.
*/
@Cacheable(
cacheNames = [CacheConfig.ONE_MIN_CACHE],
key = "'concert-' + #concertId",
condition = "#token != null && #concertId != null",
sync = true,
)
fun getConcertSchedules(
token: String,
concertId: Long,
Expand Down Expand Up @@ -85,7 +99,6 @@ class ConcertService(
}

private fun validateQueueStatus(token: String) {
val queue = queueManager.findByToken(token)
if (queue.queueStatus != QueueStatus.PROCESSING) throw BusinessException.BadRequest(ErrorCode.Queue.NOT_ALLOWED)
if (queueManager.getQueueStatus(token) != QueueStatus.PROCESSING) throw BusinessException.BadRequest(ErrorCode.Queue.NOT_ALLOWED)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package com.hhplus.concert.business.application.service

import com.hhplus.concert.business.application.dto.PaymentServiceDto
import com.hhplus.concert.business.domain.entity.Reservation
import com.hhplus.concert.business.domain.manager.ConcertManager
import com.hhplus.concert.business.domain.manager.PaymentManager
import com.hhplus.concert.business.domain.manager.QueueManager
import com.hhplus.concert.business.domain.manager.UserManager
import com.hhplus.concert.business.domain.manager.concert.ConcertCacheManager
import com.hhplus.concert.business.domain.manager.concert.ConcertManager
import com.hhplus.concert.business.domain.manager.queue.QueueManager
import com.hhplus.concert.business.domain.manager.reservation.ReservationManager
import com.hhplus.concert.common.error.code.ErrorCode
import com.hhplus.concert.common.error.exception.BusinessException
import com.hhplus.concert.common.type.ConcertStatus
import com.hhplus.concert.common.type.QueueStatus
import com.hhplus.concert.common.type.SeatStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -22,6 +22,7 @@ class PaymentService(
private val paymentManager: PaymentManager,
private val queueManager: QueueManager,
private val concertManager: ConcertManager,
private val concertCacheManager: ConcertCacheManager,
) {
/**
* 결제를 진행한다.
Expand All @@ -39,29 +40,20 @@ class PaymentService(
val user = userManager.findById(userId)
val requestReservations = reservationManager.findAllById(reservationIds)

if (requestReservations.isEmpty()) throw BusinessException.BadRequest(ErrorCode.Payment.NOT_FOUND)
validateReservations(userId, requestReservations)

// 결제 요청을 시도하는 user 와 예악한 목록의 user 가 일치하는지 확인한다.
if (requestReservations.any { it.user.id != userId }) {
throw BusinessException.BadRequest(ErrorCode.Payment.BAD_REQUEST)
}

// 결제를 한다.
// 결제를 하고, 성공하면 결제 내역을 저장한다.
val executedPayments =
paymentManager.execute(
paymentManager.executeAndSaveHistory(
user,
requestReservations,
)

// 결제 내역을 저장한다.
paymentManager.saveHistory(user, executedPayments)

// reservation 상태를 PAYMENT_COMPLETED 로 변경한다.
reservationManager.complete(requestReservations)

// queue 상태를 COMPLETED 로 변경한다.
val queue = queueManager.findByToken(token)
queueManager.updateStatus(queue, QueueStatus.COMPLETED)
// queue 를 완료 시킨다.
queueManager.completeProcessingToken(token)

// 결제 완료 후, 해당 Concert 의 좌석이 모두 매진이라면, Concert 의 상태를 UNAVAILABLE 로 변경한다.
updateConcertStatusToUnavailable(requestReservations)
Expand All @@ -76,6 +68,20 @@ class PaymentService(
}
}

private fun validateReservations(
userId: Long,
reservations: List<Reservation>,
) {
if (reservations.isEmpty()) {
throw BusinessException.BadRequest(ErrorCode.Payment.NOT_FOUND)
}

// 결제 요청을 시도하는 user 와 예악한 목록의 user 가 일치하는지 확인한다.
if (reservations.any { it.user.id != userId }) {
throw BusinessException.BadRequest(ErrorCode.Payment.BAD_REQUEST)
}
}

// 예약정보에 있는 콘서트의 좌석이 모두 UNAVAILABLE 일 경우, 콘서트의 상태를 UNAVAILABLE 으로 변경한다.
private fun updateConcertStatusToUnavailable(reservations: List<Reservation>) {
val concertSchedules = reservations.map { it.seat.concertSchedule }.distinct()
Expand All @@ -85,6 +91,8 @@ class PaymentService(
if (allSeats.all { it.seatStatus == SeatStatus.UNAVAILABLE }) {
val concert = schedule.concert
concertManager.updateStatus(concert, ConcertStatus.UNAVAILABLE)
concertCacheManager.evictConcertCache()
concertCacheManager.evictConcertScheduleCache(concert.id)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,94 +1,64 @@
package com.hhplus.concert.business.application.service

import com.hhplus.concert.business.application.dto.QueueServiceDto
import com.hhplus.concert.business.domain.manager.QueueManager
import com.hhplus.concert.business.domain.manager.UserManager
import com.hhplus.concert.business.domain.manager.queue.QueueManager
import com.hhplus.concert.common.type.QueueStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime

@Service
class QueueService(
private val queueManager: QueueManager,
private val userManager: UserManager,
) {
/**
* userId 전제 검증
* 현재 웨이팅 상태인 queue 가 저장되어있는지 검증
* 있다면 기존 queue 상태를 초기화, 대기열에서 밀려남
* userId 를 통해 user 를 찾아온다.
* waiting 상태로 queue 저장 및 token 발급
*/
@Transactional
fun issueQueueToken(userId: Long): QueueServiceDto.IssuedToken {
val user = userManager.findById(userId)
val queue =
queueManager.findByUserIdAndStatus(
userId = user.id,
queueStatus = QueueStatus.WAITING,
)

// queue 가 기존에 존재한다면, 그 queue 를 취소상태로 변경한다.
if (queue != null) {
queueManager.updateStatus(queue, QueueStatus.CANCELLED)
}

return QueueServiceDto.IssuedToken(
token = queueManager.enqueueAndIssueToken(user),
createdAt = LocalDateTime.now(),
token = queueManager.enqueueAndIssueToken(user.id),
)
}

/**
* token 을 통해 queue 의 정보를 반환한다.
* token 을 통해 queue 의 상태를 조회한다.
* 현재 queue 상태가 waiting 상태라면 현재 대기열이 얼마나 남았는지를 계산하여 반환한다.
* 그 밖의 상태라면, 얼마나 대기를 해야하는지 알 필요가 없으므로 0 을 반환한다.
*/
@Transactional
fun findQueueByToken(token: String): QueueServiceDto.Queue {
val queue = queueManager.findByToken(token)
val status = queueManager.getQueueStatus(token)
val isWaiting = status == QueueStatus.WAITING

val position = if (isWaiting) queueManager.getPositionInWaitingStatus(token) else NO_REMAINING_WAIT
val estimatedWaitTime = if (isWaiting) queueManager.calculateEstimatedWaitSeconds(position) else NO_REMAINING_WAIT

return QueueServiceDto.Queue(
queueId = queue.id,
status = queue.queueStatus,
joinAt = queue.joinedAt,
remainingWaitListCount =
if (queue.queueStatus == QueueStatus.WAITING) {
queueManager.getPositionInWaitingStatus(queue.id)
} else {
NO_REMAINING_WAIT_LIST_COUNT
},
status = status,
remainingWaitListCount = position,
estimatedWaitTime = estimatedWaitTime,
)
}

/**
* 스케쥴러를 통해 queue 의 process 상태인 상태의 queue 개수를 유지시킨다.
* 스케쥴러를 통해 WAITING 상태의 대기열을 PROCESSING 상태로 변경한다.
*/
@Transactional
fun maintainProcessingCount() {
val neededToUpdateCount = ALLOWED_MAX_SIZE - queueManager.countByQueueStatus(QueueStatus.PROCESSING)

if (neededToUpdateCount > 0) {
queueManager.updateStatus(
queueIds = queueManager.getNeededUpdateToProcessingIdsFromWaiting(neededToUpdateCount),
queueStatus = QueueStatus.PROCESSING,
)
}
fun updateToProcessingTokens() {
queueManager.updateToProcessingTokens()
}

/**
* 스케쥴러를 통해 일정 시간이 지났지만 여전히 WAITING 상태인 queue 를 CANCELLED 로 변환시킨다.
* 스케쥴러를 통해 만료 시간이 지났지만 여전히 WAITING 상태인 대기열을 삭제한다.
*/
@Transactional
fun cancelExpiredWaitingQueue() {
queueManager.updateStatus(
queueIds = queueManager.getExpiredWaitingQueueIds(),
queueStatus = QueueStatus.CANCELLED,
)
queueManager.removeExpiredWaitingQueue()
}

companion object {
const val NO_REMAINING_WAIT_LIST_COUNT = 0
const val ALLOWED_MAX_SIZE = 100
const val NO_REMAINING_WAIT = 0L
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.hhplus.concert.business.application.service

import com.hhplus.concert.business.application.dto.ReservationServiceDto
import com.hhplus.concert.business.domain.manager.ConcertManager
import com.hhplus.concert.business.domain.manager.QueueManager
import com.hhplus.concert.business.domain.manager.UserManager
import com.hhplus.concert.business.domain.manager.concert.ConcertManager
import com.hhplus.concert.business.domain.manager.queue.QueueManager
import com.hhplus.concert.business.domain.manager.reservation.ReservationLockManager
import com.hhplus.concert.business.domain.manager.reservation.ReservationManager
import com.hhplus.concert.common.error.code.ErrorCode
Expand Down Expand Up @@ -70,8 +70,7 @@ class ReservationService(
}

private fun validateQueueStatus(token: String) {
val queue = queueManager.findByToken(token)
if (queue.queueStatus != QueueStatus.PROCESSING) throw BusinessException.BadRequest(ErrorCode.Queue.NOT_ALLOWED)
if (queueManager.getQueueStatus(token) != QueueStatus.PROCESSING) throw BusinessException.BadRequest(ErrorCode.Queue.NOT_ALLOWED)
}

private fun validateReservationRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,37 @@ class PaymentManager(
* - payment 를 저장한다.
* - 예상치 못한 예외 발생 시, 결제 실패로 저장한다.
*/
fun execute(
fun executeAndSaveHistory(
user: User,
requestReservations: List<Reservation>,
): List<Payment> =
requestReservations.map { reservation ->
runCatching {
Payment(
user = user,
reservation = reservation,
amount = reservation.seat.seatPrice,
executedAt = LocalDateTime.now(),
paymentStatus = PaymentStatus.COMPLETED,
).let { paymentRepository.save(it) }
}.getOrElse {
Payment(
user = user,
reservation = reservation,
amount = reservation.seat.seatPrice,
executedAt = LocalDateTime.now(),
paymentStatus = PaymentStatus.FAILED,
).let { paymentRepository.save(it) }
): List<Payment> {
val payments =
requestReservations.map { reservation ->
runCatching {
Payment(
user = user,
reservation = reservation,
amount = reservation.seat.seatPrice,
executedAt = LocalDateTime.now(),
paymentStatus = PaymentStatus.COMPLETED,
).let { paymentRepository.save(it) }
}.getOrElse {
Payment(
user = user,
reservation = reservation,
amount = reservation.seat.seatPrice,
executedAt = LocalDateTime.now(),
paymentStatus = PaymentStatus.FAILED,
).let { paymentRepository.save(it) }
}
}
}

fun saveHistory(
saveHistory(user, payments)

return payments
}

private fun saveHistory(
user: User,
payments: List<Payment>,
) {
Expand Down
Loading

0 comments on commit 38964a5

Please sign in to comment.