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
41 changes: 41 additions & 0 deletions order_invens/src/main/java/OrderInfo.java
Original file line number Diff line number Diff line change
@@ -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 +
'}';
}
}
98 changes: 98 additions & 0 deletions order_invens/src/main/java/OrderService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class OrderService {

private final Map<String, Integer> productDatabase = new ConcurrentHashMap<>();

private final ThreadLocal<Map<String, OrderInfo>> 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<String, OrderInfo> 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();
}
}
64 changes: 64 additions & 0 deletions order_invens/src/test/java/OrderServiceTest.java
Original file line number Diff line number Diff line change
@@ -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, "재고 불일치 발생!");
}
}