데이터베이스

DB 모듈화

Leo.K 2022. 4. 22. 11:26

오늘은 데이터베이스를 모듈화해서 JDBC를 사용해 자바로 핸들링하는 실습을 진행해보겠습니다.

1. 프로그램 구조

모듈화하고자 하는 프로그램의 구조는 다음과 같습니다. 

2. 프로그램 실행 과정

전체 프로그램의 대략적인 개요는 다음과 같은 순서로 실행됩니다.

다음의 순서는 조회를 하는 예시입니다.

1. 사용자가 DB에 있는 데이터에 대해서 CRUD 명령을 요청합니다.(누구에게? DAO에게)

ㄴ> 호출하는 방법 -> DeptDao.getInstance() [Dao의 객체를 생성 or 호출합니다.]

ㄴ> 싱글톤 패턴을 사용했기때문에 가장 처음 호출에만 객체을 생성하고, 그 이후에는 이전에 만들어 둔 객체를 반환합니다.   

2. 요청을 받은 DAO는 DB에 연결을 하기 위해서 연결 요청을 합니다. (누구에게?DBService에게)

ㄴ> 연결이 되면 DAO는 DB에 접근해서 사용자가 요청한 데이터를 조회합니다. (insert, update, delete 가능합니다.)

ㄴ> preparedstatement객체와 resultSet객체를 사용해 테이블을 행 단위로 읽어옵니다. 

ㄴ> 읽어와서 읽어온 DeptVo타입으로 포장합니다. 

ㄴ> 포장한 데이터를 ArrayList에 저장한 후에, 모든 명령을 마치면 list를 반환합니다. 

3. 다시 사용자 

ㄴ> DAO에게서 전달받은 list를 개선 루프를 사용해 출력합니다. 

전체 소스코드를 보면서 기능을 분석하기 전에 간단하게 각 모듈에 대한 특징을 짚어보겠습니다. 

 

3. 각 모듈의 기능

1) DeptVo(dao가 읽은 데이터를 저장합니다.)

:  테이블에서 조회한 데이터를 클래스의 타입으로 포장해서 저장합니다.

Vo(Value Object) == (DTO, Data Transfer Object)

: 값(레코드)을 저장 관리하기 위한 클래스

: 하나의 Vo 객체가 하나의 레코드(행)를 관리합니다. 

: DeptVo자료형을 가지는 Vo의 개수는 전체 레코드(행)의 수 입니다.

: 관리해야할 레코드의 수는 가변적입니다. (삽입, 삭제 연산으로 인해 레코드의 수가 변할 수 있습니다.)

: 각각의 헤딩명이 설정된 private변수와 리턴 타입이 설정되어 값을 리턴할 수 있는 게터/세터 메서드를 생성해줍니다.

: 게터/세터메서드를 통해 DAO파일에서 VO로 간접적으로 접근하여 값을 저장 및 관리할 수 있습니다.

 

2) DeptDao(싱글톤 패턴으로 구성)

Dao(Data Access Object)

: DB와 연결이 되면 자바 객체와 DB를 왔다 갔다하면서 사용자가 요청한 명령을 처리하는 데이터 관리 클래스입니다.

: CRUD명령을 담당해서 실행하는 클래스

기능          : Create/Read/Update/Delete  

구현명령    : insert/select/update/delete  

: 싱글톤 패턴을 사용합니다.

 

3) DBService(연결을 관리해주는 모듈)

: DAO가 연결을 요청하면 DB와 연결을 도와주는 클래스입니다. (작업 요청(select, insert, delete, update)이 있을 때마다    매번 연결요청을 합니다.)

: 싱글톤 패턴을 사용합니다.

4. 싱글톤 패턴

싱글톤이 뭐야?  + 템플릿 만들기

본격적으로 소스코드를 분석하기 전에 싱글톤의 개념을 잡고 시작하겠습니다.

싱글톤 패턴이란, 프로그램이 시작될 때 어떤 클래스가 최초로 한 번만 메모리를 static으로 할당하고 그 메모리에 인스턴스를 만들어서 사용하는 디자인 패턴입니다. 결론적으로 단 1개의 인스턴스(=객체)만 생성이 됩니다.

