Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions OrderServiceThreadSafe.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.example

import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ThreadLocalRandom
import java.util.concurrent.atomic.AtomicInteger

// 주문 정보를 저장하는 데이터 클래스
data class OrderInfo(
val productName: String,
val amount: Int,
val timestamp: Long = System.currentTimeMillis()
)

// 주문 처리를 담당하는 도메인 서비스
class OrderServiceThreadSafe {

// 상품 DB: AtomicInteger로 각 상품의 재고를 관리하여 thread-safe 보장
private val productDatabase = ConcurrentHashMap(
mapOf(
"apple" to AtomicInteger(100),
"banana" to AtomicInteger(50),
"orange" to AtomicInteger(75)
)
)

// ThreadLocal을 사용하여 각 스레드마다 독립적인 주문 데이터 보관
private val threadLocalOrderDatabase = ThreadLocal.withInitial {
mutableMapOf<String, MutableList<OrderInfo>>()
}

// 주문 처리 메서드
fun order(productName: String, amount: Int) {
val stock = productDatabase[productName]
?: throw IllegalArgumentException("Product not found: $productName")

// 재고 감소 (원자적 감소)
val remainingStock = stock.updateAndGet { currentStock ->
if (currentStock >= amount) {
Thread.sleep(ThreadLocalRandom.current().nextLong(5, 10)) // 지연 시간 추가
currentStock - amount
} else {
println("Thread ${Thread.currentThread().id} - Insufficient stock for $productName")
currentStock // 재고 부족 시 변경하지 않음
}
}

if (remainingStock >= 0) {
// 스레드별 주문 데이터 저장
val orderInfo = OrderInfo(productName, amount)
threadLocalOrderDatabase.get().computeIfAbsent(productName) { mutableListOf() }.add(orderInfo)

// 로그 출력
println("Thread ${Thread.currentThread().id} 주문 정보: $productName: 1 건 ([${amount}])")
}
}

// 재고 조회
fun getStock(productName: String): Int {
return productDatabase[productName]?.get() ?: 0
}

// 스레드별 주문 데이터 조회
fun getThreadLocalOrderList(): Map<String, List<OrderInfo>> {
return threadLocalOrderDatabase.get()
}
}
48 changes: 48 additions & 0 deletions OrderServiceThreadSafeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.example

import org.junit.jupiter.api.Test
import java.util.concurrent.Executors
import java.util.concurrent.CountDownLatch
import kotlin.test.assertEquals

class OrderServiceThreadSafeTest {

private val service = OrderServiceThreadSafe()

@Test
fun `동시 주문 테스트 - Thread-safe 검증`() {
val productName = "apple"
val orderAmount = 8
val threadCount = 100
val initialStock = service.getStock(productName)

// 동시성을 위한 스레드 풀과 CountDownLatch 생성
val executor = Executors.newFixedThreadPool(threadCount)
val latch = CountDownLatch(threadCount)

repeat(threadCount) { threadId ->
executor.submit {
try {
// 주문 수행
service.order(productName, orderAmount)
println("Thread $threadId 주문 정보: $productName: 1 건 ([${orderAmount}]) \n")
} finally {
latch.countDown() // 스레드 작업 완료
}
}
}

// 모든 스레드의 작업 완료 대기
latch.await()
executor.shutdown()

// 최종 재고 계산 및 검증
val expectedStock = initialStock % orderAmount
val finalStock = service.getStock(productName)

println("Expected Stock: $expectedStock, Final Stock: $finalStock")

// 검증
assertEquals(expectedStock, finalStock, "재고가 예상 값과 일치하지 않습니다.")
}
}