@Test
void increaseSameTime() throws Exception {
Long accountId = bankAccountRepository.findAll().get(0).getId();
int threadCount = 2; // 동시에 2개의 입금 요청
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
balanceFacade.increase(accountId, 1000L);
System.out.println("Deposit successful");
} catch (Exception e) {
System.out.println("Deposit failed: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await(); // 모든 입금 요청이 완료될 때까지 대기
executor.shutdown();
BankAccount updatedAccount = bankAccountRepository.findById(accountId).orElseThrow();
assertEquals(10000L, updatedAccount.getBalance()); // 동시 입금 요청에 대한 실패 예상으로, 잔액은 변하지 않아야 함
}
@Test
void decreaseSameTime() throws Exception {
// 잔고 출금 요청이 동시에 2개 이상 올 경우 차례대로 실행
Long accountId = bankAccountRepository.findAll().get(0).getId();
ExecutorService executor = Executors.newFixedThreadPool(2);
CompletableFuture<Void> withdrawal1 = CompletableFuture.runAsync(() -> {
try {
balanceFacade.decrease(accountId, 1000L);
System.out.println("Withdrawal 1 successful");
} catch (Exception e) {
System.out.println("Withdrawal 1 failed: " + e.getMessage());
}
}, executor);
CompletableFuture<Void> withdrawal2 = CompletableFuture.runAsync(() -> {
try {
balanceFacade.decrease(accountId, 1000L);
System.out.println("Withdrawal 2 successful");
} catch (Exception e) {
System.out.println("Withdrawal 2 failed: " + e.getMessage());
}
}, executor);
CompletableFuture.allOf(withdrawal1, withdrawal2).join();
executor.shutdown();
BankAccount updatedAccount = bankAccountRepository.findById(accountId).orElseThrow();
assertEquals(8000L, updatedAccount.getBalance());
}
@Test
void increaseAndDecreaseSameTime() throws Exception {
// 잔고 입금과 출금 요청은 동시에 올 수 있고, 요청 온 차례대로 실행
// 입금->출금 , 출금->입금으로 로직 작성할 것
Long accountId = bankAccountRepository.findAll().get(0).getId();
ExecutorService executor = Executors.newFixedThreadPool(2);
CompletableFuture<Void> deposit = CompletableFuture.runAsync(() -> {
try {
balanceFacade.increase(accountId, 1000L);
System.out.println("Deposit successful");
} catch (Exception e) {
System.out.println("Deposit failed: " + e.getMessage());
}
}, executor);
CompletableFuture<Void> withdrawal = CompletableFuture.runAsync(() -> {
try {
balanceFacade.decrease(accountId, 1000L);
System.out.println("Withdrawal successful");
} catch (Exception e) {
System.out.println("Withdrawal failed: " + e.getMessage());
}
}, executor);
CompletableFuture.allOf(deposit, withdrawal).join();
executor.shutdown();
BankAccount updatedAccount = bankAccountRepository.findById(accountId).orElseThrow();
assertEquals(10000L, updatedAccount.getBalance());
}
이게 너무 어려웠음.. 트랜잭션을 관리한다고할때 보통은 동시에 요청이 들어오면 트랜잭션을 차지한 하나는 성공 다른 하나는 실패 이게 일반적인 로직인데 둘다 실패라는건 이전 트랜잭션을 관리해야하는거라 로직을 계속 수정해도 실패했음.
어플리케이션 단계에서의 동시성은 고려가능하지만.. 다중 인스턴스를 주로 쓰는 실무환경에선 내가 작성한 로직을 적용하기 어려울수도있겠구나싶음