장점

  • 고정된 메모리 영역을 얻으면서 한 번의 new 키워드로 객체를 사용하기 때문에 메모리 낭비를 방지할 수 있습니다.
  • 싱글톤으로 만들어진 클래스의 객체는 전역이기 때문에 다른 클래스의 객체들이 쉽게 공유할 수 있습니다. 
  • 객체가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 경우 사용합니다. 
  • 두 번째 이용시 부터는 객체를 로딩하는 시간이 줄어들어 성능이 좋아집니다. 
  • 공통된 객체를 여러개 생성해서 사용해야 하는 상황에서 많이 사용됩니다

단점

  • 하나뿐인 인스턴스가 너무 많은 일을 하게 되는 경우 다른 클래스의 객체들 간에 결합도가 높아져 객체지향 설계 방법론인 "개방-폐쇄 원칙"을 위배하게 됩니다. [다른 클래스에서 싱글톤 객체를 쉽게 공유할 수 있기 때문에 싱글톤 객체의 역할이 너무 많아지면 클래스의 객체들간의 결합도가 높아집니다.]  
  • 이로 인해 수정이 어려워지고, 유지보수의 비용이 증가합니다. 
  • 싱글톤을 만들 때는 동시성 문제를 고려 해야하는데, 멀티쓰레드 환경에서 동기화 처리를 하지 않으면 2개의 인스턴스가 생성될 수 있는 가능성이 있습니다. 
1
2
3
4
5
6
7
8
9
10
11
static DBService single = null;
 
public static DBService getInstance() {
 
    //객체가 생성되어 있지 않으면 만들어라.
    if (single == null) {
        single = new DBService();
    }
    //이전에 만들어 놨던 객체를 그대로 반환한다.
    return single;
}
cs

싱글톤의 개념을 우리가 만들고자 하는 프로그램에 적용시켜보겠습니다. 

사용자로부터 요청을 받은 DAO는 명령어의 처리가 필요할 때마다 DBService 클래스에게 연결 요청을 합니다. 이 말은 요청이 들어올 때 마다 객체를 생성해서 연결을 시도해줘야 한다는 뜻입니다.[이것이 DBService의 주요 기능]

위에서 본 것처럼 공통된 객체를 여러개 생성해서 사용해야 하는 경우 싱글톤을 사용해서 단 하나의 객체만 만든다고 했습니다. 

처음에 초기화 하지 않은 객체를 선언만 해주고, 메소드가 호출이 되면[=DAO로부터 연결요청이 들어오면] 메소드를 실행하는데, 가장 초기에만 null값이 있는 상태로 객체를 한 번 생성합니다.[5-7번 라인] 

그 이후의 호출에 대해서는 고정된 메모리 영역에 생성되어 있는 객체를 반환만 해줍니다.

+ 소스 코드 템플릿 만드는 법

템플릿은 같은 워크스페이스에서만 사용가능합니다. 템플릿은 워크스페이스 폴더안에 .metadata폴더에 저장됩니다.

다른 워크스페이스에서 사용하려면 export후 사용하고자 하는 워크스페이스에 템플릿을 복사하고 import하면 됩니다.

  1. 템플릿으로 만들고자 하는 소스코드를 복사합니다. 
  2. window -> preference로 접속합니다. 
  3. Java -> Edit -> templates을 검색합니다. 
  4. new를 눌러 새로운 템플릿을 생성합니다.

   5. 템플릿의 이름을 설정하고, pattern영역에 복사한 소스를 붙여넣습니다. 이때, 템플릿을 적용하는 클래스명이 모두        다르기 때문에, 자동으로 변환되도록 클래스명을 변수로 대체해줘야 합니다. 

 

 

5. 모듈 소스 코드 분석

1) DBService

소스코드를 보면서 각 모듈의 상세 기능 살펴보기

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
package service;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
 
public class DBService {
    
