์„œ๋น„์Šค ํ™•์žฅ์„ ์œ„ํ•œ ํŠธ๋žœ์žญ์…˜ ๋ถ„๋ฆฌ์™€ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์„ค๊ณ„

2024. 11. 15. 02:06ยท๐Ÿ’ป ๊ฐœ๋ฐœ/๐Ÿ€ Spring

 

๋“ค์–ด๊ฐ€๋ฉฐ

๋Œ€๊ทœ๋ชจ ์‹œ์Šคํ…œ์—์„œ ํŠธ๋žœ์žญ์…˜์˜ ๋ฒ”์œ„์™€ ์„œ๋น„์Šค ๊ฐ„์˜ ๊ฒฐํ•ฉ๋„๋Š” ์„ฑ๋Šฅ๊ณผ ํ™•์žฅ์„ฑ์— ํฐ ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค. ํŠนํžˆ, ๊ธฐ์กด ๋กœ์ง์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์œผ๋ฉด์„œ ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฒƒ์€ ์„œ๋น„์Šค์˜ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ด๋Š” ์ค‘์š”ํ•œ ์ž‘์—…์ž…๋‹ˆ๋‹ค. ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ํ˜„์žฌ ์ฝ˜์„œํŠธ ์˜ˆ์•ฝ ์„œ๋น„์Šค์˜ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๋ฅผ ๋ถ„์„ํ•˜๊ณ , ์„œ๋น„์Šค๋“ค์„ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ถ„๋ฆฌํ•˜๋ฉฐ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์„ค๊ณ„๋ฅผ ๋„์ž…ํ•˜๋Š” ๊ณผ์ •์— ๋Œ€ํ•ด ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

 

ํ˜„์žฌ ์ฝ”๋“œ์˜ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ๋ถ„์„

 

ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„

ํ˜„์žฌ ์ฝ”๋“œ์—์„œ @Transactional์ด ์ ์šฉ๋œ ํ•ต์‹ฌ ๋กœ์ง ์ค‘ ์˜ˆ์•ฝ ์ƒ์„ฑ๊ณผ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ๋กœ์ง์— ๋Œ€ํ•ด ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๋ฅผ ๋ถ„์„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋‘ ๋กœ์ง์€ ์•„๋ž˜๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

 

 

์˜ˆ์•ฝ ์ƒ์„ฑ

@Service
class ReservationService(
    private val seatFinder: SeatFinder,
    private val concertManager: ConcertManager,
    private val reservationRepository: ReservationRepository,
) {
    @Transactional
    fun createPendingReservation(
        userId: Long,
        scheduleId: Long,
        seatId: Long,
    ): CreateReservationInfo {
        seatFinder.getAvailableSeat(scheduleId, seatId).reserve()
        occupySeat(scheduleId)
        val reservation = concertManager.createPendingReservation(userId, scheduleId, seatId)
        return reservation.toCreateReservationInfo(success = true)
    }
}

 

๊ฒฐ์ œ ์ฒ˜๋ฆฌ

@Component
class PaymentFacade(
    private val waitingQueueService: WaitingQueueService,
    private val userService: UserService,
    private val reservationService: ReservationService,
    private val paymentService: PaymentService,
    private val concertService: ConcertService,
) {
    @Transactional
    fun processPayment(command: PaymentCommand): PaymentInfo {
        return runCatching {
            val reservationInfo: ReservationInfo = reservationService.confirmReservation(command.reservationId)
            val seatInfo: SeatInfo = concertService.verifyAndGetSeatInfo(reservationInfo.seatId)
            userService.updateUserBalance(command.userId, seatInfo.price, PointTransactionType.USE)
            waitingQueueService.expireToken(command.token)
            paymentService.savePayment(command.userId, command.reservationId, seatInfo.price)
        }.getOrElse { ex ->
            throw CoreException(
                errorType = ErrorType.PAYMENT_FAILED,
                details = {
                    "cause" to ex.message
                },
            )
        }
    }
}

 

