Java 정리 37
08 Jan 2019
Reading time ~9 minutes
Java 정리 37 - JDBC(4), Singleton Pattern, ResultSetMetaData, CLOB
PreparedStatement를 사용한 CRUD
- 소스코드
-
DAO(Data Access Object)
- DB업무에 관련된 업무를 처리하는 클래스
- 사용하는 쪽은 업무에 관한 단어로 method명을 사용
- DAO는 쿼리를 포함한 method명을 사용
- 드라이버로딩과 Connection얻기는 method로 한번만 수행
-
DB쪽은 객체를 안만들고 얻고 쓰는 코드(싱글톤 패턴)가 주를 이룬다
- 객체 생성조차 속도에 영향을 미치기 때문에 이러한 패턴 사용
-
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();
}
// toString을 Override하면 객체주소가 아닌 값이 찍힌다.
@Override
public String toString() {
return "CpEmp2AllVO [empno=" + empno + ", sal=" + sal + ", ename=" + ename + ", hiredate=" + hiredate + "]";
}
- 오라클 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()));
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);
}
}
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으로 만든 클래스 메소드로 얻는다.
// 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();
}
}
}
CLOB
- Character Large Object의 약자
-
Oracle에서 제공하는 문자열을 저장하기 위한 데이터형
-
CHAR
- 고정길이
- 데이터가 적게 입력되더라도 처음의 크기를 유지
- 2000byte
-
VARCHAR2
- 가변길이
- 데이터가 적게 입력되면 처음 크기를 무시하고 줄어드는 데이터형
- 4000byte
- 4000자 이상 문자열이 입력되면 문자열 저장이 불가능
-
CHAR
- CLOB은 4GB의 문자열을 저장
rs.getString("컬럼명"); //으로 CLOB을 얻을 수 없다.
-
local에선 getString으로 Clob을 가져올 수 있음.
-
웹서버 등에 올리면 CLOB을 getString으로 가져올 수 없다!
- 별도의 Stream을 구성하여 데이터를 얻는다.
-
웹서버 등에 올리면 CLOB을 getString으로 가져올 수 없다!
- 사용법
// 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();
}
}
}
-
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(); }
}
}
숙제
- 어제 구현한 테이블 선택 창에서 선택버튼 클릭 시 다음과 같은 결과를 보이시오.
@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();
}
}