    //Oracle Driver Loading
    static {
 
        
        try {
            Class.forName("oracle.jdbc.driver.OracleDriver");
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    //single-ton : 객체 1개만 생성해서 사용하자
    static DBService single = null;
 
    public static DBService getInstance() {
 
        //객체가 생성되어 있지 않으면 만들어라.
        if (single == null) {
            single = new DBService();
        }
        //이전에 만들어 놨던 객체를 그대로 반환한다.
        return single;
    }
 
    //외부에서 생성하지 못하도록 접근제한. 객체는 getInstance메소드를 통해서만 생성가능.
    private DBService() {
        // TODO Auto-generated constructor stub
    }
    
    public Connection getConnection() throws SQLException {
        Connection conn = null;
        
        String url = "jdbc:oracle:thin:@localhost:1521:xe";
        String user= "test";
        String pwd = "test";
        
        conn = DriverManager.getConnection(url, user, pwd);
        
        return conn;
    }
}
 
cs
 


1. 가장 처음으로 할 작업은, DB와의 소통을 위해서 jdbc드라이버 파일을 현재 작업중인 프로젝트로 가져와야 합니다.

드라이버 파일을 추가하는 방법은 여기를 참고해주세요. 

ㄴ> 초기에 jdbc 드라이버 위치를 프로젝트에 build path를 통해서 지정해주게 되면, 같은 프로젝트 내에서 전역적으로 사용되기 때문에 다른 파일에서는 추가적인 로딩 작업 없이 사용할 수 있습니다. 드라이버가 어느 곳에 저장되있는지는 중요하지 않고 build path를 통해 현재 저장되어 있는 위치만 정확하게 명시해주면 됩니다.

Class.forName("oracle.jdbc.driver.OracleDriver");

ㄴ> 명령문을 통해서 현재 드라이버가 저장된 경로를 클래스 영역에 로딩해줍니다.

2. 싱글톤 패턴을 사용하여 객체를 생성하는 메소드를 만듭니다. 

ㄴ> 이에 대한 설명은 싱글톤 부분에서 했기에 넘어가겠습니다.

ㄴ> 싱글톤 패턴을 사용하여 객체를 생성하는 방법을 getInstance() 메서드로만 국한했기 때문에, 클래스의 생성자는 private으로 설정해서 외부에서 접근할 수 없도록 해주어야 합니다.

3. 이 클래스의 주된 기능인 연결관리 객체를 만드는 getConnection() 메서드입니다. 

ㄴ> conn = DriverManager.getConnection(url, user, pwd);

ㄴ> 접속을 원하는 계정의 정보와 url을 담아서 연결을 시도합니다. 

ㄴ> 단, 이때 주의할 점은 위의 명령에서 (연결이 안 되면 어떡할래?)SQLException이 발생하는데, 클래스 내부에서 예외를 처리해버리면 클래스에게 연결을 요청한 DAO는 에러 메세지를 받지 못한 상태로 연결이 된 줄 알고, 한 참뒤에 실행해보고 나서야 에러를 발견할 수 있게 됩니다. 따라서 예외를 직접 처리하지 않고 메서드를 호출한 DAO에게 "throws"을 통해 예외 처리를 위임 시켜서 연결이 되지 않음을 알려야 합니다.  

 

2) DAO

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
//전체 조회 
public List<DeptVo> selectList(){
 
    List<DeptVo> list = new ArrayList<DeptVo>();
    Connection           conn  = null;
    PreparedStatement pstmt = null;
    ResultSet          rs      = null;
    String               sql   = "select * from dept";
    
    try {
        //1. connection 얻어오기
        conn = DBService.getInstance().getConnection();
        
        //2. PreparedStatement 얻어오기
        pstmt = conn.prepareStatement(sql);
        
        //3. ResultSet 얻어오기 
        rs = pstmt.executeQuery();
        
        //4. 포장
        while(rs.next()) {
            //rs가 가리키는 행(레코드)의 값을 읽어 온다.
            int deptno   = rs.getInt("deptno");
            String dname = rs.getString("dname");
            String loc   = rs.getString("loc");
            
            //Vo로 포장(반복을 1회 돌아서 새로운 데이터를 읽을 때마다 이 레코드를 저장할 vo를 만들어서 포장해 한다.)
            DeptVo vo = new DeptVo();
            vo.setDeptno(deptno);
            vo.setDname(dname);
            vo.setLoc(loc);
            
            //list에 추가 
            list.add(vo);
            
        }
        
        
    } catch (Exception e) {
        // TODO: handle exception
        e.printStackTrace();//
    } finally {//반드시 실행하는 구문
        
        try {
            
            //연결되어 있는 상태면 끊어라.(생성 역순으로)
            
            if(rs != null) rs.close();         //3
            if(pstmt != null) pstmt.close();//2
            if(conn != null) conn.close();//1
            
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    
    return list;
}
cs

1) conn = DBService.getInstance().getConnection();

ㄴ> DBService에 정의된 메서드를 호출하여 연결을 요청합니다. 

2) pstmt = conn.prepareStatement(sql);

ㄴ> PreparedStatement, 명령어를 처리해주는 객체를 생성합니다. 

3) rs = pstmt.executeQuery();

