JAVA 웹개발 과정 국비 23일차 정리

23일차

※ 배우는 과정이므로 정확하지 않을 수 있습니다.
잘못된 점은 알려주시면 수정하도록 하겠습니다!


1. 메뉴 관리 시스템 리팩토링

메뉴 관리 시스템을 JDBC를 이용하는 MVC 패턴으로 리팩토링 해보자.

1) DAO로 옮기기

DAO(Data Access Object)란?
이전 Java 실습 때 맛보기로 MVC 패턴을 연습했을 때의 Manager 클래스
데이터만 담당하는 클래스이다.
Main과 DBMS의 중개자 역할(DAO Layer)을 한다.

데이터에 관련된 코드를 CafeMenuDAO 클래스에 옮긴다.
우선 insert 메소드를 만들어보자.

// 수정 전
// Main Class
Connection con = DriverManager.getConnection(url,userName,passWord);
Statement stat = con.createStatement();
String sql = "INSERT INTO cafe_menu VALUES(cafe_menu_seq.NEXTVAL,'"+name+"',"+price+")";
int result = stat.executeUpdate(sql);
System.out.println("입력 성공!");
con.commit();
con.close();

getConnection 메소드를 옮겨왔기 때문에 그에 맞는 변수 url, userName, passWord도 함께 옮겨오고, 드라이버 생성 코드도 마찬가지로 메소드 안으로 같이 옮겨온다.
sql 쿼리문 안 name, price를 파라미터로 받고, int형 변수 result를 리턴하는 insert 메소드를 완성한다.

2) 예외 전가

그러나 main에서와 같이 예외가 생길 수 있는 위험이 아직 존재하는데, CafeMenuDAO 클래스에서 예외를 처리해줄 필요가 없으므로 throws Exception을 해서 Main으로 예외를 전가해버리고, Main에서 try-catch문의 catch문으로 예외를 잡아버리면 된다.

Callee(Func)의 예외를 Caller(Main)으로 전가할 수 있다.
Main에서 동일하게 예외를 전가해버리면 ‘처리할 의지가 없다’는 것으로 간주되기 때문에 Main에서는 예외전가보다는 try-catch문을 이용한 예외 처리를 이용하자.

// 수정 후
// CafeMenuDAO Class
public int insert(String name, int price) throws Exception {
    Class.forName("oracle.jdbc.driver.OracleDriver");
    String url = "jdbc:oracle:thin:@localhost:1521:xe";
    String userName = "kh";
    String passWord = "kh";
    Connection con = DriverManager.getConnection(url,userName,passWord);
    
    Statement stat = con.createStatement();
    String sql = "INSERT INTO cafe_menu VALUES(cafe_menu_seq.NEXTVAL,'"+name+"',"+price+")";
    int result = stat.executeUpdate(sql);
    con.commit();
    con.close();
    return result;

insert 메소드를 만들었기 때문에 Main에 dao를 호출해서 insert 기능을 적용해주면 된다.

// Main Class
CafeMenuDAO dao = new CafeMenuDAO();

if(menu.equals("1")) {
	System.out.print("메뉴 이름 : ");
	String name = sc.nextLine();
	System.out.print("메뉴 가격 : ");
	int price = Integer.parseInt(sc.nextLine());
	
	int result = dao.insert(name, price);
	if(result>0) {
		System.out.println("입력 완료.");
	}
}

3) Extract Method

그룹으로 묶을 수 있는 코드가 있으면 코드의 목적이 잘 드러나도록 메소드의 이름을 지어 별도의 메소드로 뽑아내는 것.

insert 기능과 비슷한 update/delete 기능도 동일하게 만들 수 있다.
그런데, 드라이버 인스턴스 생성부터 getConnection 메소드까지는 기능을 만들 때마다 동일하게 작용하기 때문에 계속 코드가 반복이 된다.
이때 Extract 메소드를 만들어주는 것이다.

private Connection getConnection() throws Exception {
	Class.forName("oracle.jdbc.driver.OracleDriver");
	String url = "jdbc:oracle:thin:@localhost:1521:xe";
	String userName = "kh";
	String passWord = "kh";
	Connection con = DriverManager.getConnection(url,userName,passWord);
	return con;
}

4) insert/update/delete 메소드 생성

위의 단계들을 거쳐 최종적으로 메소드를 만들어주면 아래와 같이 리팩토링 할 수 있다.

// CafeMenuDAO Class

// insert 메소드
public int insert(String name, int price) throws Exception {
	Connection con = getConnection();
	Statement stat = con.createStatement();
	String sql = "INSERT INTO cafe_menu VALUES(cafe_menu_seq.NEXTVAL,'"+name+"',"+price+")";
	int result = stat.executeUpdate(sql);
	con.commit();
	con.close();
	return result;
}

// update 메소드
public int update(String name, int price, int id) throws Exception {
	Connection con = getConnection();
	Statement stat = con.createStatement();
	String sql = "UPDATE cafe_menu SET name='"+name+"', price="+price+" WHERE id="+id;
	int result = stat.executeUpdate(sql);
	con.commit();
	con.close();
	return result;
}

