• Home
  • About
    • Young's Github Pages photo

      一日不作一日不食

    • About
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

Java 정리 37

08 Jan 2019

Reading time ~9 minutes

Java 정리 37 - JDBC(4), Singleton Pattern, ResultSetMetaData, CLOB


PreparedStatement를 사용한 CRUD

01

  • 소스코드
  • DAO(Data Access Object)
    • DB업무에 관련된 업무를 처리하는 클래스
  • 사용하는 쪽은 업무에 관한 단어로 method명을 사용

02

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

02-2

  • 드라이버로딩과 Connection얻기는 method로 한번만 수행
    • DB쪽은 객체를 안만들고 얻고 쓰는 코드(싱글톤 패턴)가 주를 이룬다
      • 객체 생성조차 속도에 영향을 미치기 때문에 이러한 패턴 사용
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에서 필요한 처리는 다 하고 원하는 결과만 받아 사용하는게 가장 좋다.
String selectAllCpEmp2 = "SELECT empno, ename, "
		+ "TO_CHAR(hiredate, 'yyyy-mm-dd') hiredate, sal FROM cp_emp2";
  • executeUpdate()의 결과는 int로 0~n이 반환, 즉 반환값이 0만 아니면 정상수행된 것
int cnt = pstmt.executeUpdate();

if (cnt != 0) { delete나 update는 변경된 여러행이 반환(1~n)
     // 0이 아니면 정상수행 되었다는 의미
     flag = true;
}
  • 값을 확인할 때 toString 오버라이드 한 후 찍어보는게 좋다.
    • VO 객체의 경우 기본적으로 toString으로 출력시 객체 주소가 찍힌다.
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

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

04

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

Design Pattern

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

Singleton Pattern

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

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

  • 어제 만든 Zipcode앱 사용
    • Statement가 아닌 PreparedStatement로 구현
    • PreparedStatement 사용시 SQL Injection 방어코드가 필요 없다.
  • PreparedStatement 사용시 LIKE주의
    • LIKE 사용시 %와 함께 사용되는 bind변수를 인식하지 못한다.
LIKE %?%          -- bind변수를 인식을 못함
LIKE '%'||?||'%'  -- %를 일반문자열로 만들어 붙임
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가 없어도 테이블의 컬럼정보를 얻는 일을 하는 인터페이스
  • 컬럼정보
    • 컬럼의 수, 이름, 데이터의 형, 크기 등
    • 제약사항은 알 수 없다.
  • 사용법
// 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

// 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;
    }
}
/**
* 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

CLOB

  • Character Large Object의 약자
  • Oracle에서 제공하는 문자열을 저장하기 위한 데이터형
    • CHAR
      • 고정길이
      • 데이터가 적게 입력되더라도 처음의 크기를 유지
      • 2000byte
    • VARCHAR2
      • 가변길이
      • 데이터가 적게 입력되면 처음 크기를 무시하고 줄어드는 데이터형
      • 4000byte
    • 4000자 이상 문자열이 입력되면 문자열 저장이 불가능
  • CLOB은 4GB의 문자열을 저장
rs.getString("컬럼명"); //으로 CLOB을 얻을 수 없다.
  • local에선 getString으로 Clob을 가져올 수 있음.
    • 웹서버 등에 올리면 CLOB을 getString으로 가져올 수 없다!
      • 별도의 Stream을 구성하여 데이터를 얻는다.
  • 사용법
// 1. Clob 얻기
Clob clob = rs.getClob(컬럼명);

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

// 3. 반복하여 줄단위로 읽어들인다.
String str = "";
while((str = br.readLine()) != null) {
    // str
}
  • CLOB 예제용 테이블 생성
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()으로도 읽힌다.
    • 잘못된 방법
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

  • CLOB을 읽는 올바른 방법
    • 얻어온 CLOB을 통해 새로운 Stream을 만들어서 읽어들인다.
    • 실행 결과는 위와 동일
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

@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;
    }
}
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();
    }
}


Java