๋ค์ด๊ฐ๋ฉฐ
์ฝ์ํธ ์์ฝ ์๋น์ค
์ ๊ฐ์ ์ ํ๋ฆฌ์ผ์ด์
์์๋ ๋์์ฑ ์ฒ๋ฆฌ๋ฅผ ๊ณ ๋ คํด์ผ ํฉ๋๋ค. ์ข์ ์๋งค ์คํ ์ ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ์์ฝ์ ์๋ํ๋ฉด, ์์์ด ์์ ํ๊ฒ ๊ด๋ฆฌ๋์ง ์์ ๊ฒฝ์ฐ ์์ฝ์ด ์ด๊ณผ๋๊ฑฐ๋ ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ด ๊นจ์ง ์ํ์ด ์์ต๋๋ค. ์ด๋ฒ ์๊ฐ์ ์ฝ๋ ์ ์ฉ ๋ฐ ํ
์คํธ๋ฅผ ํตํด ๊ฐ ๋์์ฑ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ฅ๋จ์ ์ ๊ฒํ ํ๊ณ , ์ต์ข
์ ์ผ๋ก ๋น์ฆ๋์ค ๋ก์ง์ ์ ํฉํ ๋์์ฑ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ ์ ํด๋ณด๊ฒ ์ต๋๋ค.
๋์์ฑ ์ด์๊ฐ ๋ฐ์ํ ์ ์๋ ๋น์ฆ๋์ค ๋ก์ง
์ด ์๋น์ค์์ ๋์์ฑ ์ด์๊ฐ ๋ฐ์ํ ์ ์๋ ์ฃผ์ ๋ก์ง์ ์๋์ ๊ฐ์ต๋๋ค.
- ์ข์ ์์ฝ
- ๋ฝ์ ๊ฑธ์ด์ผ ํ๋ ์์
seat
ํ ์ด๋ธ์status
concert_schedule
ํ ์ด๋ธ์available_seats
- ๋ฝ์ ๊ฑธ์ด์ผ ํ๋ ์์
- ํฌ์ธํธ ์ถฉ์ ๋ฐ ๊ฒฐ์
- ๋ฝ์ ๊ฑธ์ด์ผ ํ๋ ์์
user
ํ ์ด๋ธ์balance
reservation
ํ ์ด๋ธ์status
- ๋ฝ์ ๊ฑธ์ด์ผ ํ๋ ์์
๋์์ฑ ์ ์ด ๋ฐฉ์
1. Synchronized
์ค๋ช
synchronized
๋ Java/Kotlin์์ ํน์ ๋ฉ์๋๋ ๋ธ๋ก์ ๋ํด ๋จ์ผ ์ค๋ ๋๋ง ์ ๊ทผํ ์ ์๋๋ก ๋ณด์ฅํ๋ ํค์๋์
๋๋ค. ์ฃผ๋ก ๋ฉํฐ์ค๋ ๋ ํ๊ฒฝ์์ ๊ณต์ ์์์ ๋ณดํธํ๊ธฐ ์ํด ์ฌ์ฉ๋ฉ๋๋ค.
์์ ์ฝ๋
@Component
class ConcertFacade(
/* ... */
) {
@Synchronized // ๋ฉ์๋์ synchronized ์ ์ธ
fun createReservation(command: ReservationCommand): CreateReservationInfo {
waitingQueueService.verifyMatchingScheduleId(command.token, command.scheduleId)
userService.checkUserExists(command.userId)
concertService.checkScheduleAvailability(command.scheduleId)
return reservationService.createPendingReservation(command.userId, command.scheduleId, command.seatId)
}
}
์ฅ๋จ์
- ์ฅ์ : ๊ตฌํ์ด ๋งค์ฐ ๊ฐ๋จํ๊ณ ์ฌ์ฉํ๊ธฐ ์ฝ์ต๋๋ค. JVM ๋ ๋ฒจ์์ ๋์ํ๋ฏ๋ก ์ธ๋ถ ์์คํ ์ด ํ์ ์์ต๋๋ค.
- ๋จ์ : ๋ฉํฐ ์ธ์คํด์ค ํ๊ฒฝ์์๋ ์ธ์คํด์ค ๊ฐ์ ๋๊ธฐํ๊ฐ ๋ณด์ฅ๋์ง ์์ ๋ถ์ฐ ํ๊ฒฝ์์๋ ์ ํฉํ์ง ์์ต๋๋ค.
์ ์ฉํ์ง ์์ ์ด์
๋ฉํฐ ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํ๋ ์์คํ ์ด๋ฏ๋ก, ์ธ์คํด์ค ๊ฐ์ ๊ณต์ ์์์ ์์ ํ๊ฒ ๋ณดํธํ์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ ํด๋น ๋ฐฉ๋ฒ์ ์ ์ฉํ์ง ์์์ต๋๋ค.
2. DB ๋ฝ - ๋๊ด์ ๋ฝ (Optimistic Lock)
์ค๋ช
๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์ถฉ๋์ ํ์งํ๋ ๋ฐฉ์์ผ๋ก, ์ผ๋ฐ์ ์ผ๋ก '๋ฒ์ ๋ฒํธ'๋ฅผ ์ฌ์ฉํ์ฌ ์ถฉ๋ ์ฌ๋ถ๋ฅผ ํ๋จํฉ๋๋ค. @Version
์ด๋
ธํ
์ด์
์ ํตํด ์ํฐํฐ์ ๋ฒ์ ์ ๊ด๋ฆฌํ๋ฉฐ, ์
๋ฐ์ดํธ ์ ์ถฉ๋์ ๊ฐ์งํ ์ ์์ต๋๋ค. ๋น์ฆ๋์ค ๊ด์ ์์๋ ์คํจํด๋ ๋๋ ๋ก์ง์ด๋ผ ํ๋จ๋ ๋ ์ฌ์ฉํฉ๋๋ค.
์์ ์ฝ๋
@Entity
class Seat(
val scheduleId: Long,
seatNumber: Int,
price: BigDecimal,
status: SeatStatus,
@Version
val version: Long = 0L, // ๋ฒ์ ๋ฒํธ ๊ด๋ฆฌ
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
) : BaseEntity() { ... }
@Component
class ConcertFacade(
/* ... */
) {
fun createReservation(command: ReservationCommand): CreateReservationInfo {
waitingQueueService.verifyMatchingScheduleId(command.token, command.scheduleId)
userService.checkUserExists(command.userId)
concertService.checkScheduleAvailability(command.scheduleId)
// retry ๋ก์ง์ ๋ฃ๋๋ค.
while (/*...*/) {
try {
return reservationService.createPendingReservation(command.userId, command.scheduleId, command.seatId)
} catch (ex: ObjectOptimisticLockingFailureException) {
} catch (ex: Exception) {
}
}
}
}
์ฅ๋จ์
- ์ฅ์ : ๋ฝ์ ๊ฑธ์ง ์๊ธฐ ๋๋ฌธ์ ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์ฑ๋ฅ์ด ๋ฐ์ด๋ฉ๋๋ค.
- ๋จ์ : ์ถฉ๋์ด ๋น๋ฒํ ๊ฒฝ์ฐ ๋ฐ๋ณต์ ์ธ ์ฌ์๋๊ฐ ๋ฐ์ํ์ฌ ์ฑ๋ฅ์ด ์ ํ๋ ์ ์์ต๋๋ค.
๊ธฐ์กด ์ฝ๋์ ์ ์ฉ๋ ๋ถ๋ถ
user
ํ
์ด๋ธ์ balance
ํ๋์ ๋๊ด์ ๋ฝ์ ์ ์ฉํ์ฌ ํฌ์ธํธ ์ถฉ์ ๋ฐ ๊ฒฐ์ ์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๊ณ ์์ต๋๋ค.
3. DB ๋ฝ - ๋น๊ด์ ๋ฝ (Pessimistic Lock)
์ค๋ช
๋ฐ์ดํฐ๋ฅผ ์กฐํํ ๋ ๋ฝ์ ๊ฑธ์ด ๋ค๋ฅธ ํธ๋์ญ์
์ด ํด๋น ๋ฐ์ดํฐ๋ฅผ ์์ ํ์ง ๋ชปํ๋๋ก ํ๋ ๋ฐฉ์์
๋๋ค. SELECT ... FOR UPDATE
๊ตฌ๋ฌธ์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ์ ๋ฝ์ ์ค์ ํ๋ฉฐ, ์ฃผ๋ก ์์์ ๋ํด ์ถฉ๋์ด ๋น๋ฒํ๊ฒ ๋ฐ์ํ๊ฑฐ๋, ๋น์ฆ๋์ค ๊ด์ ์์ ๋ฐ๋์ ์ฑ๊ณตํด์ผ ํ๋ ๋ก์ง์ด๋ผ ํ๋จ๋ ๋ ์ฌ์ฉํฉ๋๋ค.
์์ ์ฝ๋
interface SeatJpaRepository : JpaRepository<Seat, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE) // ๋น๊ด์ ๋ฝ
@Query("select s from Seat s where s.id = :id")
fun findByIdOrNullWithLock(id: Long): Seat?
}
@Service
class ReservationService(
/* ... */
) {
@Transactional
fun createPendingReservation(
userId: Long,
scheduleId: Long,
seatId: Long,
): CreateReservationInfo {
seatFinder.getAvailableSeatWithLock(scheduleId, seatId).reserve() // ๋น๊ด์ ๋ฝ ์ ์ฉ
occupySeat(scheduleId)
val reservation = concertManager.createPendingReservation(userId, scheduleId, seatId)
return reservation.toCreateReservationInfo(success = true)
}
}
์ฅ๋จ์
- ์ฅ์ : ๋ฐ์ดํฐ ์ถฉ๋์ด ๋ง์ ๋ ์์ ํ๊ฒ ์ผ๊ด์ฑ์ ์ ์งํ ์ ์์ต๋๋ค.
- ๋จ์ : ๋ฐ๋๋ฝ ๋ฐ์ ๊ฐ๋ฅ์ฑ์ด ์์ผ๋ฉฐ, ๋ฝ์ ๊ฑธ๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ์ด ์ ํ๋ ์ ์์ต๋๋ค.
๊ธฐ์กด ์ฝ๋์ ์ ์ฉ๋ ๋ถ๋ถ
seat
ํ
์ด๋ธ์ status
์ reservation
ํ
์ด๋ธ์ status
ํ๋์ ๋น๊ด์ ๋ฝ์ ์ ์ฉํ์ฌ ์ข์ ์์ฝ๊ณผ ๊ฒฐ์ ์ ๋ค๋ฅธ ํธ๋์ญ์
์ด ์ ๊ทผํ์ง ๋ชปํ๋๋ก ํด๋์์ต๋๋ค.
4. ๋ถ์ฐ ๋ฝ (Distributed Lock)
์ค๋ช
์ฌ๋ฌ ์ธ์คํด์ค์์ ์์์ ๋์์ ์ ๊ทผํ์ง ๋ชปํ๋๋ก ํ์ฌ ๋ถ์ฐ ํ๊ฒฝ์์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค. Redis ๊ฐ์ ๋ฐ์ดํฐ ์ ์ฅ์๋ฅผ ํ์ฉํ์ฌ ๋ฝ์ ์ค์ ํฉ๋๋ค.
์์
@Component
class ConcertFacade(
private val redissonClient: RedissonClient,
/* ... */
) {
fun createReservation(command: ReservationCommand): CreateReservationInfo {
waitingQueueService.verifyMatchingScheduleId(command.token, command.scheduleId)
userService.checkUserExists(command.userId)
concertService.checkScheduleAvailability(command.scheduleId)
// redisson ๋ถ์ฐ ๋ฝ ์ ์ฉ
val lock = redissonClient.getLock("seat:${command.seatId}") // seatId๋ฅผ key๋ก ์ ์ฉ
val isLocked = lock.tryLock(3, 10, TimeUnit.SECONDS)
if (!isLocked) {
throw CoreException(ErrorType.LOCK_ACQUISITION_FAILED)
}
try {
// ํธ๋์ญ์
return reservationService.createPendingReservation(command.userId, command.scheduleId, command.seatId)
} finally {
if (isLocked) {
lock.unlock()
}
}
}
}
์ฅ๋จ์
- ์ฅ์ : ๋ถ์ฐ ํ๊ฒฝ์์๋ ์ผ๊ด์ฑ์ ์ ์งํ ์ ์์ผ๋ฉฐ, ์ฆ๊ฐ์ ์ธ ๋ฐ์์ด ๊ฐ๋ฅํฉ๋๋ค.
- ๋จ์ : Redis์ ๊ฐ์ ์ธ๋ถ ์์คํ ์ ์์กดํ๋ฏ๋ก ์ค์ ๋ฐ ๊ด๋ฆฌ๊ฐ ๋ค์ ๋ณต์กํ๋ฉฐ ๋คํธ์ํฌ ์ง์ฐ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
5. Kafka ์ด๋ฒคํธ ์ฒ๋ฆฌ
์ค๋ช
๋ฐ์ดํฐ ๋ณ๊ฒฝ ์์ ์ ์ด๋ฒคํธ๋ก ๋ง๋ค์ด ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ๋๋ค. Kafka๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ๋ฅผ ์ ๋ฌํ๊ณ , ์ด๋ฅผ ์๋นํ์ฌ ๋์์ฑ์ ์ ์ดํฉ๋๋ค.
์ฅ๋จ์
- ์ฅ์ : ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ฏ๋ก ํ์ฅ์ฑ์ด ๋ฐ์ด๋๋ฉฐ, ๋๋์ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
- ๋จ์ : ์ค์๊ฐ ๋ฐ์์ด ์ด๋ ค์ฐ๋ฉฐ, ์์ฝ ์ํ์ ์ผ๊ด์ฑ์ ์ ์งํ๋ ๋ฐ ์ด๋ ค์์ด ์์ ์ ์์ต๋๋ค.
์ ์ฉํ์ง ์์ ์ด์
์์ ๋จ์ ์ฒ๋ผ ์ฝ์ํธ ์์ฝ์ ๋น์ฆ๋์ค ๋ก์ง์ ๊ด์ ์์๋ ์ฆ๊ฐ์ ์ธ ๋ฐ์์ด ํ์ํ๊ธฐ ๋๋ฌธ์ ์ ์ฉํ์ง ์์์ต๋๋ค. ๋ค๋ง ์ถํ์ concert_schedule
ํ
์ด๋ธ์ available_seats
ํ๋์ ๋ํด์๋ ์ฌ์ฉ์์ ์กฐํ์ฉ ๋ฐ์ดํฐ ์ฑ๊ฒฉ์ ๊ฐ๊น์ฐ๋ฏ๋ก Eventual Consistency
๋ฅผ ์ ์ฉํ์ฌ ๋ฝ์ ์ํ ์ฒ๋ฆฌ ๋ถํ๋ฅผ ์ค์ผ ๊ณํ์
๋๋ค.
๋์์ฑ ์ ์ด ๋ฐฉ์์ ๋ฐ๋ฅธ ์ฑ๋ฅ๊ณผ ์ฅ๋จ์ ์ ๋ค์ ํ๋ก ์ ๋ฆฌํด๋ณด์์ต๋๋ค.
๋์์ฑ์ ์ด ๋ฐฉ์ | ๊ตฌํ ๋ณต์ก๋ | ์ฑ๋ฅ | ํจ์จ์ฑ | ๋ถ์ฐ ํ๊ฒฝ ์ง์ | ์ ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ |
Synchronized | ๋ฎ์ | ์ค๊ฐ | ๋ฎ์ | ๋ถ๊ฐ๋ฅ | ์ ํฉํ์ง ์์ |
DB ๋ฝ (๋๊ด์ ๋ฝ) | ์ค๊ฐ | ๋์ | ์ค๊ฐ | ๊ฐ๋ฅ | ๊ฐ๋ฅ |
DB ๋ฝ (๋น๊ด์ ๋ฝ) | ์ค๊ฐ | ์ค๊ฐ | ๋์ | ๊ฐ๋ฅ | ๊ฐ๋ฅ |
๋ถ์ฐ ๋ฝ (Redisson) | ์ค๊ฐ | ์ค๊ฐ | ๋์ | ๊ฐ๋ฅ | ๊ฐ๋ฅ |
Kafka ์ด๋ฒคํธ ์ฒ๋ฆฌ | ๋์ | ์ค๊ฐ~๋์ | ์ค๊ฐ~๋์ | ๊ฐ๋ฅ | ์ต์ข ์ผ๊ด์ฑ์ ์ ํฉ |
๋์์ฑ ์ ์ด ๋ฐฉ์์ ์ฑ๋ฅ ํ ์คํธ ๋ฐ ๊ฒฐ๊ณผ
์ ๋์์ฑ ์ ์ด ๋ฐฉ์ ์ค DB ๋ฝ(๋๊ด์ ๋ฝ, ๋น๊ด์ ๋ฝ)๊ณผ ๋ถ์ฐ ๋ฝ์ ๋ํด์ ์ฑ๋ฅ ํ ์คํธ๋ฅผ ์๋ํด๋ณด์์ต๋๋ค.
๋ ์ข ๋ฅ์ ํ ์คํธ๋ฅผ ์ค๋นํ๋๋ฐ, ์ฒซ ๋ฒ์งธ๋ ์คํ๋ง ๋ถํธ JUnit5 ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ ๋์์ฑ ํตํฉ ํ ์คํธ๋ก, ์ฌ๋ฌ ์ ์ ๊ฐ ๋์์ ์ข์์ ์์ฝํ๋ ์๋๋ฆฌ์ค์์ ์๋ต ์๊ฐ์ ์ธก์ ํ์ต๋๋ค. ๋ ๋ฒ์งธ๋ Grafana k6๋ฅผ ์ฌ์ฉํ ๋์ ๋ถํ ํ ์คํธ๋ก, ํด๋น ๊ฒฐ๊ณผ๋ฅผ InfluxDB์ ๋ฃ๊ณ , Grafana๋ก ์๊ฐํํ์ต๋๋ค.
ํ ์คํธ๋ ๋์ผํ ๋น์ฆ๋์ค ๋ก์ง(์ข์ ์์ฝ)์์์ ๋์์ฑ ์ ์ด ๋ฐฉ์๋ง ๋ฐ๊ฟ ์งํํ์ต๋๋ค.
๊ณตํต ํ ์คํธ ํ๊ฒฝ
- Spring Boot๋ IntelliJ๋ฅผ ํตํด ๊ธฐ๋
- H2 Database, Redis๋ ๋คํธ์ํฌ๋ฅผ ์ฌ์ฉํ๋๋ก ๋ ๋ฆฝ๋ Docker ์ปจํ ์ด๋๋ก ์คํ
1. JUnit5 ํตํฉ ํ ์คํธ
- ์ ์ 1,000 or 5,000๋ช ์ด ๋์์ ํ ์ข์์ ์์ฝํ๋ ์๋๋ฆฌ์ค
- ์ค๋ ๋ํ ํฌ๊ธฐ๋ 100์ผ๋ก ์ค์
- ๊ฐ ๋์์ฑ ์ ์ด ๋ฐฉ์์ ์๋ต ์๋๋ฅผ ๋น๊ตํ๊ณ , ๋๊ด์ ๋ฝ์ ๊ฒฝ์ฐ ์ฌ์๋ ํ์์ ๋ฐ๋ฅธ ์ฑ๋ฅ ์ฐจ์ด๋ฅผ ์ธก์
ํ ์คํธ ์ฝ๋
@Test
fun `should reserve seat for only one user when multiple requests are made concurrently`() {
// Given
val seatId = 1L
val scheduleId = 1L
val userIds = (1L..1000L).toList()
/* ... */
// When
val tasks =
userIds.map { userId ->
Callable {
startLatch.await()
val startTime = System.nanoTime()
try {
concertFacade.createReservation(
ReservationCommand(
userId = userId,
scheduleId = scheduleId,
seatId = seatId,
token = "123e4567-e89b-12d3-a456-426614174000",
),
)
successCount.incrementAndGet()
} catch (e: CoreException) {
failureCount.incrementAndGet()
} catch (e: ObjectOptimisticLockingFailureException) {
failureCount.incrementAndGet()
} catch (e: Exception) {
failureCount.incrementAndGet()
} finally {
val endTime = System.nanoTime()
responseTimes.add(endTime - startTime)
endLatch.countDown()
}
}
}
tasks.forEach { executor.submit(it) }
startLatch.countDown()
endLatch.await()
executor.shutdown()
// Then
assertEquals(1, successCount.get())
assertEquals(userIds.size - 1, failureCount.get())
// ์๋ต ์๊ฐ
val responseTimeList = responseTimes.toList()
val fastestResponse = responseTimeList.minOrNull()?.let { it / 1_000_000 } ?: 0
val slowestResponse = responseTimeList.maxOrNull()?.let { it / 1_000_000 } ?: 0
val averageResponse = if (responseTimeList.isNotEmpty()) responseTimeList.average() / 1_000_000 else 0.0
println("์ต๋จ ์๋ต ์๊ฐ: ${fastestResponse}ms")
println("์ต์ฅ ์๋ต ์๊ฐ: ${slowestResponse}ms")
println("ํ๊ท ์๋ต ์๊ฐ: ${averageResponse}ms")
}
ํ ์คํธ ๊ฒฐ๊ณผ
1. ๋๊ด์ ๋ฝ (Optimistic Lock)
์ ์ ์ | ์ฌ์๋ ํ์ | ์ต๋จ ์๋ต ์๊ฐ | ์ต์ฅ ์๋ต ์๊ฐ | ํ๊ท ์๋ต ์๊ฐ |
1,000๋ช | ์์ | 32ms | 1766ms | 425.774687ms |
1,000๋ช | 1ํ | 35ms | 1541ms | 473.508084ms |
1,000๋ช | 2ํ | 49ms | 1468ms | 441.298920ms |
5,000๋ช | ์์ | 22ms | 1859ms | 358.117976ms |
5,000๋ช | 1ํ | 26ms | 1102ms | 320.788765ms |
5,000๋ช | 2ํ | 20ms | 1486ms | 350.476428ms |
2. ๋น๊ด์ ๋ฝ (Pessimistic Lock)
์ ์ ์ | ์ต๋จ ์๋ต ์๊ฐ | ์ต์ฅ ์๋ต ์๊ฐ | ํ๊ท ์๋ต ์๊ฐ |
1,000๋ช | 66ms | 1511ms | 511.275845ms |
5,000๋ช | 43ms | 2359ms | 643.524741ms |
3. ๋ถ์ฐ ๋ฝ (Distributed Lock with Redisson)
์ ์ ์ | ์ต๋จ ์๋ต ์๊ฐ | ์ต์ฅ ์๋ต ์๊ฐ | ํ๊ท ์๋ต ์๊ฐ |
1,000๋ช | 22ms | 3768ms | 1141.718405ms |
5,000๋ช | 17ms | 4044ms | 854.586101ms |
ํ ์คํธ ๊ฒฐ๊ณผ ๋ถ์
- ๋๊ด์ ๋ฝ๊ณผ ๋น๊ด์ ๋ฝ ๋น๊ต
- ์ ์ 1,000๋ช ๊ธฐ์ค์์๋ ๋๊ด์ ๋ฝ๊ณผ ๋น๊ด์ ๋ฝ์ ์ฑ๋ฅ ์ฐจ์ด๊ฐ ํฌ์ง ์์์ต๋๋ค.
- ์ ์ ์๋ฅผ 5,000๋ช ์ผ๋ก ๋๋ฆฌ๊ณ ๋๋ ๋น๊ด์ ๋ฝ์ ์ต์ฅ ์๋ต ์๊ฐ์ด ํฌ๊ฒ ๋์ด๋ฌ์ต๋๋ค.
- ๋น๊ด์ ๋ฝ์ด ์์์ ๋ํด ๋๊ธฐ ์ํ์ ์ค๋ ๋๋ฅผ ์ค๋ ์ก์๋๋ ๊ฒ์ด ์์ธ์ผ๋ก ๋ณด์ ๋๋ค.
- ๋ถ์ฐ ๋ฝ์ ์ต์ฅ ์๋ต ์๊ฐ ์ฆ๊ฐ
- ๋ถ์ฐ ๋ฝ์ ๊ฒฝ์ฐ, Redisson์ ์ฌ์ฉํด ๋์์ฑ์ ์ ์ดํ์ต๋๋ค. ํ์ง๋ง ๋ถ์ฐ ๋ฝ์ ๋คํธ์ํฌ ์ง์ฐ ๋ฐ Redis์ ๋ฝ ํ๋/ํด์ ์๊ฐ๊น์ง ํฌํจ๋๋ฏ๋ก ๋ฝ์ ์ ์ ํ์ง ๋ชปํ ์ค๋ ๋์ ๋๊ธฐ ์๊ฐ์ด ๊ธธ์ด์ง๋ ๊ฒฝํฅ์ด ๋ํ๋ฌ์ต๋๋ค.
2. K6 ๋์ ๋ถํ ํ ์คํธ
ํ ์คํธ ์๋๋ฆฌ์ค
- ์ฌ์ฉ์ 100๋ช ์ด ๊ฐ์ 10๋ฒ์ฉ ๋ฐ๋ณตํ์ฌ ์ข์ ์์ฝ์ ์๋
- ์๋๋ฆฌ์ค์ ์ต๋ ์คํ ์๊ฐ์ 30์ด๋ก ์ค์
- 1,000๊ฐ์ ์์ฒญ ์ค 1๊ฐ๋ง ์ฑ๊ณตํจ์ ์ฒดํฌ
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
const BASE_URL = 'http://192.168.0.57:8080/api/concerts/reservations';
const QUEUE_TOKEN = '25f8bba1-262f-476e-8fd8-a15e0ef6b3f6';
// userId ์์ฑ (1๋ถํฐ 100๊น์ง์ ID ๋ฐฐ์ด)
const userIds = new SharedArray('userIds', function () {
return Array.from({ length: 100 }, (_, i) => i + 1);
});
let userIndex = 0;
export const options = {
scenarios: {
reservation_scenario: {
executor: "per-vu-iterations",
vus: 100,
iterations: 10,
maxDuration: "30s",
},
},
};
export default function () {
const userId = userIds[userIndex % userIds.length];
userIndex++;
const payload = JSON.stringify({
userId: userId,
concertId: 1,
scheduleId: 1,
seatId: 1,
});
const headers = {
'Content-Type': 'application/json',
'Queue-Token': QUEUE_TOKEN,
};
// ์์ฝ ์์ฒญ
const res = http.post(BASE_URL, payload, { headers: headers });
check(res, {
'reservation success': (r) => r.json().status == 'success',
});
}
ํ ์คํธ ๊ฒฐ๊ณผ
๊ฐ ํ ์คํธ๋ง๋ค์ cli ๊ฒฐ๊ณผํ๋ฉด๊ณผ ๋์ผ ๋ฐ์ดํฐ๋ฅผ ์ข ๋ ๋ณด๊ธฐ์ฝ๋๋ก grafana๋ก ์๊ฐํํ ๊ทธ๋ํ๋ฅผ ์บก์ณํ์ต๋๋ค.
1. ๋๊ด์ ๋ฝ (Optimistic Lock)
2. ๋น๊ด์ ๋ฝ (Pessimistic Lock)
3. ๋ถ์ฐ ๋ฝ (Distributed Lock with Redisson)
ํ ์คํธ ๊ฒฐ๊ณผ
- ๋ชจ๋ ๋ฝ์์, 1,000๊ฐ์ ์์ฒญ ์ค 1๊ฐ๋ง ์ฑ๊ณตํ๊ณ 999๊ฐ๊ฐ ์คํจํ๋ ๊ฒ์ ๋น์ฆ๋์ค ๋ก์ง์์ ์ ์์ ์ธ ๊ฒฐ๊ณผ์ ๋๋ค. ํ ์ข์์ ๋ํด ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ์ ๊ทผํ๋ ๊ฒฝ์ฐ, ํ ๋ช ๋ง ์ฑ๊ณตํ๊ณ ๋๋จธ์ง๋ ๋ชจ๋ ์คํจํ๋๋ก ์ค๊ณํด๋์๋๋ก ๋์ํจ์ ํ์ธํ ์ ์์์ต๋๋ค.
- ๋๊ด์ ๋ฝ์ ํ๊ท ์๋ต ์๊ฐ์ ์ฝ 31.04ms๋ก ๋ํ๋ฌ์ผ๋ฉฐ, ์ต์ฅ ์๋ต ์๊ฐ์ 189.97ms๋ก ์ธก์ ๋์์ต๋๋ค. ๋๊ด์ ๋ฝ์ด ์ฌ์๋ ์์ด ๋น ๋ฅด๊ฒ ์คํจ๋ฅผ ์ฒ๋ฆฌํ๊ณ , ์ฑ๊ณต์ ์ธ ์์ฒญ์ ๋ํด์๋ง ์์์ ์ ๋ฐ์ดํธํ๊ธฐ ๋๋ฌธ์ 3๊ฐ์ ๋์์ฑ ์ฒ๋ฆฌ ์ค ๊ฐ์ฅ ๋น ๋ฅธ ํ๊ท ์๋ต ์๊ฐ์ ์ ์งํ ์ ์์์ต๋๋ค.
์ต์ข ์ ์ฉ ๋ฐฉ์
1. ์ข์ ์์ฝ - ๋๊ด์ ๋ฝ (Optimistic Lock)
์ ์ ์ด์
์ฝ์ํธ ์์ฝ์ ๊ฒฝ์ฐ, ์ด๋ฏธ ์์ฝ๋ ์ข์์ ๋ค์ ์์ฝํ๋ ค๋ ์๋๋ ๋๋ฌผ๊ธฐ ๋๋ฌธ์ ์ถํ์ ๊ฒฝํฉ์ด ์ค์ด๋ค๊ฒ ๋ฉ๋๋ค. ๋์ ๊ฒฝํฉ ์์๋ '์ด๋ฏธ ์ ์ ๋ ์ข์'์ ๋ํด ์์ฝ ์คํจํ ์ฌ์ฉ์์๊ฒ ๋น ๋ฅธ ํผ๋๋ฐฑ์ ์ ๊ณตํ์ฌ ์ฆ์ ๋ค๋ฅธ ์ข์์ ์์ฝํ๋๋ก ์ ๋ํ๋ ๊ฒ์ด ๋น์ฆ๋์ค์ ์ผ๋ก ์ค์ํ๋ค๊ณ ํ๋จ๋ฉ๋๋ค. ๊ฒฝํฉ์ด ๋๊ฐ ์ด๊ธฐ์ ์ง์ค๋๋ฉฐ, ์ดํ์๋ ์์์ ์ํ๊ฐ ์์ ํ๋๊ธฐ ๋๋ฌธ์ ๋๊ด์ ๋ฝ์ด ์ต์ ์ ์ ํ์ด๋ผ๊ณ ์๊ฐ๋์์ต๋๋ค.
2. ํฌ์ธํธ ์ถฉ์ ๋ฐ ๊ฒฐ์ - ๋๊ด์ ๋ฝ (Optimistic Lock)
์ ์ ์ด์
ํฌ์ธํธ๋ ์ฌ์ฉ์์๊ฒ ๋ฏผ๊ฐํ ์ ๋ณด์ด๋ฏ๋ก, ์ถฉ๋ ๋ฐ์ ์ ์ฑ๊ณต ๋๋ ์คํจ ์ฌ๋ถ๋ฅผ ์ฌ์ฉ์์๊ฒ ๋น ๋ฅด๊ฒ ์๋ฆฌ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ์ฌ ๋น ๋ฅด๊ฒ ํผ๋๋ฐฑ์ ์ ๊ณตํจ์ผ๋ก์จ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ ์ ์์ผ๋ฉฐ, ์์ธ ์ฒ๋ฆฌ๋ฅผ ํตํด ์ผ๊ด์ฑ์ ์ ์งํ ์ ์์ผ๋ฏ๋ก ๋๊ด์ ๋ฝ์ด ์ ํฉํ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
๋ง์น๋ฉฐ
์ข์ ์์ฝ๊ณผ ํฌ์ธํธ ๊ฒฐ์ ๋ ์ค์๊ฐ์ ์ธ ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ด ์ค์ํ ๋น์ฆ๋์ค ๋ก์ง์ ๋๋ค. ์ด๋ฒ ์๊ฐ์ ํตํด ๋ค์ํ ๋์์ฑ ์ ์ด ๋ฐฉ์์ ๊ฒํ ํ๊ณ , ์ต์ข ์ ์ผ๋ก ์ฝ์ํธ ์์ฝ ์๋น์ค์๋ ๋๊ด์ ๋ฝ์ ๋น์ฆ๋์ค ์ํฉ์ ๋ง๋ ๋์์ฑ ์ฒ๋ฆฌ ๋ฐฉ์์ผ๋ก ์ ์ ํ์ต๋๋ค. ์์ผ๋ก๋ ์์คํ , ๋น์ฆ๋์ค ๋ก์ง์ ๋ง๋ ๊ธฐ์ ์ ์ ์ ํ๋๋ฐ์ ์์ด ์ง๊ธ๊ณผ ๊ฐ์ ๋ง์ ๊ณ ๋ฏผ์ด ํ์ํด ๋ณด์ ๋๋ค.