// delete 메소드
public int delete(int id) throws Exception {
	Connection con = getConnection();
	Statement stat = con.createStatement();
	String sql = "DELETE FROM cafe_menu WHERE id=" + id;
	int result = stat.executeUpdate(sql);
	con.commit();
	con.close();	
	return result;
}
// Main Class
if(menu.equals("1")) {
	System.out.print("메뉴 이름 : ");
	String name = sc.nextLine();
	System.out.print("메뉴 가격 : ");
	int price = Integer.parseInt(sc.nextLine());
	int result = dao.insert(name, price);
	if(result>0) {
	System.out.println("입력 완료.");
	}
}else if(menu.equals("3")) {
	System.out.print("수정할 메뉴 ID : ");
	int id = Integer.parseInt(sc.nextLine());
	System.out.print("수정할 메뉴 이름 : ");
	String name = sc.nextLine();
	System.out.print("수정할 메뉴 가격 : ");
	int price = Integer.parseInt(sc.nextLine());
	int result = dao.update(name, price, id);
	if(result>0) {
	System.out.println("수정 완료.");
	}
}else if(menu.equals("4")) {
	System.out.print("삭제할 메뉴 ID : ");
	int id = Integer.parseInt(sc.nextLine());
	int result = dao.delete(id);
	if(result>0) {
	System.out.println("삭제 완료.");
	}
}

5) select 메소드 생성

select 메소드는 위의 입력/수정/삭제와는 다른 방향으로 메소드를 만들어야한다.

① 메소드의 리턴 타입은?

executeUpdate 메소드와는 달리 executeQuery 메소드를 쓰기 때문에, 리턴 값이 int가 아니라 ResultSet이다.
ResultSet 은 자바에 있는 데이터가 아닌 DB에 있는 내용을 가리키는 포인터.
커넥션이 close 되어버리면 데이터를 자바로 가져올 수 없게 된다.

그렇다면 커넥션을 밖에서 close하면 어떨까?
Connection 변수 con은 지역변수이기 때문에 메소드 밖에 나가게 되면 아무 소용이 없어진다.
따라서 ResultSet 대신 모든 데이터를 밖(자바)으로 꺼내오면 된다!

SELECT로 나온 데이터들을 하나씩 꺼내온다고 가정하면 id, name, price, id, name, price … 이런식으로 꺼내오게 될 것이다.
자료형도 int, String… 여러개가 있고, 총 몇개가 있는지도 모른다.
이럴 때는 ArrayList로 데이터를 받아주자.

그렇다면 비슷한 특성을 가진 배열로도 가능할까?
그러나 사이즈가 정해져 있는 배열은 데이터가 몇개 있는지 모르는 ResultSet의 데이터를 받기는 부적합하다.

그럼 ArrayList를 사용하는 것은 확실해졌다. 가져오는 데이터들을 다시 봐보면 일정한 규칙이 있는 것을 알 수 있다.
id, name, price / id, name, price … 여기서 id, name, price를 합치면 무엇이 될까?
바로 Menu의 인스턴스가 되는 것이다.
따라서 select 메소드의 리턴 타입은 ArrayList<Menu>임을 알 수 있다.

Menu 인스턴스들을 받을 수 있는 ArrayList<Menu> 인스턴스를 result 변수에 담아준다.