์ข€ ๋” ๋ณด๊ธฐ ์‰ฝ๊ฒŒ ์ˆ˜๋„(pseudo) ์ฝ”๋“œ๋กœ ๋‹ค์‹œ ์ž‘์„ฑํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

 

์˜ˆ์•ฝ ์ƒ์„ฑ

// ReservationService.createPendingReservation()
@Transactional
fun ์˜ˆ์•ฝ์ƒ์„ฑ(์‚ฌ์šฉ์žID, ์ผ์ •ID, ์ขŒ์„ID) {
    val ์ขŒ์„ = ์‚ฌ์šฉ๊ฐ€๋Šฅํ•œ์ขŒ์„๊ฐ€์ ธ์˜ค๊ธฐ(์ผ์ •ID, ์ขŒ์„ID)
    ์ขŒ์„.์ขŒ์„์˜ˆ์•ฝ()
    ์ขŒ์„์ ์œ ์—…๋ฐ์ดํŠธ(์ผ์ •ID) // ์žฌ๊ณ  ๊ฐ์†Œ
    val ์˜ˆ์•ฝ = ์˜ˆ์•ฝ์ƒ์„ฑ๋ฐ์ €์žฅ(์‚ฌ์šฉ์žID, ์ผ์ •ID, ์ขŒ์„ID)
    return ์˜ˆ์•ฝ.์˜ˆ์•ฝ์ •๋ณด๋ณ€ํ™˜() // ์˜ˆ์•ฝ ์ •๋ณด ๋ฐ˜ํ™˜
}

 

๊ฒฐ์ œ ์ฒ˜๋ฆฌ

// PaymentFacade.processPayment()
@Transactional
fun ๊ฒฐ์ œ์ฒ˜๋ฆฌ(command): ๊ฒฐ์ œ์ •๋ณด {
    return runCatching {
        val ์˜ˆ์•ฝ์ •๋ณด = ์˜ˆ์•ฝํ™•์ธ๋ฐํ™•์ •์ฒ˜๋ฆฌ(์˜ˆ์•ฝID)
        val ์ขŒ์„์ •๋ณด = ์ขŒ์„์ •๋ณด๊ฒ€์ฆ๋ฐ๊ฐ€์ ธ์˜ค๊ธฐ(์˜ˆ์•ฝ์ •๋ณด.์ขŒ์„ID)
        ์‚ฌ์šฉ์ž์ž”์•ก์ฐจ๊ฐ(์‚ฌ์šฉ์žID, ์ขŒ์„์ •๋ณด.๊ฐ€๊ฒฉ, USE)
        ๋Œ€๊ธฐ์—ดํ† ํฐ๋งŒ๋ฃŒ์ฒ˜๋ฆฌ(ํ† ํฐ)
        ๊ฒฐ์ œ์ •๋ณด์ €์žฅ(์‚ฌ์šฉ์žID, ์˜ˆ์•ฝID, ์ขŒ์„์ •๋ณด.๊ฐ€๊ฒฉ)
    }.getOrElse { ์˜ˆ์™ธ ->
        throw ์ต์…‰์…˜()
    }
}

 

  • ์˜ˆ์•ฝ ์ƒ์„ฑ, ๊ฒฐ์ œ ์ฒ˜๋ฆฌ์˜ ๋ฉ”์„œ๋“œ๋Š” ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์—ฌ ์žˆ์–ด, ๋ชจ๋“  ์ž‘์—…์ด ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
  • ๊ฒฐ์ œ ์ฒ˜๋ฆฌ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต์—์„œ ํŒŒ์‚ฌ๋“œ๋กœ ์ž‘์„ฑ๋˜์–ด ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ ์„œ๋น„์Šค ํ˜ธ์ถœํ•˜๋ฉด์„œ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋†’์€ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ์˜ ๋ฌธ์ œ์ 

1. ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๊ฐ€ ๋„“์Œ