ㄴ> 처리된 명령어를 출력하는 객체를 생성합니다. (select를 제외한 다른 명령은 executeUpdate()메서드를 사용합니다.)

4) 4번의 포장과정이 가장 중요합니다. 데이터를 읽어오는 방식과 포장이 되는 방식, 마지막으로 List에 추가되는 방식을 그림으로 그리면서 명확히 이해해야 합니다.  정확한 과정에 대한 설명은 여기를 확인해주세요. 

- rs의 커서가 가리키는 위치의 레코드를 읽어서 Vo타입으로 포장합니다. 

- Vo로 포장된 데이터를 그림과 같이 리스트에 삽입합니다.

5) 마지막으로 관련된 명령을 모두 처리했을 경우 반드시 객체를 close메서드로 반납해줘야 합니다. 

ㄴ> 객체를 생성한 역순서로 진행합니다. 

3) VO

https://colorscripter.com/
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 class DeptVo {
    
    //이름을 맞춰주어라. 이름을 다르게 만들면 나중에 다시 맞춰줘야 한다. -> 작업량 多 -> 야근;;
    private int    deptno;
    private String dname;
    private String loc;
    
    public int getDeptno() {
        return deptno;
    }
    public void setDeptno(int deptno) {
        this.deptno = deptno;
    }
    public String getDname() {
        return dname;
    }
    public void setDname(String dname) {
        this.dname = dname;
    }
    public String getLoc() {
        return loc;
    }
    public void setLoc(String loc) {
        this.loc = loc;
    }
    
    
}
 
cs

Vo의 코드는 어렵지 않아서 직관적으로 이해가 되실겁니다. 

한 가지만 짚고 넘어가자면, 위에서 설명한 바와 같이 읽어들인 데이터를 Vo타입으로 포장한다고 했는데, 쉽게 말하면 읽어 들인 데이터를 Vo클래스 내부에 있는 변수에 Set하여 저장해주는 겁니다. 이렇게 데이터를 포장하여 저장하는 이유는 읽어들인 데이터를 바로 사용하지 않고, 저장해두었다가 필요할 때 꺼내 쓰기 위함입니다.


DB프로그램에서 다음 세개의 변수명을 똑같이 맞춰주면 아주 편합니다. 별도의 매칭 작업없이 이름이 같은 변수에 넣을 수 있기 때문입니다.  변수명을 통일하지 않으면 나중에 일일히 다 찾아서 매칭해야 할지도,,,, 
DB column명 == Vo의 property(속성)명 == 파라미터 명(HTML입력폼)

4) 사용자

1
2
3
4
5
6
7
List<DeptVo> list = DeptDao.getInstance().selectList(); 
//selectList메서드의 내부 실행 과정을 이해하라. (연결, 조회, 포장, 전달)
 
//개선루프
System.out.println("---DeptDao가 가져다준 부서목록");
for(DeptVo vo : list) {
    System.out.printf("[%d-%s-%s]\n", vo.getDeptno(), vo.getDname(), vo.getLoc());
}
cs

사용자는 위와 같이 메서드를 호출함으로써 결과를 list형태로 받고 출력할 수 있습니다. 

 

다음시간에 성적관리 폼을 이용한 DB를 구현해서 자바 프로그램과 연동해보도록 하겠습니다.

'데이터베이스' 카테고리의 다른 글

Tomcat다운로드&환경설정_HTML_국비_DAY40  (0) 2022.04.26
JDBC 실습_국비_DAY39  (0) 2022.04.22
JDBC의 사용법  (0) 2022.04.21
Join  (0) 2022.04.20
ERD(Entity_Relation_Diagram)  (0) 2022.04.20