> Transaction 대상 쿼리문 하나로 작업이 완료되면 auto commit은 문제를 발생시키지 않지만,
여러 개의 쿼리문이 하나의 Transaction을 구성하면 문제를 발생시킬 수 있음
> 여러 개의 작업이 모두 성공했을 때에만 DB작업이 완료되어야 하고,
여러 개의 작업 중 하나라도 실패하면 모든 DB작업이 취소되어야 함
> 여러 쿼리가 있을 때 Transaction으로 관리하자
- 작업방법)
1) Connection의 auto commit을 해제
con.setAutoCommit(false) ; // 개발자가 transaction 완료 후 commit 또는 rollback 해야 함
*주의 : method 안에서 Connection을 종료하면 commit이 된 후 Connection이 종료되므로,
연결 끊기는 DB작업 method 밖에서 끊음
(Connection을 DB작업 method 외부에서 선언 <- method가 2개로 분리됨)
2) 쿼리문 작업 외부에 Connection 선언
3) DB작업 method가 쿼리 수행 행수를 반환
4) DB작업 method가 개발자가 목표로 한 레코드 수를 반환했을 때에만 commit 또는 rollback을 수행하고 연결을 끊음
- rollback 처리시 차이점:
1) update/delete의 경우 : 쿼리문 수행 실패 시 0건 수행이므로 else 부분에서 rollback 처리
2) insert의 경우 : 쿼리문 수행 실패 시 SQLException 발생하므로 catch 부분에서 rollback 처리
# 오늘의 코딩 #
- Transaction을 처리해보자
- 여러 쿼리문을 실행하고 모두 수행되었을 경우에만 commit!
# DB 구성은 아래와 같다!
- TRANSACTION2 테이블의 주소컬럼의 크기를 작게 설정하여 INSERT 되지 않을 경우/될 경우를 TEST 예정
TRANSACTION1TRANSACTION2
# Transaction Test할 클래스
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import test.dao.DbConnection;
/**
* 쿼리문 여러개로 하나의 Transaction이 구성되는 경우의 처리
* @author user
*/
public class TestTransaction {
/**
* transaction을 처리할 쿼리문을 실행하고, 결과를 받아와서 transaction을 완료하는 method
*/
@SuppressWarnings("resource")
public void transaction() {
Connection con = null;
try {
// 2. 커넥션 얻기
con = DbConnection.getInstance().getConn();
// autocommit 해제
con.setAutoCommit(false);
// 3. 쿼리문 수행 후 결과 얻기
int cnt = sqlJob(con); // 여러개의 쿼리문이 하나의 transaction을 구성
// 쿼리문이 실행된 후 목표로 한 행수가 나오면 commit 아니면 rollback
if (cnt == 2) {
con.commit(); }// 트랜잭션 완료
} catch (SQLException e) {
try {
// insert로 트랜잭션이 구성되는 경우 catch 에서 rollback을 처리함
con.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}//end catch
e.printStackTrace();
} finally {
try {
if (con != null) {
con.close();
} // end if
} catch (SQLException e) {
e.printStackTrace();
} // end catch
} // end finally
}// transaction
/**
* Connection을 받아, DB작업을 수행하는 method<br>
* transaction1 테이블과 transaction2 테이블에 이름과 주소를 입력하여 추가된 행수를 반환하는 일
*
* @param con
* @return 쿼리문을 수행한 결과 횟수 (총합)
* @throws SQLException
*/
public int sqlJob(Connection con) throws SQLException {
int allCnt = 0; // 쿼리문 수행 성공 횟수를 담을 변수
String name = "김동동";
String addr = "서울시 동작구";
// 쿼리문 수행객체 얻기
// transaction1 테이블에 작업
String insert1 = "insert into transaction1(name,addr) values (?,?)";
PreparedStatement pstmt = con.prepareStatement(insert1);
pstmt.setString(1, name);
pstmt.setString(2, addr);
int cnt1 = pstmt.executeUpdate();
// transaction2 테이블에 작업
String insert2 = "insert into transaction2(name,addr) values (?,?)";
PreparedStatement pstmt2 = con.prepareStatement(insert2);
pstmt2.setString(1, name);
pstmt2.setString(2, addr);
int cnt2 = pstmt2.executeUpdate();
allCnt = cnt1 + cnt2;
return allCnt;
}// sqlJob
public static void main(String[] args) {
TestTransaction tt = new TestTransaction();
tt.transaction();
}// main
}// class
- JVM에서 하나의 인스턴스를 생성하고 사용하는 개발 패턴 (Design pattern)
- 메모리를 절감할 수 있음
- 참조하는 속도가 빠름
- Framework에서 주로 객체를 생성하고 사용하는 방식
- 아래 작성법이 class안에 들어가있으면 singleton 패턴인 클래스 라고 함
- Ex) Calendar Class의 getInstance()
- 작성법)
작성법
코드 예
1. 객체화를 클래스 안에서만 할 수 있도록 작성함 => 생성자의 접근지정자를 private로 지정 2. 객체를 생성하여 반환하는 일을 하는 method 작성 => public static 클래스명 getInstance() { 3. 객체를 하나로 유지하고 반환하는 일. if ( 객체 == null ) { 객체 = new 생성자(); } return 객체; }
class Test{ private static Test t; private Test(){ }
public static Test getInstance(){ if (t == null) { t = new Test(); } return t; } }
# 예시 #
- 싱글톤 패턴의 클래스
/**
* 싱글톤 패턴으로 만드는 클래스 : 객체가 하나로만 사용되는 클래스
* @author user
*/
public class Singleton {
private static Singleton single;
private Singleton() { // 접근지정자 private으로 클래스 외부에서 객체화 불가
}// Singleton
/**
* 생성된 객체를 얻기 위한 method
* @return
*/
public static Singleton getInstance() {
// 객체를 하나로 생성 관리할 수 있는 코드
if (single == null) {
single = new Singleton();
}//end if
return single;
}// getInstance
}// class
- 싱글톤 클래스 사용
public class UseSingleton {
public static void main(String[] args) {
//생성자가 private이기 때문에 클래스 외부에서 객체화 될 수 없음
// Singleton single = new Singleton(); // 생성자가 보이지 않아서 생성 불가! getInstance 사용해야함
//getInstance method를 통해서만 객체를얻음
Singleton single = Singleton.getInstance();
Singleton single2 = Singleton.getInstance();
Singleton single3 = Singleton.getInstance();
//몇개를 생성하든 객체는 하나이므로 주소는 똑같당
System.out.println(single);
System.out.println(single2);
System.out.println(single3);
//일반 class는 객체 생성 횟수만큼 heap 주소가 할당됨!
UseStatement us = new UseStatement();
UseStatement us2 = new UseStatement();
UseStatement us3 = new UseStatement();
System.out.println(us);
System.out.println(us2);
System.out.println(us3);
}//main
}//class
- 클래스에서 쿼리문 실행 method 작성 시 연결을 반드시 끊기 위해 try~finally로 작성
Ex) Connection con = null; Statement stmt = null;
try { connection 연결 ~ 쿼리문 생성 객체 얻기~ 쿼리문 수행 후 결과얻기~ }
finally { conncetion 및 쿼리문 객체 연결 끊기}
- SQLException은 getErrorCode() 존재 : DB에서 발생한 error code를 확인할 수 있음
> 반환형 int
1 : PK 제약
1438 : 정수 컬럼의 설정된 길이보다 큰 값이 입력된 경우
12899 : 문자열 컬럼의 설정된 길이보다 큰 값이 입력된 경우
2.SQLInjection
- Web에서 DBMS의 정보를 탈취하기 위해 가장 많이 발생하는 공격
- 프로그램 외부에서 쿼리문을 예상하여, 나머지 쿼리문을 작성하여 입력하는 방법
- 방어 방법) PreparedStatement를 사용 / injection block code 작성
- Statement 사용시 발생 / PreparedStatement 사용하면 발생 X
Ex) login창에서 id와 password를 입력받아서 인증을 진행하는 경우
> Statement 사용시, JTextArea로 사용자로부터 값을 입력받을 때 ' or 1=1 -- 로그인 창에서 id에 id대신 이것을 입력하면
항상 참이 되어 첫번째 사람 정보로 로그인 됨
> SQLInjection 발생
> 따라서, Injection 방어 코드를 아래와 같이 만들어서 쿼리 실행문에 적용해야 함
- PreparedStatement를 사용하면 방어 코드 필요 없음!!
# 방어코드 작성 예시 #
/**
* SQLInjection 방어하기<br>
* 입려값에 공백, 주석, ', 쿼리문이 입력되면 해당 데이터를 삭제함
* @return
*/
public String blockInjection(String value) {
//' or 1=1 -- : 로그인 창에서 id에 id대신 이것을 입력하면 항상 참이 되어 첫번째 사람 정보로 로그인 됨
//>> SQLInjection 발생!
return value.replaceAll(" ", "").replaceAll("--", "").replaceAll("'", "");
// --대신 -해도 됨
}//blockInjection
# 방어코드 적용 예시 #
// 쿼리문 생성 객체 작성시
StringBuilder selectName = new StringBuilder();
selectName
.append(" select name ")
.append(" from test_login ")
.append(" where id='").append(blockInjection(loginVO.getId())).append("' and pass='")
.append(loginVO.getPass()).append("'");
Connection con = DriverManager.getConnection(url, id, pass);
① url
- 연결할 DB의 포트 번호와 DB명 넣어야 함
- @은 ip주소 표시하는 것, localhost(127.0.01.)는 loopback
- oracle은 1521가 기본 포트고, SID는 컴퓨터마다 유일한 하나의 값을 가짐!
② ip/pw
- 설정된 url에 id와 pw를 가지고 DB와 연동을 기다림
③ getConnection
- DB는 port를 열고 접속을 기다림 (오라클 port 번호 : 1521 / name : orcl)
> DB가 접속을 허가하면 인증 정보를 getConnection이 받는다
3. 쿼리문 생성객체 얻기
- 쿼리문 생성 객체 종류 : statement, preparedstatement, callablestatement
> Statement, PreparedStatement : 쿼리 실행
> CallableStatement : procedure 호출
- java.sql.Connection Interface로부터 얻음
Statement -얻기 : Statement stmt = con.createStatement(); - 객체는 쿼리문을 실행할 때마다 반복적으로 계속 생성하여 실행하는 객체 > 쿼리문 실행 : excuteXXX(String sql) - Statement 객체는 실행할 쿼리문을 알지 못함 - 쿼리문이 반복적으로 실행될 때 효율이 떨어짐 - 쿼리문에 값을 넣어 생성하기 때문에 쿼리문의 복잡도가 높음 - PreparedStatement / CallableStatement의 부모 - SQLInjection 발생 : injecton block code 작성 필요
PreparedStatement -얻기 : PreparedStatement pstmt = con.prepareStatement(String sql); > 쿼리문 실행 : excute() - 객체는 쿼리문을 한 번만 생성하고 값만 넣어서 실행하는 객체 - PreparedStatement 객체는 실행할 쿼리문을 알고 있음 - 쿼리문에 bind변수를 설정하고 값을 나중에 입력함 > bind 변수 : 쿼리문 안에 ? 로 정의하는 변수, 추후에 값이 반드시 들어가야 함, PreparedStatment에서 많이 사용됨 - 쿼리문을 한 번만 생성하고 값을 변경하여 실행하기 때문에 쿼리문이 반복적으로 실행되는 환경에서 효율이 좋음 > Java는 대용량 서비스를 많이 제공하기 때문에 반복 쿼리가 많은 편이라 PreparedStatement가 자주 사용됨! - 쿼리문과 입력되는 값을 따로 작성하기 때문에 쿼리문의 복잡도가 낮아짐 - Statement Interface의 하위 Interface이나, 부모의 method는 잘 쓰지 않음 -SQLInjection 발생하지 않음 : injecton block code 불필요
CallableStatement - 얻기 : CallableStatement cstmt = con.prepareCall(String sql); > 여기서의 sql은 호출할 procedure명 - procedure를 호출하는 일 (직접 실행 - 쿼리문 없이 실행하는 것) // 쿼리는 간접 실행 - PreparedStatement Interface의 하위 Interface - bind 변수로 입력하는 값을 할당할 수 있음 - registerOutParameter() method를 사용하여 procedure의 OutParameter를 처리해야 함 (OutParameter : java method의 반환형같은 것! OurParameter 여러 개 설정 가능)
4. 쿼리문 수행 후 결과 얻기
- 쿼리를 작성하여 실행
- 쿼리문 분류에 따라 사용하는 실행 method 가 다르므로 주의
(어떤 excute를 써도 실행은 되나, 올바른 결과가 아님)
DB를 변경하는 쿼리문 create, drop, truncate, grant, revoke - 처리 됐는지 안됐는지로 판단하므로, 반환형이 boolean인 Statement Interface의 execute() 사용 - stmt.excute() insert, update, delete - 몇 행이 추가/변경/삭제 되었는가가 중요하므로, 반환형이 int인 Statement Interface의 excuteUpdate() 사용 - 단, insert는 subquery가 아니라면 성공 아님 예외라서 return값을 안 받아도 됨 update와 delete : 0건 적용 (조건 잘못 넣었을때) or 적용완료 or 실패임 - stmt.excuteUpdate() commit, rollback - commit과 rollback은 Connection Interface에 method가 따로 존재하므로 그걸 씀 > commit(), rollback()
DB를 변경하지 않는 쿼리문 select - 결과가 조회되어야 함! 반환형이 ResultSet인 Statment Interface의 excuteQuery() 사용 - ResultSet = stmt.excuteQuery() // ResultSet은 다음편에!
5. bind 변수에 값 설정 (PreparedStatment의 경우 해당)
- bind 변수는 쿼리문에 ?표로 기술하는 변수
- 장점) SQL문과 값이 분리된다
- PreparedStatement의 method 활용 (parameter index => bind index를 말함)
- bind 변수는 차례로 1번의 index부터 시작하며, setXXX()로 bind 변수에 값을 설정함
> 숫자 : pstmt.setInt (bind변수의 인덱스, 값);
> 문자열 : pstmt.setString (bind변수인덱스, 값);
- 주의)
① bind 변수에는 ‘ 를 사용하지 않음
② Java는 like 의 % 와 함께 사용되면 바인드 변수를 인식하지 못함
해결방법) where 컬럼명 like ‘?%’ (X)
=> %를 일반 문자열로 변경하고 ?와 붙여줌(‘와 || 사용)
=> where 컬럼명 like ?||’%’ (O)
③ bind 변수는 값이 들어가는 부분에만 사용할 수 있음 & 테이블명/컬럼명을 직접 사용할 수 없음
해결방법) 변수로 갖다 붙이자! "select * from ?" // 안됨=> "select * from " + 테이블명
> 단, 테이블명이 값으로 들어갈 때에는 bind변수를 사용할 수 있음 (다른 변수에 할당해서 사용은 가능)
6. 연결 끊기 (반드시!!)
- rs.close(); // select query의 경우 생성되는 ResultSet 객체가 있으면 이거 먼저 끊음