본문 바로가기
JAVA

[JAVA ] JDBC 활용한 미니 프로젝트 - 3단계

by 방준이 2021. 9. 21.
반응형

0. 계좌관리 프로그램

해당 프로젝트는 KOSTA EDU 교육에서 진행하는 수업의 일부를 공부하는 목적으로 올리는 용도입니다 :) 단계별로 프로젝트를 실습할 예정이며 TestUnit 클래스를 먼저 만들고 여러 가지 예외적인 상황에 대해서 생각해 본 후 해당 기능을 구현하는 순서로 진행하였습니다. 참고로 TestUnit 클래스는 하나의 기능을 테스트하는 목적으로 설계된 클래스입니다.

 

1. 요구사항8  출금하는 기능 TestUnit Class 작성하기

 

요구사항8 : 출금 시에는 계좌번호, 비밀번호가 일치해야 하며 잔액 확인 절차가 필요하다. 어디서 본 문장인 듯하다 요구사항5는 다음과 같다.  

요구사항5:  "잔액조회는 계좌번호가 존재해야 하고 계좌번호에 맞는 비밀번호가 일치해야 한다."

 

따라서 잔액 확인을 위해서 만들어 놓았던 findBalanceByAccountNo() 메서드를 재활용하여 코드를 구성하면 코드의 중복을 피하고 간단히 구성할 수 있다.  UnitTest 클래스에서는 어떤 테스트를 해봐야 할지 생각해 보자.  잔액을 현재 잔액 이상으로 출금을 시도하거나 존재하지 않는 계좌번호, 틀린 패스워드를 입력해서 여러 가지 예외상황을 테스트해 볼 수 있을 것이다. 또한 그에 해당하는 예외처리 메시지를 전달하는지를 살펴보자. 참고로 계좌번호의 존재 유무 및 패스워드 일치 여부는 findBalanceByAccountNo() 메서드를 통해서 확인할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package test;
 
import java.sql.SQLException;
import model.AccountDAO;
import model.AccountNotFoundException;
import model.InsufficientBalanceException;
import model.NoMoneyException;
import model.NotMatchedPasswordException;
 
