refactor: 예약 도메인 로깅 추가

This commit is contained in:
이상진 2025-07-27 23:27:52 +09:00
parent f5e9212c01
commit 57c890cc64
2 changed files with 183 additions and 94 deletions

View File

@ -1,5 +1,6 @@
package roomescape.reservation.business package roomescape.reservation.business
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.jpa.domain.Specification import org.springframework.data.jpa.domain.Specification
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -20,31 +21,37 @@ import roomescape.time.infrastructure.persistence.TimeEntity
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
private val log = KotlinLogging.logger {}
@Service @Service
@Transactional @Transactional
class ReservationService( class ReservationService(
private val reservationRepository: ReservationRepository, private val reservationRepository: ReservationRepository,
private val timeService: TimeService, private val timeService: TimeService,
private val memberService: MemberService, private val memberService: MemberService,
private val themeService: ThemeService, private val themeService: ThemeService,
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findReservations(): ReservationRetrieveListResponse { fun findReservations(): ReservationRetrieveListResponse {
val spec: Specification<ReservationEntity> = ReservationSearchSpecification() val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.confirmed() .confirmed()
.build() .build()
val reservations = findAllReservationByStatus(spec)
log.info { "[ReservationService.findReservations] ${reservations.size} 개의 확정 예약 조회 완료" }
return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) return ReservationRetrieveListResponse(reservations)
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findAllWaiting(): ReservationRetrieveListResponse { fun findAllWaiting(): ReservationRetrieveListResponse {
val spec: Specification<ReservationEntity> = ReservationSearchSpecification() val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.waiting() .waiting()
.build() .build()
val reservations = findAllReservationByStatus(spec)
log.info { "[ReservationService.findAllWaiting] ${reservations.size} 개의 대기 예약 조회 완료" }
return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) return ReservationRetrieveListResponse(reservations)
} }
private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationRetrieveResponse> { private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationRetrieveResponse> {
@ -52,102 +59,127 @@ class ReservationService(
} }
fun deleteReservation(reservationId: Long, memberId: Long) { fun deleteReservation(reservationId: Long, memberId: Long) {
validateIsMemberAdmin(memberId) validateIsMemberAdmin(memberId, "deleteReservation")
log.info { "[ReservationService.deleteReservation] 예약 삭제 시작: reservationId=$reservationId, memberId=$memberId" }
reservationRepository.deleteById(reservationId) reservationRepository.deleteById(reservationId)
log.info { "[ReservationService.deleteReservation] 예약 삭제 완료: reservationId=$reservationId" }
} }
fun createConfirmedReservation( fun createConfirmedReservation(
request: ReservationCreateWithPaymentRequest, request: ReservationCreateWithPaymentRequest,
memberId: Long memberId: Long,
): ReservationEntity { ): ReservationEntity {
val themeId = request.themeId val themeId = request.themeId
val timeId = request.timeId val timeId = request.timeId
val date: LocalDate = request.date val date: LocalDate = request.date
validateIsReservationExist(themeId, timeId, date) validateIsReservationExist(themeId, timeId, date, "createConfirmedReservation")
val reservation: ReservationEntity = createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED) log.debug { "[ReservationService.createConfirmedReservation] 예약 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
val reservation: ReservationEntity =
createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED)
return reservationRepository.save(reservation) return reservationRepository.save(reservation)
.also { log.info { "[ReservationService.createConfirmedReservation] 예약 추가 완료: reservationId=${it.id}, status=${it.reservationStatus}" } }
} }
fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse { fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse {
validateIsReservationExist(request.themeId, request.timeId, request.date) validateIsReservationExist(request.themeId, request.timeId, request.date)
log.debug { "[ReservationService.createReservationByAdmin] 관리자의 예약 추가: memberId=${request.memberId}, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
return addReservationWithoutPayment( return addReservationWithoutPayment(
request.themeId, request.themeId,
request.timeId, request.timeId,
request.date, request.date,
request.memberId, request.memberId,
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
) ).also {
log.info { "[ReservationService.createReservationByAdmin] 관리자 예약 추가 완료: reservationId=${it.id}" }
}
} }
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse { fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse {
validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId) validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId)
log.debug { "[ReservationService.createWaiting] 예약 대기 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
return addReservationWithoutPayment( return addReservationWithoutPayment(
request.themeId, request.themeId,
request.timeId, request.timeId,
request.date, request.date,
memberId, memberId,
ReservationStatus.WAITING ReservationStatus.WAITING
) ).also {
log.info { "[ReservationService.createWaiting] 예약 대기 추가 완료: reservationId=${it.id}, status=${it.status}" }
}
} }
private fun addReservationWithoutPayment( private fun addReservationWithoutPayment(
themeId: Long, themeId: Long,
timeId: Long, timeId: Long,
date: LocalDate, date: LocalDate,
memberId: Long, memberId: Long,
status: ReservationStatus status: ReservationStatus,
): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status) ): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status)
.also { .also {
reservationRepository.save(it) reservationRepository.save(it)
}.toRetrieveResponse() }.toRetrieveResponse()
private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) { private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) {
log.debug {
"[ReservationService.validateMemberAlreadyReserve] 회원의 중복 예약 여부 확인: themeId=$themeId, timeId=$timeId, date=$date, memberId=$memberId"
}
val spec: Specification<ReservationEntity> = ReservationSearchSpecification() val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.sameMemberId(memberId) .sameMemberId(memberId)
.sameThemeId(themeId) .sameThemeId(themeId)
.sameTimeId(timeId) .sameTimeId(timeId)
.sameDate(date) .sameDate(date)
.build() .build()
if (reservationRepository.exists(spec)) { if (reservationRepository.exists(spec)) {
log.warn { "[ReservationService.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
throw ReservationException(ReservationErrorCode.ALREADY_RESERVE) throw ReservationException(ReservationErrorCode.ALREADY_RESERVE)
} }
} }
private fun validateIsReservationExist(themeId: Long, timeId: Long, date: LocalDate) { private fun validateIsReservationExist(
themeId: Long,
timeId: Long,
date: LocalDate,
calledBy: String = "validateIsReservationExist"
) {
log.debug {
"[ReservationService.$calledBy] 예약 존재 여부 확인: themeId=$themeId, timeId=$timeId, date=$date"
}
val spec: Specification<ReservationEntity> = ReservationSearchSpecification() val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.confirmed() .confirmed()
.sameThemeId(themeId) .sameThemeId(themeId)
.sameTimeId(timeId) .sameTimeId(timeId)
.sameDate(date) .sameDate(date)
.build() .build()
if (reservationRepository.exists(spec)) { if (reservationRepository.exists(spec)) {
log.warn { "[ReservationService.$calledBy] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED) throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
} }
} }
private fun validateDateAndTime( private fun validateDateAndTime(
requestDate: LocalDate, requestDate: LocalDate,
requestTime: TimeEntity requestTime: TimeEntity,
) { ) {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val request = LocalDateTime.of(requestDate, requestTime.startAt) val request = LocalDateTime.of(requestDate, requestTime.startAt)
if (request.isBefore(now)) { if (request.isBefore(now)) {
log.info { "[ReservationService.validateDateAndTime] 날짜 범위 오류. request=$request, now=$now" }
throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME) throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
} }
} }
private fun createEntity( private fun createEntity(
timeId: Long, timeId: Long,
themeId: Long, themeId: Long,
date: LocalDate, date: LocalDate,
memberId: Long, memberId: Long,
status: ReservationStatus status: ReservationStatus,
): ReservationEntity { ): ReservationEntity {
val time: TimeEntity = timeService.findById(timeId) val time: TimeEntity = timeService.findById(timeId)
val theme: ThemeEntity = themeService.findById(themeId) val theme: ThemeEntity = themeService.findById(themeId)
@ -156,86 +188,132 @@ class ReservationService(
validateDateAndTime(date, time) validateDateAndTime(date, time)
return ReservationEntity( return ReservationEntity(
date = date, date = date,
time = time, time = time,
theme = theme, theme = theme,
member = member, member = member,
reservationStatus = status reservationStatus = status
) )
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun searchReservations( fun searchReservations(
themeId: Long?, themeId: Long?,
memberId: Long?, memberId: Long?,
dateFrom: LocalDate?, dateFrom: LocalDate?,
dateTo: LocalDate? dateTo: LocalDate?,
): ReservationRetrieveListResponse { ): ReservationRetrieveListResponse {
validateDateForSearch(dateFrom, dateTo) log.debug { "[ReservationService.searchReservations] 예약 검색 시작: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" }
validateSearchDateRange(dateFrom, dateTo)
val spec: Specification<ReservationEntity> = ReservationSearchSpecification() val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.confirmed() .confirmed()
.sameThemeId(themeId) .sameThemeId(themeId)
.sameMemberId(memberId) .sameMemberId(memberId)
.dateStartFrom(dateFrom) .dateStartFrom(dateFrom)
.dateEndAt(dateTo) .dateEndAt(dateTo)
.build() .build()
val reservations = findAllReservationByStatus(spec)
return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) return ReservationRetrieveListResponse(reservations)
.also { log.info { "[ReservationService.searchReservations] 예약 ${reservations.size}개 조회 완료: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" } }
} }
private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) { private fun validateSearchDateRange(startFrom: LocalDate?, endAt: LocalDate?) {
if (startFrom == null || endAt == null) { if (startFrom == null || endAt == null) {
return return
} }
if (startFrom.isAfter(endAt)) { if (startFrom.isAfter(endAt)) {
log.info { "[ReservationService.validateSearchDateRange] 조회 범위 오류: startFrom=$startFrom, endAt=$endAt" }
throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE) throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
} }
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse { fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse {
return MyReservationRetrieveListResponse(reservationRepository.findAllByMemberId(memberId)) val reservations = reservationRepository.findAllByMemberId(memberId)
log.info { "[ReservationService.findReservationsByMemberId] memberId=${memberId}${reservations.size}개의 예약 조회 완료" }
return MyReservationRetrieveListResponse(reservations)
} }
fun confirmWaiting(reservationId: Long, memberId: Long) { fun confirmWaiting(reservationId: Long, memberId: Long) {
validateIsMemberAdmin(memberId) log.debug { "[ReservationService.confirmWaiting] 대기 예약 승인 시작: reservationId=$reservationId (by adminId=$memberId)" }
validateIsMemberAdmin(memberId, "confirmWaiting")
log.debug { "[ReservationService.confirmWaiting] 대기 여부 확인 시작: reservationId=$reservationId" }
if (reservationRepository.isExistConfirmedReservation(reservationId)) { if (reservationRepository.isExistConfirmedReservation(reservationId)) {
log.warn { "[ReservationService.confirmWaiting] 승인 실패(이미 확정된 예약 존재): reservationId=$reservationId" }
throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS) throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
} }
log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 시작: reservationId=$reservationId" }
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" }
log.info { "[ReservationService.confirmWaiting] 대기 예약 승인 완료: reservationId=$reservationId" }
} }
fun deleteWaiting(reservationId: Long, memberId: Long) { fun deleteWaiting(reservationId: Long, memberId: Long) {
val reservation: ReservationEntity = findReservationOrThrow(reservationId) log.debug { "[ReservationService.deleteWaiting] 대기 취소 시작: reservationId=$reservationId, memberId=$memberId" }
val reservation: ReservationEntity = findReservationOrThrow(reservationId, "deleteWaiting")
if (!reservation.isWaiting()) { if (!reservation.isWaiting()) {
log.warn {
"[ReservationService.deleteWaiting] 대기 취소 실패(대기 예약이 아님): reservationId=$reservationId" +
", currentStatus=${reservation.reservationStatus} memberId=$memberId"
}
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
} }
if (!reservation.isReservedBy(memberId)) { if (!reservation.isReservedBy(memberId)) {
log.error {
"[ReservationService.deleteWaiting] 대기 취소 실패(예약자 본인의 취소 요청이 아님): reservationId=$reservationId" +
", memberId=$memberId "
}
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
} }
log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 시작: reservationId=$reservationId" }
reservationRepository.delete(reservation) reservationRepository.delete(reservation)
log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" }
log.info { "[ReservationService.deleteWaiting] 대기 취소 완료: reservationId=$reservationId, memberId=$memberId" }
} }
fun rejectWaiting(reservationId: Long, memberId: Long) { fun rejectWaiting(reservationId: Long, memberId: Long) {
validateIsMemberAdmin(memberId) validateIsMemberAdmin(memberId, "rejectWaiting")
val reservation: ReservationEntity = findReservationOrThrow(reservationId) log.debug { "[ReservationService.rejectWaiting] 대기 예약 삭제 시작: reservationId=$reservationId (by adminId=$memberId)" }
val reservation: ReservationEntity = findReservationOrThrow(reservationId, "rejectWaiting")
if (!reservation.isWaiting()) { if (!reservation.isWaiting()) {
log.warn {
"[ReservationService.rejectWaiting] 대기 예약 삭제 실패(이미 확정 상태): reservationId=$reservationId" +
", status=${reservation.reservationStatus}"
}
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
} }
reservationRepository.delete(reservation) reservationRepository.delete(reservation)
log.info { "[ReservationService.rejectWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" }
} }
private fun validateIsMemberAdmin(memberId: Long) { private fun validateIsMemberAdmin(memberId: Long, calledBy: String = "validateIsMemberAdmin") {
log.debug { "[ReservationService.$calledBy] 관리자 여부 확인: memberId=$memberId" }
val member: MemberEntity = memberService.findById(memberId) val member: MemberEntity = memberService.findById(memberId)
if (member.isAdmin()) { if (member.isAdmin()) {
return return
} }
log.warn { "[ReservationService.$calledBy] 관리자가 아님: memberId=$memberId, role=${member.role}" }
throw ReservationException(ReservationErrorCode.NO_PERMISSION) throw ReservationException(ReservationErrorCode.NO_PERMISSION)
} }
private fun findReservationOrThrow(reservationId: Long): ReservationEntity { private fun findReservationOrThrow(
reservationId: Long,
calledBy: String = "findReservationOrThrow"
): ReservationEntity {
log.debug { "[ReservationService.$calledBy] 예약 조회: reservationId=$reservationId" }
return reservationRepository.findByIdOrNull(reservationId) return reservationRepository.findByIdOrNull(reservationId)
?: throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) ?.also { log.info { "[ReservationService.$calledBy] 예약 조회 완료: reservationId=$reservationId" } }
?: run {
log.warn { "[ReservationService.$calledBy] 예약 조회 실패: reservationId=$reservationId" }
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
}
} }
} }

