Java ATM CLI 개발 로그 #2: 현금 이체, 멈춤?

발행: (2025년 12월 16일 오전 08:52 GMT+9)
5 min read
원문: Dev.to

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 경험을 가진 시니어 엔지니어가 이전 접근 방식이 잘못된 관행이라고 지적했습니다.
서비스 레이어를 두 개의 더 작고 단일 목적의 작업인 debitcredit으로 분리했습니다.

업데이트된 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를 클릭하여 직접 확인할 수 있습니다.

음, 지금은 여기까지입니다.

다음 커밋까지, 안녕 👋

Back to Blog

관련 글

더 보기 »