Java Web7 - JDBC
JDBC 연동 작업 4단계
1단계 - JDBC 드라이버 클래스 로드
<%
Class.forName("com.mysql.cj.jdbc.Driver");
%>
JDBC 드라이버 클래스를 로드하려면 java.lang 패키지의 Class 클래스의 static 메서드인 forName() 메서드를 호출하여 메서드 파라미터로 드라이버 클래스가 위치한 패키지명과 클래스명을 문자열로 전달한다. MySQL 의 경우 com.mysql.jdbc 패키지 내의 Driver.class 파일을 지정하면 된다.
2단계 - DB 연결
// 1단계까지 완료...
String url = "jdbc:mysql://localhost:3306/jsp3";
String user = "root";
String password = "1234";
Connection con = DriverManager.getConnection(url, user, password);
java.sql 패키지의 DriverManager 클래스의 static 메서드인 getConnection() 메서드를 호출하여 메서드 파라미터로 DB 접속 URL, 접속 계정, 패스워드를 문자열로 차례대로 전달한다. DB 연결에 성공할 경우 DB 연결 정보를 관리하는 객체가 java.sql.Connection 타입으로 리턴된다. 연결에 성공한 후 DB 접근에 필요한 객체를 가지고 있어야 하니 리턴되는 객체를 Connection 타입 변수에 저장하여 연결 상태를 유지해준다.
3단계 - SQL 구문 작성 및 전달
<%
// 1단계까지 완료...
// 2단계까지 완료...
// 1.
int idx = 11;
String name = "박길동";
String id = "park";
String passwd = "1234";
String sql = "INSERT INTO test2 VALUES (" + idx + ", '" + name + "', '" + id + "', '" + passwd + "')";
// 2.
PreparedStatement pstmt = con.prepareStatement(sql);
%>
-
Connection 객체의 prepareStatement() 메서드를 호출하여 SQL 구문 전달 작업을 수행한다. 파라미터는 실행할 SQL 구문 문자열이며 리턴타입은 java.sql.PreparedStatement이다.
-
SQL문을 작성하였으면 Connection 객체의 prepareStatement() 메서드를 호출하여 실행할 SQL 구문을 전달한다. 파라미터는 실행할 SQL 구문 문자열이며 리턴타입은 java.sql.PreparedStatement이다. 아직까지 SQL 구문을 실행하는 단계는 아니다.
4단계 - SQL 구문 실행 및 결과 처리
<%
// 1단계까지 완료...
// 2단계까지 완료...
// 3단계까지 완료...
int count = pstmt.executeUpdate();
%>
3단계에서 리턴받은 PreparedStatement 객체의 executeXXX() 메서드를 호출하여 SQL 구문을 실행한다. 수행하려는 SQL 구문의 종류에 따라 다른 메서드를 호출하며 리턴타입도 달라진다.
- executeUpdate()
DB 에 조작을 가하는(변경이 발생하는) 쿼리를 실행하는 용도의 메서드 주로 DML 중 INSERT, UPDATE, DELETE 나 DDL(CREATE, ALTER, DROP)을 실행한다. 리턴값으로 int형 값이 리턴되며 조작 후 영향을 받은 레코드 수가 리턴된다. - executeQuery()
SELECT 구문을 실행하는 용도의 메서드다.
INSERT, UPDATE, DELETE는 SQL문의 차이만 있고 전 과정이 동일하다.
SELECT
ResultSet rs = pstmt.executeQuery();
SELECT구문도 3단계 까지는 동일하다. 4단계에서 실행 과정은 같으나 메서드명과 리턴타입이 달라지고 리턴타입에 대한 처리 과정도 달라진다. SELECT 구문 실행을 위해 PreparedStatement 객체의 executeQuery() 메서드를 호출하고 리턴되는 결과값을 java.sql.ResultSet 타입 변수에 저장한다.
아래와 같은 형태의 테이블 구조가 ResultSet 타입 객체로 리턴된다.
idx | name |
---|---|
1 | 홍길동 |
2 | 이순신 |
2 | 김길동 |
<%
out.print(rs.next() + "<br>"); // true
out.print(rs.next() + "<br>"); // true
out.print(rs.next() + "<br>"); // true
out.print(rs.next() + "<br>"); // false(레코드 없음)
%>
맨 처음 시작시에는 현재 커서 위치가 개념상 항상 첫번째 레코드 위에 위치하여 아직 가리키는 열이 없다고 보면된다. 여기서 rs.next()메서드를 호출하면 커서가 아래로 한칸씩 내려가며 가리키는 곳에 레코드가 존재한다면 true, 아니면 false를 리턴하게 된다.
// 컬럼 인덱스로 접근하는 경우
<%
out.print(rs.next()); // 가장 처음에는 한번 호출해줘야 첫번째 레코드를 가리킨다.
out.print(rs.getInt(1)); // 첫번째 컬럼
out.print(rs.getString(2)); // 두번째 컬럼
%>
// 컬럼명으로 접근하는 경우
<%
out.print(rs.next());
out.print(rs.getInt("idx")); // idx 컬럼
out.print(rs.getString("name")); // name 컬럼
%>
커서가 위치한 레코드의 각 컬럼에 접근하여 데이터를 가져오려면 ResulteSet 객체의 getXXX()메서드를 호출하여 각 컬럼 데이터를 가져오면 된다. 위의 경우에서 첫번째 컬럼의 데이터타입이 int고 두번째 컬럼의 데이터타입이 varchar라고 하면 첫번째 컬럼은 getString()메서드로, 두번째 컬럼은 getInt()메서드로 가져오면 된다. getXXX()메서드의 파라미터는 컬럼의 인덱스번호(1부터 시작) 또는 컬럼명(String타입)을 지정하면 된다.
// 단일 레코드일 경우
if(rs.next()) {
out.print(rs.getInt("idx"));
out.print(rs.getString("name"));
} else {
out.print("레코드 없음!");
}
// 복수 레코드일 경우
while(rs.next()) {
out.print(rs.getInt("idx"));
out.print(rs.getString("name"));
}
항상 next()메서드가 true일 때만 작업을 수행하게 만들어 문제의 여지를 사전에 차단하도록 코드를 작성하자.
SQL Injection attack 방지
Statement 객체에 전달하는 쿼리문은 파라미터를 전달할 경우 애플리케이션에서 문자열 결합(+ 연산자)을 통해 데이터를 포함시키는데 이 때, 외부 공격자로부터 개발자가 의도하지 않은 쿼리문이 실행될 수 있는 문제점을 갖는다. 이것이, 해킹 기법 중 하나인 SQL 삽입 공격이다.
PreparedStatement 객체에 전달하는 쿼리문은 외부로부터 전달받은 데이터를 쿼리문에 삽입하기 위해 변수와 문자열 결합을 수행할 수도 있지만 Wildcard를 사용하여 데이터의 위치를 표시만 한 상태로 미리 SQL 구문의 형태를 확정짓고 이후, 별도의 setXXX() 메서드를 호출하여 Wildcard를 데이터로 대체(Bind)하면서 입력 값 검증까지 동시에 수행이 가능하다. 따라서, 데이터에 해당하는 입력 값에 SQL 구문을 입력하더라도 구문으로 인식하지 않고 데이터로 인식하게 되므로 SQL 삽입 공격에 대한 원천적인 봉쇄가 가능해진다.
만능문자(?)를 활용한 SQL 삽입 공격 방지
String sql = "DELETE FROM student WHERE student_no = ?";
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setString(1, student_no);
Leave a comment