View File

@ -1,5 +1,6 @@
package roomescape.reservation.business package roomescape.reservation.business
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.payment.business.PaymentService import roomescape.payment.business.PaymentService
@ -11,48 +12,58 @@ import roomescape.reservation.web.ReservationCreateWithPaymentRequest
import roomescape.reservation.web.ReservationRetrieveResponse import roomescape.reservation.web.ReservationRetrieveResponse
import java.time.OffsetDateTime import java.time.OffsetDateTime
private val log = KotlinLogging.logger {}
@Service @Service
@Transactional @Transactional
class ReservationWithPaymentService( class ReservationWithPaymentService(
private val reservationService: ReservationService, private val reservationService: ReservationService,
private val paymentService: PaymentService private val paymentService: PaymentService,
) { ) {
fun createReservationAndPayment( fun createReservationAndPayment(
request: ReservationCreateWithPaymentRequest, request: ReservationCreateWithPaymentRequest,
paymentInfo: PaymentApproveResponse, paymentInfo: PaymentApproveResponse,
memberId: Long memberId: Long,
): ReservationRetrieveResponse { ): ReservationRetrieveResponse {
log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 시작: memberId=$memberId, paymentInfo=$paymentInfo" }
val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId) val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId)
return paymentService.createPayment(paymentInfo, reservation) return paymentService.createPayment(paymentInfo, reservation)
.reservation .also { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 완료: reservationId=${reservation.id}, paymentId=${it.id}" } }
.reservation
} }
fun createCanceledPayment( fun createCanceledPayment(
cancelInfo: PaymentCancelResponse, cancelInfo: PaymentCancelResponse,
approvedAt: OffsetDateTime, approvedAt: OffsetDateTime,
paymentKey: String paymentKey: String,
) { ) {
paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey) paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey)
} }
fun deleteReservationAndPayment( fun deleteReservationAndPayment(
reservationId: Long, reservationId: Long,
memberId: Long memberId: Long,
): PaymentCancelRequest { ): PaymentCancelRequest {
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 시작: reservationId=$reservationId, memberId=$memberId" }
val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId) val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
reservationService.deleteReservation(reservationId, memberId)
reservationService.deleteReservation(reservationId, memberId)
return paymentCancelRequest return paymentCancelRequest
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun isNotPaidReservation(reservationId: Long): Boolean = !paymentService.isReservationPaid(reservationId) fun isNotPaidReservation(reservationId: Long): Boolean {
log.info { "[ReservationWithPaymentService.isNotPaidReservation] 예약 결제 여부 확인: reservationId=$reservationId" }
return !paymentService.isReservationPaid(reservationId)
.also { log.info { "[ReservationWithPaymentService.isNotPaidReservation] 결제 여부 확인 완료: reservationId=$reservationId, 결제 여부=${!it}" } }
}
fun updateCanceledTime( fun updateCanceledTime(
paymentKey: String, paymentKey: String,
canceledAt: OffsetDateTime canceledAt: OffsetDateTime,
) { ) {
log.info { "[ReservationWithPaymentService.updateCanceledTime] 취소 시간 업데이트: paymentKey=$paymentKey" }
paymentService.updateCanceledTime(paymentKey, canceledAt) paymentService.updateCanceledTime(paymentKey, canceledAt)
} }
} }