// 출금 테스트
public class TestUnit5 {
    public static void main(String[] args) {        
        try {
            AccountDAO dao=new AccountDAO();
            String accountNo="1";            //출금계좌번호
            String password="1234";            //계좌패스워드
            int money=100;                    //출금액
            System.out.println("출금전 계좌잔액:"+dao.findBalanceByAccountNo(accountNo, password));
            dao.withdraw(accountNo,password,money);
            System.out.println("출금후 계좌잔액:"+dao.findBalanceByAccountNo(accountNo, password));
        }catch(NoMoneyException e) {
            System.out.println(e.getMessage());
        }catch(AccountNotFoundException e) { 
            System.out.println(e.getMessage());
        }catch(NotMatchedPasswordException e) {    
            System.out.println(e.getMessage());
        }catch(InsufficientBalanceException e) {
            System.out.println(e.getMessage());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
 
 

 

2.  출금하는 기능 withdraw() 메서드 구현하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void withdraw(String accountNo, String password, int money) throws NoMoneyException, SQLException, 
AccountNotFoundException, NotMatchedPasswordException, InsufficientBalanceException {
    if(money<=0)
        throw new NoMoneyException("출금액은 0원을 초과해야 합니다");
    int balance=findBalanceByAccountNo(accountNo, password);
    if(balance<money)
        throw new InsufficientBalanceException("잔액보다 출금액이 커서 출금할 수 없습니다");
    Connection con=null;
    PreparedStatement pstmt=null;
    try {
        con=getConnection();
        String sql="update account set balance=balance-? where account_no=?";
        pstmt=con.prepareStatement(sql);
        pstmt.setInt(1, money);
        pstmt.setString(2, accountNo);
        pstmt.executeUpdate();
    }finally {
        closeAll(pstmt, con);
    }
}
 
 

 

간단히 설명을 하면,  요구사항에 의해 출금액이 0원 이하이면 NoMoneyException을 발생시키고 throw 한다. 계좌번호가 존재하지 않거나 패스워드가 다른 경우 findBalanceByAccountNo() 메서드를 통해서 예외가 발생되고 계좌번호가 존재하며 패스워드가 일치한다는 것을 보장받을 수 있다.  또한 메서드의 반환 값을 통해서  출금액이 현재 존재하는 잔고보다 큰 경우 예외를 발생시키고 그렇지 않다면 DB의 update를 통해서 출금 작업을 완성할 수 있다. 

 

 

3-1. 기능의 분할, 계좌 존재 유무 확인 기능 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test;
 
import java.sql.SQLException;
import model.AccountDAO;
 
// 계좌번호 존재유무를 확인하는 메서드 테스트 
public class TestUnit6 {
    public static void main(String[] args) {
        try {
            AccountDAO dao=new AccountDAO();
            String accountNo="1"; // 존재o
            accountNo="11"; // 존재X
            boolean result=dao.existsAccountNo(accountNo);
            if(result)
                System.out.println(accountNo+" 계좌가 존재합니다");
            else
                System.out.println(accountNo+" 계좌가 존재하지 않습니다");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
 
 

 

계좌 존재의 유무를 확인하는 테스트는 위와 같다. 이 경우 해볼 수 있는 테스트는 딱 2가지가 있다. 계좌가 존재하는 경우와 존재하지 않는 경우로 나뉘어서 테스트를 수행해 볼 수 있다. 

 

3-2. 계좌 존재 유무확인 기능 existsAccountNo() 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean existsAccountNo(String accountNo) throws SQLException {
    boolean result=false;
    Connection con=null;
    PreparedStatement pstmt=null;
    ResultSet rs=null;
    try {
        con=getConnection();
        String sql="select count(*) from account where account_no=?";
        pstmt=con.prepareStatement(sql);
        pstmt.setString(1, accountNo);
        rs=pstmt.executeQuery();
        if(rs.next()&&rs.getInt(1)==1)         //조회 결과가 1 이면 존재하므로 true를 할당 
            result=true;
    }finally {
        closeAll(rs, pstmt, con);
    }
    return result;
}
 
 

 

계좌존재 유무를 확인하는 메서드를 구현하면 위와 같다. 여기서 핵심이라고 하면은 sql문이 되겠다. 

밑의 2개의 sql문의 차이를 살펴보자

 

  • String sql = "select count(*) from account where account_no = ?";  
  • String sql  = "select password from account where account_no = ?";

적어도 계좌의 존재 유무를 확인하는 상황에서 만큼은 첫 번째 코드가 더 좋다고 말할 수 있다. 아래의 경우 계좌번호는 primary key 값으로 계좌번호가 존재한다면 password든 어떤 속성이든 값을 전달받을 수 있다.  password의 경우 not null이기 때문에 필히 값이 존재하겠지만 not null 이 아닌 경우에는 값을 전달받지 못할 수 있다. 따라서 어떤 경우에서든지 안정적인 첫 번째 sql문이 더 안정적이라고 볼 수 있다. 코드의 구성은 좀 복잡해지겠지만 위의 코드가 훨씬 더 좋은 코드라고 볼 수 있다.

 

 

4-1. 계좌이체 기능 TestUnit Class 작성하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package test;
 
import java.sql.SQLException;
import model.AccountDAO;
import model.AccountNotFoundException;
import model.InsufficientBalanceException;
import model.NoMoneyException;
import model.NotMatchedPasswordException;
 
//step7. 계좌이체 테스트 
public class TestUnit7 {
    public static void main(String[] args) {
        try {
            AccountDAO dao=new AccountDAO();
            String senderAccountNo="1";
            String password="1234";
            int money=100;
            String receiverAccountNo="2";
            System.out.println("이체전 송금자 계좌잔액:"+dao.findBalanceByAccountNo(senderAccountNo, password));
            System.out.println("이체전 수금자 계좌잔액:"+dao.findBalanceByAccountNo(receiverAccountNo, "1"));
            dao.transfer(senderAccountNo,password,money,receiverAccountNo);
            System.out.println(money+"원 계좌이체완료");
            System.out.println("이체후 송금자 계좌잔액:"+dao.findBalanceByAccountNo(senderAccountNo, password));
            System.out.println("이체후 수금자 계좌잔액:"+dao.findBalanceByAccountNo(receiverAccountNo, "1"));
        }catch(NoMoneyException e) {    
            System.out.println(e.getMessage());              // 이체액은 0원을 초과해야 합니다 
        }catch(AccountNotFoundException e) { 
            System.out.println(e.getMessage());                 //계좌번호에 해당하는 계좌가 존재하지 않습니다 or 이체받을 계좌가 존재하지 않습니다
        }catch(NotMatchedPasswordException e) {
            System.out.println(e.getMessage());               //계좌의 패스워드가 일치하지 않습니다
        }catch(InsufficientBalanceException e) {
            System.out.println(e.getMessage());                 //잔액 부족으로 이체할 수 없습니다
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
 
 

 

 위의 TestUnit은 요구사항 9-11에 해당하는 기능을 테스트하기 위한 클래스이다. 여러 가지 경우로 테스트가 가능하겠지만  findBalanceByAccountNo는 여러가지 테스트케이스로 검증된 함수이면서 transfer() 함수가 정상적으로 동작을 하는지 확인하기 위한 테스트용으로서 확인하기 위한 코드이다.  바로 transfer() 함수를 살펴보도록 하자. 

 

 

4-2. 계좌이체 기능 transfer() 구현하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
     * 계좌이체메서드 
     * 
     * 이체액이 0원 초과인지를 확인 
     * 수금자 계좌번호 확인 
     * 송금자 계좌번호와 비밀번호 확인 
     * 송금자 계좌잔액을 확인 ( 이체액보다 잔액이 작으면 예외발생,전파 ) 
     * 
     * con.setAutoCommit(false)
     * 
     * 송금자 계좌 출금작업 
     * 수금자 계좌 입금작업 
     * 
     * commit() -> try의 가장 마지막 부분
     * rollback() -> catch 
     * 
     * @param senderAccountNo
     * @param password
     * @param money
     * @param receiverAccountNo
     * @throws NoMoneyException 
     * @throws AccountNotFoundException 
     * @throws SQLException 
     * @throws NotMatchedPasswordException 
     * @throws InsufficientBalanceException 
     */
    public void transfer(String senderAccountNo, String password, int money, String receiverAccountNo) 
throws NoMoneyException, AccountNotFoundException, SQLException, NotMatchedPasswordException, InsufficientBalanceException {
        if(money<=0)
            throw new NoMoneyException("이체액은 0원을 초과해야 합니다");
        if(existsAccountNo(receiverAccountNo)==false)
            throw new AccountNotFoundException("이체받을 계좌가 존재하지 않습니다");
        int balance=findBalanceByAccountNo(senderAccountNo, password);
        if(balance<money)
            throw new InsufficientBalanceException("잔액 부족으로 이체할 수 없습니다");
        Connection con=null;
        PreparedStatement pstmt=null;
        try {
            con=getConnection();
            // 수동커밋모드로 설정 -> 트랜잭션 제어를 위해 
            con.setAutoCommit(false);
            // 이체하는 사람의 계좌에서 출금 
            String withdrawSql="update account set balance=balance-? where account_no=?";
            pstmt=con.prepareStatement(withdrawSql);
            pstmt.setInt(1, money);
            pstmt.setString(2, senderAccountNo);
            pstmt.executeUpdate();
            pstmt.close();
            // 이체받는 사람의 계좌에 입금 
            String depositSql="update account set balance=balance+? where account_no=?";
            pstmt=con.prepareStatement(depositSql);
            pstmt.setInt(1, money);
            pstmt.setString(2, receiverAccountNo);
            pstmt.executeUpdate();
            con.commit();//출금,입금 모든 작업이 정상적으로 처리되면 실제 db에 반영한다 
        }catch(Exception e) {
            con.rollback();//문제가 발생하면 작업을 취소하고 원상태로 되돌린다 
            throw e;
        }finally {
            closeAll(pstmt, con);
        }
    }
 
 

 

 위의 코드에서 예외처리를 해주는 부분은 단지 요구사항에 맞게 해당 조건을 만족하지 못하면 예외를 throw 해주었다. 이체액은 0원을 초과해야 할 뿐만 아니라 이체하려고 하는 금액 이상으로 계좌에 금액이 존재해야 한다. 송신자의 계좌번호의 존재 유무는 잔액을 확인하면서 동시에 계좌가 존재하는지 확인할 수 있는 findBalanceByAccountNo()를 호출해주었다. 또한 이체받을 계좌가 존재해야 하며 이는 앞에서 기능 분할에서 정의해 놓은 existsAccountNo() 함수를 호출해 주었다. 이 메서드에서 가장 핵심적인 부분이라고 한다면  Line 41인 con.setAutoCommit(false) : 수동 커밋 모드로 설정하는 부분이다. 계좌에서 금액을 출금하는 영역과 계좌에서 금액을 입금하는 작업은 하나의 세트 단위로 작업이 진행되어야 한다. 이 두 개의 작업이 모두 정상적으로 완료되었을 경우에 db에 반영을 해야 하기 때문에 AutoCommit모드를 false로 해준 것이다. 그렇지 않은 경우에 송신자의 계좌에서 출금을 완료하고 수신자의 계좌에서 금액을 추가시키는 과정에서 예외가 발생하게 된다면 이미 db에 반영이 됐을 가능성이 높다. 따라서 rollback()으로 처리 가능한 AutoCommit 모드를 꺼준 것이다. 마지막으로 해당 메서드까지 구현을 하면서 느끼는 거지만 메서드를 기능 단위별로 분할할수록 이를 활용할 가능성이 많아지고 이는 곧 코드의 중복을 많이 줄여준 것 같은 느낌을 받는다. 

 

 

5-1. 최고 잔액을 가진 계좌 리스트를 조회

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package test;
 
import java.sql.SQLException;
import java.util.ArrayList;
import model.AccountDAO;
import model.AccountVO;
 
//step8 최고 잔액을 가진 계좌 리스트를 조회 ( subquery ) 
public class TestUnit8 {
    public static void main(String[] args) {
        try {
            AccountDAO dao=new AccountDAO();
            ArrayList<AccountVO> list=dao.findHighestBalanceAccount();
            for(AccountVO vo:list)
                System.out.println(vo.getAccountNo()+" "+vo.getName()+" "+vo.getBalance());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
 
 

 

이 기능은 단순히 서브 쿼리를 적용해 보는 간단한 기능이다. 최고잔액을 가진 계좌정보를 불러와서 단순히 출력하는 예제이며 최고잔액을 가진 계좌가 여러 개 있을 가능성이 있으므로 ArrayList로 정보를 받아왔다. 함수의 구현 부분을 살펴보자.

 

 

5-2. 최고 잔액 리스트 조회 구현 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public ArrayList<AccountVO> findHighestBalanceAccount() throws SQLException {
    ArrayList<AccountVO> list=new ArrayList<AccountVO>();
    Connection con=null;
    PreparedStatement pstmt=null;
    ResultSet rs=null;
    
    try {
        con=getConnection();
        
        StringBuilder sql=new StringBuilder("select account_no,name,balance ");
        sql.append("from account ");
        sql.append("where balance=(select max(balance) from account)");
        pstmt=con.prepareStatement(sql.toString());
        rs=pstmt.executeQuery();
        
        
        while(rs.next()) {
            //list.add(new AccountVO(rs.getString(1),rs.getInt(3),rs.getString(2)));
            //아래와 같이 컬럼명을 이용해도 된다 
            //list.add(new AccountVO(rs.getString("account_no"),rs.getInt("balance"),rs.getString("name")));
            //4개 매개변수 생성자 이용해도 됨 : password에 null을 할당 
                list.add(new AccountVO(rs.getString(1),rs.getString(2),null,rs.getInt(3)));
            }
        }finally {
            closeAll(rs, pstmt, con);
        }
        return list;
    }    
}
 
 

 

일단 서브 쿼리인 제일 안쪽 쿼리문을 살펴보자. 

 

  1. 여러 계좌 중 최고 잔액을 찾아야 하기 때문에 "select max(balance) from account" 로 구성해주었다.
  2. 최고 잔액을 가진 계좌정보를 조회 "select * from account where balance=(select max(balance) from account"

위와 같이 구성을 해주었고 최고 잔액을 가진 계좌가 하나의 계좌가 아닌 여러 계좌에서 가질 수 있으므로 ArrayList에 담아서 반환을 해주었다. 

 

 

 

 

반응형