diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f814142 --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.1' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..34b3170 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'redis_1st_pre' diff --git a/src/main/java/com/example/threadsafepractice/OrderServiceWithConcurrentHashMap.java b/src/main/java/com/example/threadsafepractice/OrderServiceWithConcurrentHashMap.java new file mode 100644 index 0000000..9477e1a --- /dev/null +++ b/src/main/java/com/example/threadsafepractice/OrderServiceWithConcurrentHashMap.java @@ -0,0 +1,54 @@ +package com.example.threadsafepractice; + +import java.util.concurrent.ConcurrentHashMap; + +public class OrderServiceWithConcurrentHashMap { + // 상품 DB + private final ConcurrentHashMap productDatabase = new ConcurrentHashMap<>(); + // 가장 최근 주문 정보를 저장하는 DB + private final ConcurrentHashMap latestOrderDatabase = new ConcurrentHashMap<>(); + + public OrderServiceWithConcurrentHashMap() { + // 초기 상품 데이터 + productDatabase.put("apple", 100); + productDatabase.put("banana", 50); + productDatabase.put("orange", 75); + } + + // 주문 처리 메서드 + public void order(String productName, int amount) { + productDatabase.compute(productName, (key, currentStock) -> { + if (currentStock == null) { + throw new IllegalArgumentException("상품이 존재하지 않습니다."); + } + + try { + Thread.sleep(1); // 동시성 이슈 유발을 위한 인위적 지연 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + if (currentStock >= amount) { + System.out.printf("Thread %d 주문 정보:\n", Thread.currentThread().threadId()); + System.out.printf("%8s: %d 건 ([%d])\n", productName, 1, amount); + + productDatabase.put(productName, currentStock - amount); + latestOrderDatabase.put(productName, new OrderInfo(productName, amount, System.currentTimeMillis())); + return currentStock - amount; // 기존 재고에서 주문 수량 차감 + } + System.out.println("[ERROR] 재고 부족: " + productName); + return currentStock; // 차감할 수 없으면 기존 재고 유지 + }); + } + + public static class OrderInfo { + public OrderInfo(String productName, int amount, long orderTime) { + } + } + + // 재고 조회 + public int getStock(String productName) { + return productDatabase.getOrDefault(productName, 0); + } +} diff --git a/src/main/java/com/example/threadsafepractice/OrderServiceWithReentrantLock.java b/src/main/java/com/example/threadsafepractice/OrderServiceWithReentrantLock.java new file mode 100644 index 0000000..a667601 --- /dev/null +++ b/src/main/java/com/example/threadsafepractice/OrderServiceWithReentrantLock.java @@ -0,0 +1,66 @@ +package com.example.threadsafepractice; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +public class OrderServiceWithReentrantLock { + + // 상품 DB (공유 자원) + private final Map productDatabase = new HashMap<>(); + private final Map latestOrderDatabase = new HashMap<>(); + + // ReentrantLock 객체 추가 + private final ReentrantLock lock = new ReentrantLock(); + + public OrderServiceWithReentrantLock() { + // 초기 상품 데이터 설정 + productDatabase.put("apple", 100); + productDatabase.put("banana", 50); + productDatabase.put("orange", 75); + } + + // ReentrantLock을 사용하여 동기화된 주문 처리 메서드 + public void order(String productName, int amount) { + lock.lock(); // 락 획득 (스레드 간 경합 방지) + try { + // 재고 체크 및 감소 로직 + Integer currentStock = productDatabase.getOrDefault(productName, 0); + + try { + Thread.sleep(1); // 동시성 이슈 유발을 위한 인위적 지연 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + if (currentStock >= amount) { + System.out.printf("Thread %d 주문 정보:\n", Thread.currentThread().threadId()); + System.out.printf("%8s: %d 건 ([%d])\n", productName, 1, amount); + + // 재고 감소 + productDatabase.put(productName, currentStock - amount); + latestOrderDatabase.put(productName, new OrderInfo(productName, amount, System.currentTimeMillis())); + } else { + System.out.println("[ERROR] 재고 부족: " + productName); + } + } finally { + lock.unlock(); // 락 해제 (데드락 방지) + } + } + + public static class OrderInfo { + public OrderInfo(String productName, int amount, long orderTime) { + } + } + + // 재고 조회 메서드 + public int getStock(String productName) { + lock.lock(); // 락 획득 + try { + return productDatabase.getOrDefault(productName, 0); + } finally { + lock.unlock(); // 락 해제 + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8fee565 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=thread-safe-practice diff --git a/src/test/java/com/example/threadsafepractice/OrderServiceWithConcurrentHashMapTest.java b/src/test/java/com/example/threadsafepractice/OrderServiceWithConcurrentHashMapTest.java new file mode 100644 index 0000000..1bfc7e1 --- /dev/null +++ b/src/test/java/com/example/threadsafepractice/OrderServiceWithConcurrentHashMapTest.java @@ -0,0 +1,50 @@ +package com.example.threadsafepractice; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; + +class OrderServiceWithConcurrentHashMapTest { + + private final OrderServiceWithConcurrentHashMap service = new OrderServiceWithConcurrentHashMap(); + + @Test + void testConcurrentOrdersHandlesStockCorrectly() 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("Expected Stock: " + expectedStock + ", Actual Stock: " + actualStock); + + // 동시성 이슈 없이 재고가 일치하는 것을 확인 + assertEquals(expectedStock, actualStock); + } + +} diff --git a/src/test/java/com/example/threadsafepractice/OrderServiceWithReentrantLockTest.java b/src/test/java/com/example/threadsafepractice/OrderServiceWithReentrantLockTest.java new file mode 100644 index 0000000..75b5ca8 --- /dev/null +++ b/src/test/java/com/example/threadsafepractice/OrderServiceWithReentrantLockTest.java @@ -0,0 +1,50 @@ +package com.example.threadsafepractice; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; + +class OrderServiceWithReentrantLockTest { + + private final OrderServiceWithReentrantLock service = new OrderServiceWithReentrantLock(); + + @Test + void testConcurrentOrdersHandlesStockCorrectly() 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("Expected Stock: " + expectedStock + ", Actual Stock: " + actualStock); + + // 동시성 이슈 없이 재고가 일치하는 것을 확인 + assertEquals(expectedStock, actualStock); + } + +}