๊ฒฐ์ œ ์ฒ˜๋ฆฌ์™€ ๊ฐ™์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ, ๋ชจ๋“  ์„œ๋น„์Šค ํ˜ธ์ถœ์ด ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์—ฌ ํŠน์ • ์ž‘์—… ์‹คํŒจ ์‹œ ์ „์ฒด ๋กค๋ฐฑ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์˜ˆ์•ฝ ํ™•์ •๊ณผ ์ž”์•ก ์ฐจ๊ฐ์ด ์„ฑ๊ณตํ–ˆ์œผ๋‚˜ ๊ฒฐ์ œ ์ •๋ณด ์ €์žฅ์ด ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ ํ•ต์‹ฌ ๋กœ์ง๊นŒ์ง€ ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋„ฅ์…˜ ์ ์œ  ์‹œ๊ฐ„ ์ฆ๊ฐ€

ํŠธ๋žœ์žญ์…˜์ด ๊ธธ์–ด์ง€๋ฉด DB ์ปค๋„ฅ์…˜์„ ์˜ค๋ž˜ ์ ์œ ํ•˜๊ฒŒ ๋˜์–ด ํ•œ์ •๋œ ์“ฐ๋ ˆ๋“œ ํ’€์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ ๋‹ค๋ฅธ ์ž‘์—…์— ์˜ํ–ฅ์„ ๋ผ์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

3. ๋ฐ๋“œ๋ฝ ๊ฐ€๋Šฅ์„ฑ

ํŠธ๋žœ์žญ์…˜์ด ๊ธธ์–ด์งˆ์ˆ˜๋ก ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜๊ณผ ์ถฉ๋Œํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ง‘๋‹ˆ๋‹ค.

 

4. ํ™•์žฅ์„ฑ ํ•œ๊ณ„

์„œ๋น„์Šค ๋กœ์ง์ด ๋ณต์žกํ•ด์งˆ์ˆ˜๋ก ๋‹จ์ผ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์ˆ˜ํ–‰ํ•˜๋Š” ์ž‘์—…์ด ๋งŽ์•„์ง€๋ฉฐ, ์ด๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ํ™•์žฅํ•  ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

 

 

 

 

 

์„œ๋น„์Šค ๋ถ„๋ฆฌ ๋ฐ ๊ฐœ์„  ๋ฐฉ์•ˆ

 

1. ์„œ๋น„์Šค ๊ธฐ๋Šฅ๋ณ„ ๋ถ„๋ฆฌ

ํ˜„์žฌ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ์ฝ”๋“œ๋Š” ์˜ˆ์•ฝ, ๊ฒฐ์ œ, ์‚ฌ์šฉ์ž ์„œ๋น„์Šค๊ฐ€ ๋‹จ์ผ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์—ฌ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๊ธฐ๋Šฅ๋ณ„ ์„œ๋น„์Šค๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐœ๋ณ„ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๋ฅผ ์ขํž ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์˜ˆ์•ฝ ํ™•์ •, ์ž”์•ก ์ฐจ๊ฐ, ํ† ํฐ ๋งŒ๋ฃŒ, ๊ฒฐ์ œ ์ •๋ณด ์ €์žฅ์€ ๊ฐ๊ฐ์˜ ์„œ๋น„์Šค๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐ๊ธฐ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์„ ๊ฐ€์ง€๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

2. ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ์กฐ์ •

์ค‘์š”ํ•œ ํ•ต์‹ฌ ์ž‘์—…์— ๋Œ€ํ•ด์„œ๋งŒ ํŠธ๋žœ์žญ์…˜์„ ์œ ์ง€ํ•˜๊ณ , ๋ถ€๊ฐ€์ ์ธ ์ž‘์—…์€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๊ฒฐ์ œ ์ฒ˜๋ฆฌ์—์„œ ์˜ˆ์•ฝํ™•์ •, ์ž”์•ก์ฐจ๊ฐ ๊ฐ™์€ ์ฃผ์š” ๋กœ์ง์€ ํŠธ๋žœ์žญ์…˜์„ ์œ ์ง€ํ•˜๊ณ , ํ† ํฐ๋งŒ๋ฃŒ๋‚˜ ๊ฒฐ์ œ์ •๋ณด์ €์žฅ์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์™ธ๋ถ€ API ํ˜ธ์ถœ์ด๋‚˜ ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰์˜ ๊ฒฝ์šฐ์—๋„ ํŠธ๋žœ์žญ์…˜ ์™ธ๋ถ€์—์„œ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์ถ”๊ฐ€ํ•˜์—ฌ ์ฃผ์š” ๋กœ์ง์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

3. ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์„ค๊ณ„ ๋„์ž…

์„œ๋น„์Šค ๊ฐ„ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ธฐ ์œ„ํ•ด ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋„์ž…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์˜ˆ์•ฝ ์ƒ์„ฑ์—์„œ ์˜ˆ์•ฝ์ด ์™„๋ฃŒ๋œ ํ›„ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ  ์™ธ๋ถ€ API๊ฐ€ ์ด๋ฅผ ๊ตฌ๋…ํ•˜๋„๋ก ํ•˜๋ฉด, ํ•ด๋‹น ๋กœ์ง์€ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด ํ•œ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ๋ชจ๋“  ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง€๊ณ , ํ™•์žฅ์„ฑ๊ณผ ์œ ์—ฐ์„ฑ์ด ๋†’์•„์ง‘๋‹ˆ๋‹ค.

 

4. ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ๋ฐ ์‚ฌ๊ฐ€ ํŒจํ„ด ๋„์ž…

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋กœ ํ™•์žฅํ•  ๊ฒฝ์šฐ, ๋‹จ์ผ ํŠธ๋žœ์žญ์…˜์ด ์•„๋‹Œ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜์„ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ์‚ฌ๊ฐ€ ํŒจํ„ด์„ ๋„์ž…ํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์„ ๋‹จ๊ณ„๋ณ„๋กœ ๋‚˜๋ˆ„๊ณ , ๊ฐ ๋‹จ๊ณ„๊ฐ€ ์‹คํŒจํ•  ๊ฒฝ์šฐ ๋ณด์ƒ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์„ค๊ณ„ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

 

 

 

์˜ˆ์•ฝ ์ƒ์„ฑ ๋กœ์ง์— ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ํ•˜๊ธฐ

์œ„์˜ ๊ฐœ์„  ๋ฐฉ์•ˆ์„ ํ† ๋Œ€๋กœ ์˜ˆ์•ฝ ์ƒ์„ฑ ๋กœ์ง์— ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐ ๊ตฌ๋… ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์™ธ๋ถ€ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์š”๊ตฌ์‚ฌํ•ญ์„ ์ ์šฉํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

1. ๊ธฐ์กด ์˜ˆ์•ฝ ์„œ๋น„์Šค์˜ ์˜ˆ์•ฝ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ

์œ„์—์„œ ๋ดค๋˜ ์˜ˆ์•ฝ ์ƒ์„ฑ์˜ ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

@Transactional
fun createPendingReservation(
    userId: Long,
    scheduleId: Long,
    seatId: Long,
): CreateReservationInfo {
    seatFinder.getAvailableSeat(scheduleId, seatId).reserve()
    occupySeat(scheduleId)
    val reservation = concertManager.createPendingReservation(userId, scheduleId, seatId)
    return reservation.toCreateReservationInfo(success = true)
}

 

 

2. ์˜ˆ์•ฝ ์ด๋ฒคํŠธ POJO ํด๋ž˜์Šค ์ž‘์„ฑ

์˜ˆ์•ฝ ์ƒ์„ฑ ํ›„ ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ Event ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ ๋กœ, ApplicationEventPublisher ๋Š” ์Šคํ”„๋ง 4.2๋ถ€ํ„ฐ ApplicationEvent ํด๋ž˜์Šค ์ƒ์† ์—†์ด POJO ๊ฐ์ฒด์˜ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ข€ ๋” ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์•„๋ž˜ ๋ธ”๋กœ๊ทธ ์•„ํ‹ฐํด์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

