Java ATM CLI 개발 로그 #2: 현금 이체, 멈춤?
Source: Dev.to
Source: …
서술
이 기능을 만드는 것이 이렇게 까다로울 줄은 몰랐습니다. 개발을 진행하던 중에는 모든 것이 순조롭게 진행됐지만, 함수가 실행 중에 hang(멈춤) 현상이 발생했습니다.
문제의 원인을 파악하기 위해 ChatGPT와 Gemini를 활용해 깊이 파고들었습니다. 그 과정에서 COMMIT, ROLLBACK에 대해 배우게 되었고, 특히 제가 만들고 있는 금융 애플리케이션과 같은 경우에 트랜잭션이 왜 중요한지도 알게 되었습니다.
Transactions는 단일 논리적 단위로 취급되는 일련의 SQL 문입니다. 트랜잭션은 데이터 변경 작업으로 시작하여
COMMIT(성공) 또는ROLLBACK(오류) 중 하나로 종료됩니다.
초기 구현
AccountService.java
public static void transferCash(Account sender, Account receiver, double amount) {
if (sender.getBalance() > amount) {
double newSenderBalance = sender.getBalance() - amount;
double newReceiverBalance = receiver.getBalance() + amount;
boolean isTransactionSuccessful = AccountDAO.makeTransfer(
sender.getId(),
receiver.getId(),
sender.getAccountType(),
receiver.getAccountType(),
newSenderBalance,
newReceiverBalance
);
if (isTransactionSuccessful) {
System.out.println("Transaction Successful!");
sender.setBalance(newSenderBalance);
System.out.println("Withdrawal successful. New balance: $" + sender.getBalance());
sender.stringifyAccount();
} else {
System.out.println("Transaction Failed!");
}
} else {
System.out.println("Insufficient funds.");
}
}
AccountDAO.java
public static boolean makeTransfer(
int senderId,
int receiverId,
String senderAccountType,
String receiverAccountType,
double newSenderBalance,
double newReceiverBalance) {
String senderQuery = "UPDATE accounts SET balance = ? WHERE customerId = ? AND accountType = ?";
String receiverQuery = "UPDATE accounts SET balance = ? WHERE customerId = ? AND accountType = ?";
boolean isTransactionSuccessful = false;
try (Connection conn = DBHelper.getConnection()) {
conn.setAutoCommit(false);
// ---- Sender ----
try (PreparedStatement senderStmt = conn.prepareStatement(senderQuery)) {
senderStmt.setDouble(1, newSenderBalance);
senderStmt.setInt(2, senderId);
senderStmt.setString(3, senderAccountType);
int senderRowsAffected = senderStmt.executeUpdate();
if (senderRowsAffected == 0) {
conn.rollback();
return false;
}
} catch (SQLException e) {
e.printStackTrace();
conn.rollback();
return false;
}
// ---- Receiver ----
try (PreparedStatement receiverStmt = conn.prepareStatement(receiverQuery)) {
receiverStmt.setDouble(1, newReceiverBalance);
receiverStmt.setInt(2, receiverId);
receiverStmt.setString(3, receiverAccountType);
int receiverRowsAffected = receiverStmt.executeUpdate();
if (receiverRowsAffected == 0) {
conn.rollback();
return false;
}
} catch (SQLException e) {
e.printStackTrace();
conn.rollback();
return false;
}
conn.commit();
System.out.println("\nBalance Updated Successfully!");
isTransactionSuccessful = true;
} catch (Exception e) {
e.printStackTrace();
isTransactionSuccessful = false;
}
return isTransactionSuccessful;
}
시니어 엔지니어 피드백 후 리팩터링
은행‑Java 경험을 가진 시니어 엔지니어가 이전 접근 방식이 잘못된 관행이라고 지적했습니다.
서비스 레이어를 두 개의 더 작고 단일 목적의 작업인 debit와 credit으로 분리했습니다.
업데이트된 AccountService.java
public class AccountService {
public static void debitAccount(Account account, double amount) {
if (account.getBalance() > amount) {
double newBalance = account.getBalance() - amount;
boolean isTransactionSuccessful = AccountDAO.changeBalance(
account.getId(),
account.getAccountType(),
newBalance
);
if (isTransactionSuccessful) {
System.out.println("Transaction Successful!");
account.setBalance(newBalance);
System.out.println("Withdrawal successful. New balance: $" + account.getBalance());
account.stringifyAccount();
} else {
System.out.println("Transaction Failed!");
}
} else {
System.out.println("Insufficient funds.");
}
}
public static void creditAccount(Account account, double amount) {
double newBalance = account.getBalance() + amount;
boolean isTransactionSuccessful = AccountDAO.changeBalance(
account.getId(),
account.getAccountType(),
newBalance
);
if (isTransactionSuccessful) {
System.out.println("Transaction Successful!");
account.setBalance(newBalance);
System.out.println("Deposit successful. New balance: $" + account.getBalance());
account.stringifyAccount();
} else {
System.out.println("Transaction Failed!");
}
}
}
Note:
AccountDAO.changeBalance는 단일 계좌의 잔액을 업데이트하는 새로운 헬퍼입니다.
각 DB 작업이 이제 원자적으로 수행되어 호출자가 적절한 트랜잭션 범위 내에서 전체 이체(출금 → 입금)를 조정할 수 있습니다.
현재 상황
모든 변경을 마친 후, 나는 이 기능에 대한 작업을 일시 중지하고, 애플리케이션의 남은 부분을 마무리한 뒤 나중에 전송 로직을 제대로 고치기로 했습니다.
그때 나는 Java에서 트랜잭션을 올바르게 처리하는 방법에 대해 어느 정도 숙달했을 것입니다.
GitHub 저장소를 확인하고 싶다면, click here를 클릭하여 직접 확인할 수 있습니다.
음, 지금은 여기까지입니다.
다음 커밋까지, 안녕 👋