diff --git a/order_invens/src/main/java/OrderInfo.java b/order_invens/src/main/java/OrderInfo.java new file mode 100644 index 0000000..69648ab --- /dev/null +++ b/order_invens/src/main/java/OrderInfo.java @@ -0,0 +1,41 @@ +/** + * OrderInfo 클래스는 주문 정보를 담는 데이터 객체입니다. + * - 불변성을 유지하며, 주문 수량을 누적할 때 새로운 인스턴스를 생성합니다. + */ +public class OrderInfo { + private final String productName; + private final int amount; + private final long timestamp; + + /** + * 생성자: 주문 정보를 초기화합니다. + * + * @param productName 주문한 상품명 + * @param amount 주문 수량 + * @param timestamp 주문 시각 (밀리초 단위) + */ + public OrderInfo(String productName, int amount, long timestamp) { + this.productName = productName; + this.amount = amount; + this.timestamp = timestamp; + } + + /** + * 주문 수량을 누적하고 새로운 OrderInfo 객체를 반환합니다. + * + * @param additionalAmount 추가 주문 수량 + * @return 새로운 OrderInfo 객체 + */ + public OrderInfo incrementAmount(int additionalAmount) { + return new OrderInfo(this.productName, this.amount + additionalAmount, System.currentTimeMillis()); + } + + @Override + public String toString() { + return "OrderInfo{" + + "productName='" + productName + '\'' + + ", amount=" + amount + + ", timestamp=" + timestamp + + '}'; + } +} diff --git a/order_invens/src/main/java/OrderService.java b/order_invens/src/main/java/OrderService.java new file mode 100644 index 0000000..b51a768 --- /dev/null +++ b/order_invens/src/main/java/OrderService.java @@ -0,0 +1,98 @@ +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class OrderService { + + private final Map productDatabase = new ConcurrentHashMap<>(); + + private final ThreadLocal> latestOrderDatabase = + ThreadLocal.withInitial(ConcurrentHashMap::new); + + public OrderService() { + productDatabase.put("apple", 100); + productDatabase.put("banana", 50); + productDatabase.put("orange", 75); + } + + /** + * 주문 처리 메서드 + * @param productName 주문할 상품명 + * @param amount 주문 수량 + */ + public void order(String productName, int amount) { + productDatabase.compute(productName, (key, currentStock) -> { + + // 해당 상품이 없으면 아무 작업도 하지 않음 + if (currentStock == null) { + return null; + } + + // 인위적 지연: 동시성 이슈를 유발하기 위한 지연 (테스트 목적) + try { + Thread.sleep(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + // 재고가 충분한 경우 재고를 차감 + if (currentStock >= amount) { + int updatedStock = currentStock - amount; + + // 주문 내역 갱신 + Map perThreadOrderMap = latestOrderDatabase.get(); + OrderInfo existingOrder = perThreadOrderMap.get(key); + + if (existingOrder == null) { + // 첫 번째 주문인 경우 새로운 OrderInfo 생성 + perThreadOrderMap.put(key, new OrderInfo(key, amount, System.currentTimeMillis())); + } else { + // 기존 주문 내역이 있는 경우 수량을 누적하고 타임스탬프 업데이트 + perThreadOrderMap.put(key, existingOrder.incrementAmount(amount)); + } + + System.out.println( + "주문 완료: " + Thread.currentThread().getName() + + " | Order: " + amount + + " | Updated Stock: " + updatedStock + ); + + return updatedStock; + } else { + // 재고가 부족한 경우 재고를 그대로 유지 + System.out.println( + "재고 부족: " + Thread.currentThread().getName() + + " | Order: " + amount + + " | Insufficient Stock: " + currentStock + ); + return currentStock; + } + }); + } + + /** + * 재고 조회 메서드 + * @param productName 조회할 상품명 + * @return 현재 재고 수량 + */ + public int getStock(String productName) { + return productDatabase.getOrDefault(productName, 0); + } + + /** + * (옵션) 각 스레드(=유저)가 구매한 최신 주문 정보 조회 + * @param productName 조회할 상품명 + * @return 최신 OrderInfo 객체 또는 null + */ + public OrderInfo getLatestOrderForThread(String productName) { + return latestOrderDatabase.get().get(productName); + } + + /** + * (옵션) ThreadLocal 데이터 정리 메서드 + */ + public void removeThreadLocalData() { + latestOrderDatabase.remove(); + } +} \ No newline at end of file diff --git a/order_invens/src/test/java/OrderServiceTest.java b/order_invens/src/test/java/OrderServiceTest.java new file mode 100644 index 0000000..9683f3a --- /dev/null +++ b/order_invens/src/test/java/OrderServiceTest.java @@ -0,0 +1,64 @@ +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.*; + +class OrderServiceTest { + private final OrderService service = new OrderService(); + + /** + * 테스트 후 ThreadLocal 데이터를 정리하여 쓰레드 풀 환경에서의 데이터 혼동을 방지합니다. + */ + @AfterEach + void tearDown() { + service.removeThreadLocalData(); + } + + /** + * @throws InterruptedException 스레드 대기 중 인터럽트 발생 시 + */ + @Test + void testConcurrentOrdersShouldLeaveCorrectStock() throws InterruptedException { + String productName = "apple"; + int initialStock = service.getStock(productName); + + int orderAmount = 8; + int threadCount = 100; + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + // 각 스레드에서 주문을 수행하는 작업 생성 + for (int i = 0; i < threadCount; i++) { + executor.execute(() -> { + try { + service.order(productName, orderAmount); + } finally { + latch.countDown(); // 작업 완료 후 카운트 감소 + } + }); + } + + // 모든 스레드가 작업을 완료할 때까지 대기 + latch.await(); + executor.shutdown(); + + // 최종 재고 값 확인 + int expectedStock = initialStock % orderAmount; + int actualStock = service.getStock(productName); + + // 테스트 결과를 로그로 출력합니다. + System.out.println("=============================================="); + System.out.println("초기 Stock : " + initialStock); + System.out.println("기대 Stock : " + expectedStock); + System.out.println("결과 Stock : " + actualStock); + System.out.println("=============================================="); + + // 동시성 이슈로 인해 재고가 맞지 않는 경우를 확인 + assertEquals(expectedStock, actualStock, "재고 불일치 발생!"); + } +} \ No newline at end of file