[Spring] ์Šคํ”„๋ง์—์„œ ์ด๋ฒคํŠธ์˜ ๋ฐœํ–‰๊ณผ ๊ตฌ๋… ๋ฐฉ๋ฒ•๊ณผ ์ฃผ์˜์‚ฌํ•ญ, ์ด๋ฒคํŠธ ์‚ฌ์šฉ์˜ ์žฅ/๋‹จ์ ๊ณผ ์‚ฌ์šฉ ์˜ˆ์‹œ ์ถœ์ฒ˜: [MangKyu's Diary:ํ‹ฐ์Šคํ† ๋ฆฌ]

 

package hhplus.concertreservation.domain.concert.event

data class ReservationCreatedEvent(
    val userId: Long,
    val scheduleId: Long,
    val seatId: Long,
    val reservationId: Long,
) {
    companion object {
        fun from(reservation: Reservation): ReservationCreatedEvent {
            return ReservationCreatedEvent(
                userId = reservation.userId,
                scheduleId = reservation.scheduleId,
                seatId = reservation.seatId,
                reservationId = reservation.id,
            )
        }
    }
}

 

 

3. ์˜ˆ์•ฝ ์™„๋ฃŒ ํ›„ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์ถ”๊ฐ€

์˜ˆ์•ฝ์ด ์™„๋ฃŒ๋˜๋ฉด ์˜ˆ์•ฝ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

@Transactional
fun createPendingReservation(
    userId: Long,
    scheduleId: Long,
    seatId: Long,
): CreateReservationInfo {
    seatFinder.getAvailableSeat(scheduleId, seatId).reserve()
    occupySeat(scheduleId)
    val reservation = concertManager.createPendingReservation(userId, scheduleId, seatId)
    eventPublisher.publish(ReservationCreatedEvent.from(reservation)) // ์ด๋ฒคํŠธ ๋ฐœํ–‰
    return reservation.toCreateReservationInfo(success = true)
}

 

4. ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ํ”Œ๋žซํผ ํ˜ธ์ถœ ํด๋ž˜์Šค ์ž‘์„ฑ

๊ฐ„๋‹จํ•˜๊ฒŒ ์˜ˆ์•ฝ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๊ฒŒ ๋˜๋ฉด ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

package hhplus.concertreservation.infrastructure.api

@Component
class DataPlatformApiClient {
    private val log = LoggerFactory.getLogger(this::class.java)
    fun sendReservation() {
        log.info("์™ธ๋ถ€ API ํ˜ธ์ถœ: ์˜ˆ์•ฝ ์ „์†ก ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ")
    }
}

 

 

5. ์ด๋ฒคํŠธ Publisher์™€ Listener ์ธํ„ฐํŽ˜์ด์Šค ์ž‘์„ฑ

EventPublisher์™€ EventListener์˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฐจ์ฃผ ๊ณผ์ œ์—์„œ Spring์˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ Kafka๋กœ ์ „ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ตฌํ˜„์ฒด๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์ข‹์€ ๊ตฌ์กฐ๋กœ ์„ค๊ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

package hhplus.concertreservation.infrastructure.event

interface EventPublisher<T> {
    fun publish(event: T)
}
package hhplus.concertreservation.infrastructure.event

interface EventHandler<T> {
    fun handle(event: T)
}

 

 

6. ์Šคํ”„๋ง ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„์ฒด ์ž‘์„ฑ

์Šคํ”„๋ง ์ปจํ…์ŠคํŠธ์—์„œ ์ œ๊ณตํ•˜๋Š” ApplicationEventPublisher์™€ @TransactionalEventListener๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„์ฒด๋ฅผ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

 

package hhplus.concertreservation.infrastructure.event.reservation

@Component
class ReservationSpringEventPublisher(
    private val applicationEventPublisher: ApplicationEventPublisher
) : EventPublisher<ReservationCreatedEvent> {

    override fun publish(event: ReservationCreatedEvent) {
        applicationEventPublisher.publishEvent(event)
    }
}

 

package hhplus.concertreservation.infrastructure.event.listener

@Component
class ExternalPlatformEventListener(
    private val dataPlatformApiClient: DataPlatformApiClient
): EventListener<ReservationCreatedEvent> {
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    override fun handle(event: ReservationCreatedEvent) {
        dataPlatformApiClient.sendReservation()
    }
}

 

 

์ฐธ๊ณ ) Listener์— @Async์™€ @Transactional(REQUIRES_NEW), @TransactionalEventListener(AFTER_COMMIT)์„ ์‚ฌ์šฉํ•œ ์ด์œ 

