---
title: "Java 정리 37"
date: "2019-01-08"
tags: ["Java"]
status: archived
verified: false
---

---

## PreparedStatement를 사용한 CRUD

![01](/md/legacy-assets/java/37/01.webp)

* [소스코드](https://github.com/younggeun0/SSangYoung/tree/master/dev/workspace/sistJavaStudy2/src/date190107/preparedStatement)
* **DAO(Data Access Object)**
    * DB업무에 관련된 업무를 처리하는 클래스
* **사용하는 쪽은 업무에 관한 단어로 method명을 사용**

![02](/md/legacy-assets/java/37/02.webp)

* **DAO는 쿼리를 포함한 method명을 사용**

![02-2](/md/legacy-assets/java/37/02-2.webp)

* 드라이버로딩과 Connection얻기는 method로 한번만 수행
  * **DB쪽은 객체를 안만들고 얻고 쓰는 코드(싱글톤 패턴)가 주를 이룬다**
    * 객체 생성조차 속도에 영향을 미치기 때문에 이러한 패턴 사용

```java
private Connection getConn() throws SQLException {
     Connection con = null;
     
     // 1. 드라이버 로딩
     try {
           Class.forName("oracle.jdbc.OracleDriver");
     } catch (ClassNotFoundException cnfe) {
           cnfe.printStackTrace();
     }
     // 2. Connection 얻기
     String url = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
     String id = "scott";
     String pass = "tiger";
     
     con = DriverManager.getConnection(url, id, pass);
     
     return con;
}
```

* Oracle에서 타입이 DATE인 경우 **TO_CHAR를 사용해서 VARCHAR2 형태로 조회 후 getString으로 java로 읽어와서 사용**
  * **DB에서 필요한 처리는 다 하고 원하는 결과만 받아 사용하는게 가장 좋다.**

```java
String selectAllCpEmp2 = "SELECT empno, ename, "
		+ "TO_CHAR(hiredate, 'yyyy-mm-dd') hiredate, sal FROM cp_emp2";
```

* executeUpdate()의 결과는 int로 0~n이 반환, 즉 **반환값이 0만 아니면 정상수행된 것**

```java
int cnt = pstmt.executeUpdate();

if (cnt != 0) { delete나 update는 변경된 여러행이 반환(1~n)
     // 0이 아니면 정상수행 되었다는 의미
     flag = true;
}
```

* 값을 확인할 때 **toString 오버라이드** 한 후 찍어보는게 좋다.
  * VO 객체의 경우 기본적으로 toString으로 출력시 객체 주소가 찍힌다.

```java
public static void main(String[] args) {
UsePreparedStatementDAO u = new UsePreparedStatementDAO();
try {
     List<CpEmp2AllVO> list = u.selectAllCpEmp2();
     System.out.println(list);
} catch (SQLException e1) {
     e1.printStackTrace();
}
```

![03](/md/legacy-assets/java/37/03.webp)

```java
// toString을 Override하면 객체주소가 아닌 값이 찍힌다.
@Override
public String toString() {
     return "CpEmp2AllVO [empno=" + empno + ", sal=" + sal + ",  ename=" + ename + ", hiredate=" + hiredate + "]";
}
```

![04](/md/legacy-assets/java/37/04.webp)

* 오라클 Date 데이터형을 자바 Date 객체로 가져오면 연-일-월만 출력한다.
    * 시간까지 출력하고 싶다면 **SimpleDateFormat**을 이용해야 한다.
    * **Date는 가능하면 TO_CHAR를 사용해서 String형태로 받아오는게 좋다.**
      * SimpleDateFormat이란 객체를 하나 덜 만들어도 되므로

```java
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy EEEEE");
StringBuilder viewData = new StringBuilder();
viewData
.append("사원명 : ").append(cevo.getEname())
.append(", 연봉 : ").append(cevo.getSal())
.append(", 입사일 : ").append(sdf.format(cevo.getHiredate()));
```

![05](/md/legacy-assets/java/37/05.webp)


## Design Pattern

* GOF(Gang of Four)의 디자인 패턴이 유명
* **소프트웨어 설계에 있어 공통된 문제들에 대한 표준적 해법과 작명법**
  * 프로젝트를 분석하여 공통 코드를 몇가지로 만들어 낸 것


## Singleton Pattern

* **실행중인 JVM에서 단 하나의 객체를 만들어 사용하는 Pattern**
* 속도가 빠르다
* 메모리를 적게 사용한다.
* **객체가 소멸되면 그 이후에 해당 기능을 사용할 수 없다.**
    * 해결하는 코드 등장
* **작성법**

```java
public class Test {
    private static Test t; 
    // static 메소드에서는 인스턴스 변수를 직접 참조할 수 없다.

    // 1.객체 생성을 클래스 내부에서만 할 수 있도록 한다.
    private Test() {
    }

    // 2. 클래스 외부에서 객체를 얻어갈 수 있는 method를 작성
    public static Test getInstance() {
        if (t == null) {
            t = new Test();
        }
        return t;
    }        
}
```

```java
// 실행 중인 JVM에서 하나의 객체가 만들어지고, 하나의 객체만  사용되는 Singleton Pattern
public class Singleton {
     private static Singleton single; // static 메소드에선 인스턴스변수 사용불가
     
     // 1. class 외부에서 객체화를 하지 못하도록 막는다.
     private Singleton() {
           
     }
     
     public static Singleton getInstance() {
           // 객체가 생성되어 있지 않다면 객체를 생성하여  반환하고
           if (single == null) {
                single = new Singleton();
           }
           // 객체가 이미 생성되어 있다면 생성된 객체 반환
           return single;
     }
}
```

```java
public class UseSingleton {
     public static void main(String[] args) {
           // Singleton Pattern으로 만들어진 객체 사용.
           // 생성자가 private이므로 클래스가 외부에서  객체화가 되지 않는다.
           // Singleton s = new Singleton();
           
           Singleton s1 = Singleton.getInstance();
           Singleton s2 = Singleton.getInstance();
           Singleton s3 = Singleton.getInstance();
           Singleton s4 = Singleton.getInstance();
           
           System.out.println(s1);
           System.out.println(s2);
           System.out.println(s3);
           System.out.println(s4);
     }
}
```

![06](/md/legacy-assets/java/37/06.webp)

## PreparedStatement를 사용하여 SQL Injection 막는 예시

* 어제 만든 Zipcode앱 사용
  * Statement가 아닌 PreparedStatement로 구현
  * **PreparedStatement 사용시 SQL Injection 방어코드가 필요 없다.**
* PreparedStatement 사용시 LIKE주의
	* **LIKE 사용시 %와 함께 사용되는 bind변수를 인식하지 못한다.**

```sql
LIKE %?%          -- bind변수를 인식을 못함
LIKE '%'||?||'%'  -- %를 일반문자열로 만들어 붙임
```

```java
PreparedStatement pstmt = null;
...

String selectZipcode =
     "SELECT zipcode, sido, gugun, dong, NVL(bunji,' ') bunji  FROM zipcode  WHERE dong LIKE ?||'%'";
// %는 문자열, ''로 묶어줘야 한다
pstmt = con.prepareStatement(selectZipcode);
pstmt.setString(1, dong);

rs = pstmt.executeQuery(); // 매개변수 없는 executeQuery() 호출
```

## ResultSetMetaData

* **실행되는 쿼리문을 사용하여 dictionary가 없어도 테이블의 컬럼정보를 얻는 일을 하는 인터페이스**
* **컬럼정보**
    * 컬럼의 수, 이름, 데이터의 형, 크기 등
    * 제약사항은 알 수 없다.
* 사용법

```java
// 1. ResultSet으로 부터 ResultSetMetaData를 얻는다.
ResultSetMetaData rsmd = rs.getMetaData();

// 2. 컬럼의 수 
int colCnt = rsmd.getColumnCount();

// 3. 컬럼명
String colName = rsmd.getColumnName(컬럼인덱스);
String colLabel = rsmd.getColumnLabel(컬럼인덱스);

// 4. 컬럼의 데이터형명
String colTypeName = rsmd.getColumnTypeName(컬럼인덱스);

// 5. 컬럼의 크기
int colSize = rsmd.getPrecision(컬럼인덱스);
```

* 이제 부터 Connection이 필요한 경우 Singleton Pattern으로 만든 클래스 메소드로 얻는다.
  
![07](/md/legacy-assets/java/37/07.webp)

```java
// Singleton Pattern으로 만드는 클래스
//  DB Connection을 반환하는 일
public class GetConnection {
    private static GetConnection gc;
    
    private GetConnection() {
        
    }
    
    public static GetConnection getInstance() {
        if (gc == null) {
            gc = new GetConnection();
        }
        return gc;
    }
    
    public Connection getConn(String url, String  id, String pass) throws SQLException {
        Connection con = null;
        
        // 1. 드라이버 로딩
        try {
            Class.forName("oracle.jdbc.OracleDriver");
        } catch (ClassNotFoundException cnfe) {
            cnfe.printStackTrace();
        }
        
        // 2. Connection 얻기
        con = DriverManager.getConnection(url,  id, pass);
        
        return con;
    }
}
```

```java
/**
* ResultSetMetaData
* 실행되는 조회쿼리문을 사용하여 컬럼정보를  얻을 때 사용하는 interface
*/
public class UseResultSetMetaData {
    
    public UseResultSetMetaData() throws  SQLException {
        
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        ResultSetMetaData rsmd = null;

        GetConnection gc =  GetConnection.getInstance();

        String url =  "jdbc:oracle:thin:@localhost:1521:orcl";
        String id = "scott";
        String pass = "tiger";
        
        try {
            con = gc.getConn(url,id,pass);
            
            String selectEmp = "SELECT * FROM  emp";
            pstmt =  con.prepareStatement(selectEmp);
            rs = pstmt.executeQuery();
            
            rsmd = rs.getMetaData();
            // 컬럼의 정보 얻기
            
            int cnt = rsmd.getColumnCount();
            System.out.println("컬럼의 수 : "+  cnt);
            System.out.println("처음 컬럼의  컬럼명(Name) : "+rsmd.getColumnName(1));
            System.out.println("처음 컬럼의  컬럼명(Label) : "+rsmd.getColumnLabel(1));
            String colType =  rsmd.getColumnTypeName(1);
            System.out.println("처음 컬럼의  데이터형 명 : "+colType);
            int colPrecision =  rsmd.getPrecision(1);
            System.out.println("처음 컬럼의 크기  : "+colPrecision);
            
            System.out.println("--------------------------------------");
            
            StringBuilder output = new  StringBuilder();
            for(int i=1; i<cnt; i++) {
                output
                .append(rsmd.getColumnLabel(i)).append("\t")
                .append(rsmd.getColumnTypeName(i)).append("\t")
                .append(rsmd.getPrecision(i)).append("\n");
            }
            System.out.println(output);
            System.out.println("--------------------------------------");
            
        } finally {
            if (rs != null) { rs.close(); }
            if (pstmt != null) { pstmt.close();  }
            if (con != null) { con.close(); }
        }
    }
    
    public static void main(String[] args) {
        try {
            new UseResultSetMetaData();
        } catch (SQLException se) {
            se.printStackTrace();
        }
    }
}
```

![08](/md/legacy-assets/java/37/08.webp)

## CLOB

* **Character Large Object**의 약자
* **Oracle에서 제공하는 문자열을 저장하기 위한 데이터형**
    * **CHAR**
        * 고정길이
        * 데이터가 적게 입력되더라도 처음의 크기를 유지
        * 2000byte
    * **VARCHAR2**
        * 가변길이
        * 데이터가 적게 입력되면 처음 크기를 무시하고 줄어드는 데이터형
        * 4000byte
    * 4000자 이상 문자열이 입력되면 문자열 저장이 불가능
* **CLOB은 4GB의 문자열을 저장**

```java
rs.getString("컬럼명"); //으로 CLOB을 얻을 수 없다.
```

* **local에선 getString으로 Clob을 가져올 수 있음.**
    * **웹서버 등에 올리면 CLOB을 getString으로 가져올 수 없다!**
      * **별도의 Stream**을 구성하여 데이터를 얻는다.
* 사용법

```java
// 1. Clob 얻기
Clob clob = rs.getClob(컬럼명);

// 2. Stream 연결
BufferedReader br = new BufferedReader(clob.getCharacterStream());

// 3. 반복하여 줄단위로 읽어들인다.
String str = "";
while((str = br.readLine()) != null) {
    // str
}
```

* CLOB 예제용 테이블 생성

```sql
CREATE TABLE test_clob(
    subject VARCHAR2(90),
    news CLOB,
    reporter VARCHAR2(10),
    input_date DATE
);

INSERT INTO test_clob(subject, news, reporter, input_date)
VALUES ('野3당 \"靑특감반 특검-신재민 의혹 청문회 추진\" 합의','자유한국당·바른미래당·민주평화당 등 야3당은 8일 청와대 특별감찰반 의혹의 진상규명을 위한 특검과 신재민 전 사무관 폭로의혹 관련 국회기획재정위원회 청문회를 추진하기로 합의했다.','강성규',SYSDATE);

SELECT * FROM test_clob;

COMMIT;
```

* CLOB은 local에선 ResultSet의 **getString()**으로도 읽힌다.
    * **잘못된 방법**

```java
public class UseClob {
     
     public UseClob() throws SQLException {
           
           Connection con = null;
           PreparedStatement pstmt = null;
           ResultSet rs = null;
           
           GetConnection gc = GetConnection.getInstance();
           String url = "jdbc:oracle:thin:@localhost:1521:orcl";
           String id = "scott";
           String pass = "tiger";
           String selectClob
            = "SELECT subject, news, reporter, TO_CHAR(input_date, 'yyyy-mm-dd') input_date FROM test_clob";
           
           try {
                con = gc.getConn(url, id, pass);
                pstmt = con.prepareStatement(selectClob);
                
                rs = pstmt.executeQuery();
                
                System.out.println("번호\t기자\t작성일\t기사\n");
                System.out.println("-----------------------------------------------------");
                int cnt = 1;
                String reporter = "", input_date="", news="";
                while(rs.next()) {
                     
                     reporter = rs.getString("reporter");
                     input_date = rs.getString("input_date");
                     // CLOB는  local상에서 getString으로 얻어진다.
                     news = rs.getString("news"); 
                     System.out.printf("%d\t%s\t%s\t%s\n", cnt,  reporter, input_date, news);
                     cnt++;
                }
                System.out.println("-----------------------------------------------------");
                
           } finally {
                if (rs != null) { rs.close(); }
                if (pstmt != null) { pstmt.close(); }
                if (con != null) { con.close(); }
           }
     }
     
     public static void main(String[] args) {
           
           try {
                new UseClob();
           } catch (SQLException se) {
                se.printStackTrace();
           }
           
     }
}
```

![09](/md/legacy-assets/java/37/09.webp)

* **CLOB을 읽는 올바른 방법**
  * 얻어온 CLOB을 통해 새로운 Stream을 만들어서 읽어들인다.
  * 실행 결과는 위와 동일

```java
public UseClob() throws SQLException, IOException {
    
    Connection con = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    
    GetConnection gc = GetConnection.getInstance();
    String url = "jdbc:oracle:thin:@localhost:1521:orcl";
    String id = "scott";
    String pass = "tiger";

    String selectClob
    = "SELECT subject, news, reporter,  TO_CHAR(input_date, 'yyyy-mm-dd') input_date FROM test_clob";
    
    BufferedReader br = null;
    
    try {
        con = gc.getConn(url, id, pass);
        pstmt = con.prepareStatement(selectClob);
        
        rs = pstmt.executeQuery();
        
        System.out.println("번호\t기자\t작성일\t기사\n");
        System.out.println("-----------------------------------------------------");
        int cnt = 1;
        String reporter = "", input_date="";
        StringBuilder news = null;
        String temp = "";
        while(rs.next()) {
                
                reporter = rs.getString("reporter");
                input_date = rs.getString("input_date");
                news = new StringBuilder();
                
                // CLOB 데이터를 얻기 위한 스트림 연결
                br = new  BufferedReader(rs.getClob("news").getCharacterStream());
                while((temp = br.readLine()) != null) {
                    news.append(temp).append("\n");
                }
                
                System.out.printf("%d\t%s\t%s\t%s\n", cnt,  reporter, input_date, news);
                cnt++;
        }
        System.out.println("-----------------------------------------------------");
        
    } finally {
        if (br != null) { br.close(); }
        if (rs != null) { rs.close(); }
        if (pstmt != null) { pstmt.close(); }
        if (con != null) { con.close(); }
    }
}
```

## 숙제

* 어제 구현한 테이블 선택 창에서 선택버튼 클릭 시 다음과 같은 결과를 보이시오.

![10](/md/legacy-assets/java/37/10.webp)

```java
@SuppressWarnings("serial")
public class SelectTableView extends JFrame {
    
    private JButton jbSelect;
    private JComboBox<String> jcbTab;
    private DefaultTableModel dtm;
    private JScrollPane jspTab;
    
    public SelectTableView(JComboBox<String> jcbTab) {
        super("테이블 선택");
        
        this.jcbTab = jcbTab;
        JLabel jl = new JLabel("테이블 선택");
        jbSelect = new JButton("선택");
        
        String[] columnNames = { "컬럼명", "데이터형",  "크기", "제약사항" };
        
        dtm = new DefaultTableModel(columnNames, 3) {
            @Override
            public boolean isCellEditable(int row, int  column) {
                    return false;
            }
        };
        
        JTable tab = new JTable(dtm);
        jspTab = new JScrollPane(tab);
        
        JPanel panel = new JPanel();
        
        panel.add(jl);
        panel.add(jcbTab);
        panel.add(jbSelect);
        
        add(BorderLayout.NORTH, panel);
        add(BorderLayout.CENTER, jspTab);
        
        SelectTableViewEvt stve = new  SelectTableViewEvt(this);
        jbSelect.addActionListener(stve);
        addWindowListener(stve);
        
        setBounds(400, 300, 400, 500);
        setResizable(false);
        setVisible(true);
    }
    public JButton getJbSelect() {
        return jbSelect;
    }
    public JComboBox<String> getJcbTab() {
        return jcbTab;
    }
    public DefaultTableModel getDtm() {
        return dtm;
    }
}
```

```java
public class SelectTableViewEvt extends WindowAdapter implements  ActionListener {
    private SelectTableView stv;
    
    public SelectTableViewEvt(SelectTableView stv) {
        this.stv = stv;
    }
    
    @Override
    public void actionPerformed(ActionEvent e) {
        String tabName = (String)  stv.getJcbTab().getSelectedItem();
        
        if (tabName.equals("테이블명")) {
            JOptionPane.showMessageDialog(stv, "테이블을  선택해주세요.");
            return;
        }
        
        try {
            getRecord(tabName);
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
    }
    
    public void getRecord(String tabName) throws SQLException  {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        
        GetConnection gc = GetConnection.getInstance();

        String url = "jdbc:oracle:thin:@localhost:1521:orcl";
        String id = "scott";
        String pass = "tiger";
        
        try {
            // 1. 드라이버 로딩
            // 2. Connection 얻기
            con = gc.getConn(url, id, pass);
            
            // 쿼리문제였음..
            String selectTabInfo = "" +
                        "SELECT utc.column_name,  utc.data_type, NVL(utc.data_precision,0) data_precision,  NVL(ucc.constraint_name, ' ') constraint_name " +
                        "FROM user_tab_cols utc,  user_cons_columns ucc " +
                        "WHERE utc.table_name=? " +
                        " AND utc.table_name =  ucc.table_name(+) " +
                        " AND utc.column_name =  ucc.column_name(+)";
            
            // 3. 쿼리문 생성객체 얻기
            pstmt = con.prepareStatement(selectTabInfo);

            // 4. 바인드 변수 설정
            pstmt.setString(1, tabName);
            
            // 5. 쿼리 수행 결과 얻기
            rs = pstmt.executeQuery();
            
            // dtm 초기화 JTable Row 초기화*
            stv.getDtm().setRowCount(0);
            
            String[] rowData = new String[4];
            while(rs.next()) {
                    rowData[0] = rs.getString("COLUMN_NAME");
                    rowData[1] = rs.getSTring("DATA_TYPE");
                    rowData[2] = rs.getString("DATA_PRECISION");
                    rowData[3] = rs.getString("CONSTRAINT_NAME");
                    stv.getDtm().addRow(rowData);
            }
            
        } finally {
            // 6. 연결 끊기
            if (rs != null) { rs.close(); }
            if (pstmt != null) { pstmt.close(); }
            if (con != null) { con.close(); }
        }
    }
    @Override
    public void windowClosing(WindowEvent e) {
        stv.dispose();
    }
}
```