public ArrayList<Menu> selectAll() throws Exception {
	ArrayList<Menu> result = new ArrayList<>();
	Connection con = getConnection();
	Statement stat = con.createStatement();
	String sql = "SELECT * FROM cafe_menu ORDER BY 1";
	ResultSet rs = stat.executeQuery(sql);

② 값 하나씩 꺼내기

ResultSet은 만들어진 결과에 가장 첫번째(헤더부분)의 위치를 가리키는 포인터 역할을 한다.
ResultSet의 변수를 rs라 할 때, 이 결과를 프로그램으로 가져오고 싶다면 rs에게 물어봐야 한다. 내rs야 내가 가져올 수 있는 다음줄이 있니?

이렇게 물어볼 수 있는 메소드가 바로 next메소드이다. 리턴 값은 boolean형으로, 현재 헤더부분을 가리키지만 다음줄이 있는지를 물어보면 가리키는 부분이 아래로 내려가고, 그 자리에 데이터가 있으면 거기를 가리키면서 true를 리턴하고, 없으면 false를 리턴한다.
false를 가져왔을지도 모르는데 바로 id를 가져와라, name을 가져와라 할 수 없다. 그렇게 하면 에러가 나기 때문이다.
그래서 next 메소드는 보통 if문과 함께 쓴다.

if문 안쪽에서는 rs에게 데이터를 꺼내달라고 할 수 있다. 그러나 바로 한 줄을 꺼낼 수는 없고, 가리키는 데이터 행 중에 하나만 꺼낼 수 있는데, 꺼내고 싶은 데이터의 자바 데이터타입을 먼저 꺼내야한다. 파라미터는 컬럼이름을 쓴다. 컬럼명 대신 열 변호를 써줘도 괜찮다.

if(rs.next()) {
	int id = rs.getInt("id"); // 같은 의미 = rs.getInt(1);
	String name = rs.getString("name");
	int price = rs.getInt("price");
	System.out.println(id + " : " + name + " : " + price);
}

그리고 다음 줄도 있다면 반복해서 if문을 써주면 되는데, 계속 if문을 쓰는 것은 효율적이지 못하다. 따라서 while문을 써주면 true를 리턴한다면 계속 값을 받아오다가, false가 되면 더이상 값을 받아오지 않게된다.

while(rs.next()) {
	int id = rs.getInt("id"); // 같은 의미 = rs.getInt(1);
	String name = rs.getString("name");
	int price = rs.getInt("price");
}

③ Menu 변수에 인스턴스 담기

rs에게 데이터를 꺼내달라고 요청하고 나면, 그 데이터들을 모아 Menu 인스턴스를 만들 수 있다는 것을 알게되었다.
따라서 이 인스턴스들을 ArrayList<Menu>형 result에 add 시켜주면, ArrayList 에 Menu 인스턴스들이 while문을 돌면서 차곡차곡 저장될 것이다.

while(rs.next()) {
	int id = rs.getInt("id"); // 같은 의미 = rs.getInt(1);
	String name = rs.getString("name");
	int price = rs.getInt("price");
	
	Menu m = new Menu(id,name,price);
	result.add(m);
}

④ 연결 close

while문으로 값을 받아오는 것을 끝냈다면, con.close()로 연결을 끊어준다.
SELECT 쿼리문이기 때문에 COMMIT은 필요하지 않다.

con.close();

⑤ 최종 select 메소드 소스코드

위의 순서대로 코드를 작성하면 아래와 같이 완전한 select 메소드를 만들 수 있다.

// CafeMenuDAO Class
public ArrayList<Menu> selectAll() throws Exception {
	Connection con = getConnection();
	Statement stat = con.createStatement();
	String sql = "SELECT * FROM cafe_menu ORDER BY 1";
	ResultSet rs = stat.executeQuery(sql);
	
	ArrayList<Menu> result = new ArrayList<>();  // 1
	while(rs.next()) {  // 2
		int id = rs.getInt("id");
		String name = rs.getString("name");
		int price = rs.getInt("price");
		
		Menu m = new Menu(id,name,price);  // 3
		result.add(m);
	}
	con.close();  // 4
	return result;  // 1
}
// Main Class
else if(menu.equals("2")) {
	ArrayList<Menu> list = dao.selectAll();
	for(Menu m : list) { 
		System.out.println(m.getId() + "\t" + m.getName() + "\t" + m.getPrice());
	}
}

6) 예외 처리

위 2)번에서와 같이 CafeMenuDAO 클래스에 있는 모든 메소드들은 throws Exception으로 Main으로 예외를 전가시켰다.
Main에서는 더이상 예외를 전가할 수 없기 때문에 try-catch문으로 예외처리를 해주어야 한다.

if문(시스템 동작기능) 하나하나 try-catch문을 사용해도 괜찮지만, 똑같은 try-catch문을 사용할 것이기 때문에 if-else문 전체를 try문으로 감싸주고 아래 catch문을 사용하면, 똑같이 동작한다.

public static void main(String[] args) {
  CafeMenuDAO dao = new CafeMenuDAO();
	
  Scanner sc = new Scanner(System.in);
  while(true) {
    System.out.println("== 메뉴 관리 시스템 ==");
    System.out.println("1. 신규 메뉴 등록");
    System.out.println("2. 메뉴 목록 출력");
    System.out.println("3. 메뉴 정보 수정");
    System.out.println("4. 메뉴 정보 삭제");
    System.out.println("5. 시스템 종료");
    System.out.print(">> ");
    String menu = sc.nextLine();
        
    try {
      if(menu.equals("1")) { 
        // insert 메소드 사용
      }else if(menu.equals("2")) {
        // select 메소드 사용
      }else if(menu.equals("3")) {
        // update 메소드 사용
      }else if(menu.equals("4")) {
        // delete 메소드 사용
      }else if(menu.equals("5")) {
        // 시스템 종료
      }else {
        System.out.println("메뉴를 다시 확인하세요.");
      }
    }catch(Exception e) {
      e.printStackTrace();  // 개발자가 에러 확인 - 고객은 볼 수 없음
      System.out.println("요청하신 기능을 수행하는 중 오류가 발생했습니다.");
      System.out.println("같은 오류가 반복될 시, 관리자에게 문의해주세요.");
      System.out.println("E-mail : admin@company.com");
    }
  }
}

catch문에는 꼭 e.printStackTrace()를 써주어 어떤 오류가 나는지 개발단계에서 확인하도록 하자.
그리고 사용자에게 보여줄 오류 안내문을 출력하는 것으로 마무리한다.
이렇게 catch문을 만들면, 각 기능에서 예외가 나더라도 바깥 catch문으로 이동하여 고객에게는 안내문을, 개발자는 오류를 확인할 수 있게 된다.

댓글남기기