์Šคํ”„๋ง 4.2 ๋ฒ„์ „๋ถ€ํ„ฐ @EventListener ์• ๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์‰ฝ๊ฒŒ ์ด๋ฒคํŠธ ๊ตฌ๋… ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

package hhplus.concertreservation.infrastructure.event.listener

@Component
class ExternalPlatformEventListener(
    private val dataPlatformApiClient: DataPlatformApiClient
): EventListener<ReservationCreatedEvent> {
    @EventListener
    override fun handle(event: ReservationCreatedEvent) {
        dataPlatformApiClient.sendReservation()
    }
}

 

ํ•˜์ง€๋งŒ, ์œ„ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ๋ฉ”์ธ ๋กœ์ง๊ณผ ๋™์ผํ•œ ํŠธ๋žœ์žญ์…˜ ์‚ฌ์šฉ: ๋ฆฌ์Šค๋„ˆ ๋กœ์ง์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋ฉ”์ธ ๋กœ์ง๋„ ๋กค๋ฐฑ๋ฉ๋‹ˆ๋‹ค.
  2. ๋™๊ธฐ ์ฒ˜๋ฆฌ: ๋ฉ”์ธ ๋กœ์ง๊ณผ ๋™๊ธฐ์ ์œผ๋กœ ์ˆ˜ํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฆฌ์Šค๋„ˆ ๋กœ์ง์˜ ์ˆ˜ํ–‰ ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง€๋ฉด ๋ฉ”์ธ ๋กœ์ง์˜ ์‘๋‹ต์ด ์ง€์—ฐ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด @Async์™€ @Transactional(propagation = Propagation.REQUIRES_NEW)๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์™€ ๋…๋ฆฝ์ ์ธ ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

package hhplus.concertreservation.infrastructure.event.listener

@Component
class ExternalPlatformEventListener(
    private val dataPlatformApiClient: DataPlatformApiClient
): EventListener<ReservationCreatedEvent> {
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @EventListener
    override fun handle(event: ReservationCreatedEvent) {
        dataPlatformApiClient.sendReservation()
    }
}

 

์ด๋ ‡๊ฒŒ ๋‘๊ฐœ์˜ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€๋กœ ์„ ์–ธํ•˜๊ฒŒ ๋˜๋ฉด ์œ„์˜ ๋ฌธ์ œ์ ์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ๋‹ค๋ฅธ ๋ฌธ์ œ๊ฐ€ ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ฉ”์ธ ๋กœ์ง์—์„œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ํ›„ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜๋ฉด ๋ฉ”์ธ ๋กœ์ง์€ ์‹คํŒจํ•˜์ง€๋งŒ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋Š” ์„ฑ๊ณตํ•ด๋ฒ„๋ฆฌ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ๊ทธ ์˜ˆ์‹œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

@Transactional
fun createPendingReservation(
    userId: Long,
    scheduleId: Long,
    seatId: Long,
): CreateReservationInfo {
    seatFinder.getAvailableSeat(scheduleId, seatId).reserve()
    occupySeat(scheduleId)
    val reservation = concertManager.createPendingReservation(userId, scheduleId, seatId)
    eventPublisher.publish(ReservationCreatedEvent.from(reservation)) // ์ด๋ฒคํŠธ ๋ฐœํ–‰
    throw CoreException(ErrorType.SYSTEM_FAILURE, "์ด๋ฒคํŠธ ๋ฐœํ–‰ ํ›„ ์˜ˆ์™ธ ๋ฐœ์ƒ") // ์˜ˆ์™ธ ๋ฐœ์ƒ
    return reservation.toCreateReservationInfo(success = true)
}
@Component
class ExternalPlatformEventListener : EventListener<ReservationCreatedEvent> {
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @EventListener
    override fun handle(event: ReservationCreatedEvent) {
        println("์˜ˆ์•ฝ ์™ธ๋ถ€ API ํ˜ธ์ถœ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ค‘: $event")
        Thread.sleep(3_000)
        println("์˜ˆ์•ฝ ์™ธ๋ถ€ API ํ˜ธ์ถœ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ.")
    }
}

 

์˜ˆ์•ฝ ๋ฉ”์ธ ๋กœ์ง์€ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์œผ๋‚˜ ์ด๋ฒคํŠธ๋Š” ์„ฑ๊ณต

 

์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”์ธ ๋กœ์ง์˜ ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ปค๋ฐ‹๋˜์—ˆ์„ ๋•Œ, ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด @EventListener์˜ ํ™•์žฅ๋œ ๊ฐœ๋…์ธ @TransactionalEventListener๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@TransactionalEventListener๋Š” TransactionPhase๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ๊ฐ€ ์‹คํ–‰๋  ์‹œ์ ์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

- AFTER_COMMIT: ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ปค๋ฐ‹๋œ ํ›„ ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋จ.
- BEFORE_COMMIT: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์ง์ „์— ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋จ.
- AFTER_ROLLBACK: ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ํ›„ ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋จ.
- AFTER_COMPLETION: ํŠธ๋žœ์žญ์…˜์ด ์™„๋ฃŒ๋œ ํ›„(์ปค๋ฐ‹๋˜์—ˆ๋Š”์ง€ ๋กค๋ฐฑ๋˜์—ˆ๋Š”์ง€์™€ ์ƒ๊ด€์—†์ด) ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋จ.

 

 

๋”ฐ๋ผ์„œ, ์ตœ์ข…์ ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด 3๊ฐœ์˜ ์• ๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

package hhplus.concertreservation.infrastructure.event.listener

@Component
class ExternalPlatformEventListener(
    private val dataPlatformApiClient: DataPlatformApiClient
): EventListener<ReservationCreatedEvent> {
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    override fun handle(event: ReservationCreatedEvent) {
        dataPlatformApiClient.sendReservation()
    }
}

 

 

 

 

 

 

๋งˆ์น˜๋ฉฐ

์ด๋ฒˆ ๊ธ€์„ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ๋ถ„์„ํ•˜๊ณ , ์„œ๋น„์Šค ํ™•์žฅ์„ ์œ„ํ•ด ์„œ๋น„์Šค ๋ถ„๋ฆฌ์™€ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์„ค๊ณ„๋ฅผ ๋„์ž…ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ์ ์ ˆํžˆ ์กฐ์ •ํ•˜๊ณ  ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ๋„์ž…ํ•จ์œผ๋กœ์จ ์„œ๋น„์Šค์˜ ์„ฑ๋Šฅ๊ณผ ํ™•์žฅ์„ฑ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์ฃผ์—๋Š” Kafka๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜์„ ๋„์ž…ํ•˜์—ฌ ์ค‘์š”ํ•œ ๋กœ์ง์—์„œ์˜ ์˜ค๋ฅ˜์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•๋„ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

 

ํŒจํ‚ค์ง€ hhplus.concertreservation.config import org.springframework.context.annotation.Configuration import org.springframework.scheduling.annotation.EnableAsync @EnableAsync @Configuration ํด๋ž˜์Šค AsyncConfig
 
์ €์ž‘์žํ‘œ์‹œ (์ƒˆ์ฐฝ์—ด๋ฆผ)

'๐Ÿ’ป ๊ฐœ๋ฐœ > ๐Ÿ€ Spring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

Redis ๊ธฐ๋ฐ˜์˜ ์บ์‹ฑ ๋ฐ ๋Œ€๊ธฐ์—ด ๊ด€๋ฆฌ๋ฅผ ํ†ตํ•œ ์ฝ˜์„œํŠธ ์˜ˆ์•ฝ ์„œ๋น„์Šค ์„ฑ๋Šฅ ๊ฐœ์„   (4) 2024.11.07
๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ ์‰ฝ๊ฒŒ ์ดํ•ดํ•˜๊ธฐ (synchronized, reentrantLock)  (0) 2024.09.29
Spring Security ๋‚ด๋ถ€ ํ๋ฆ„ ์ดํ•ดํ•˜๊ธฐ  (0) 2024.02.13
์Šคํ”„๋ง ๋ถ€ํŠธ์— OpenAI Whisper API ์ ์šฉํ•˜๊ธฐ  (1) 2023.08.24
'๐Ÿ’ป ๊ฐœ๋ฐœ/๐Ÿ€ Spring' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • Redis ๊ธฐ๋ฐ˜์˜ ์บ์‹ฑ ๋ฐ ๋Œ€๊ธฐ์—ด ๊ด€๋ฆฌ๋ฅผ ํ†ตํ•œ ์ฝ˜์„œํŠธ ์˜ˆ์•ฝ ์„œ๋น„์Šค ์„ฑ๋Šฅ ๊ฐœ์„ 
  • ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ ์‰ฝ๊ฒŒ ์ดํ•ดํ•˜๊ธฐ (synchronized, reentrantLock)
  • Spring Security ๋‚ด๋ถ€ ํ๋ฆ„ ์ดํ•ดํ•˜๊ธฐ
  • ์Šคํ”„๋ง ๋ถ€ํŠธ์— OpenAI Whisper API ์ ์šฉํ•˜๊ธฐ
EastShine_
EastShine_
๋” ๋‚˜์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๊ธฐ ์œ„ํ•œ ๋‚˜์˜ ๊ธฐ๋ก ๐Ÿ“
  • EastShine_
    ๊ฐœ๋ฐœ.LOG ๐Ÿ’ป
    EastShine_
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
  • 06-26 22:39
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (27)
      • ๐Ÿ’ป ๊ฐœ๋ฐœ (21)
        • ๐Ÿ–ฅ๏ธ ์šด์˜์ฒด์ œ (3)
        • ๐ŸŒ ๋„คํŠธ์›Œํฌ (0)
        • ๐Ÿ’พ Database (3)
        • ๐ŸŽ› Java (0)
        • ๐Ÿ–ฒ Javascript (0)
        • ๐Ÿ€ Spring (5)
        • ๐ŸŽธ ETC (4)
        • ๐Ÿ“ˆ ์•Œ๊ณ ๋ฆฌ์ฆ˜ (3)
        • ๐Ÿ“– TIL (Today I Learned) (3)
      • ๐Ÿ  ์ผ์ƒ (6)
        • ๐Ÿ““ ์ผ์ƒ ์ผ๊ธฐ (6)
  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    6๊ธฐ
    Whisper API
    ๋‚™๊ด€์ ๋ฝ
    redis
    ๋Œ€๊ธฐ์—ด
    ํ”„๋กœ๊ทธ๋ž˜๋จธ์Šค
    ๋น„๊ด€์ ๋ฝ
    Python
    ์ฝ˜์„œํŠธ์˜ˆ์•ฝ์„œ๋น„์Šค
    transactionaleventlistener
    ํŠธ๋žœ์žญ์…˜ ๋ถ„๋ฆฌ
    ๋™์‹œ์„ฑ์ฒ˜๋ฆฌ
    e-book pdf ๋ณ€ํ™˜
    ์•Œ๊ณ ๋ฆฌ์ฆ˜
    e-book pdf ์ถ”์ถœ
    spring
    ๋ฐฑ์—”๋“œ
    ์ฝ”๋”ฉํ…Œ์ŠคํŠธ
    ํ•ญํ•ดํ”Œ๋Ÿฌ์Šค
    ํšŒ๊ณ 
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • ์ตœ๊ทผ ๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.1
EastShine_
์„œ๋น„์Šค ํ™•์žฅ์„ ์œ„ํ•œ ํŠธ๋žœ์žญ์…˜ ๋ถ„๋ฆฌ์™€ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์„ค๊ณ„
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”