데이터베이스 프로그래밍 기초(4) : JSP에서 JDBC 프로그래밍하기(4)

de_sj_awa·2021년 5월 22일
0

11. PreparedStatement를 사용한 쿼리 실행

java.sql.PreparedStatement는 java.sql.Statement와 동일한 기능을 제공한다. 차이점이 있다면 PreparedStatement는 SQL 쿼리의 틀을 미리 생성해 놓고 값을 나중에 지정한다는 것이다. PreparedStatement를 사용하는 순서는 다음과 같다.

  1. Connection.prepareStatement() 메서드를 사용하여 PreparedStatement 생성
  2. PreparedStatement의 set 메서드를 사용하여 필요한 값 지정
  3. PreparedStatement의 executeQuery() 또는 executeUpdate() 메서드를 사용하여 쿼리를 실행
  4. finally 블록에서 사용한 PreparedStatement를 닫음(close() 메서드 실행)

PreparedStatement를 생성할 때에는 실행할 쿼리를 미리 입력하는데, 이때 다음과 같이 값 부분을 물음표('?')로 대치한 쿼리를 사용한다.

PreparedStatement pstmt = null;
...
pstmt = conn.prepareStatement(
  "insert into MEMBER(MEMBERID, NAME, EMAIL) values (?, ?, ?)");

PreparedStatement 객체를 생성한 다음에는 PreparedStatement가 제공하는 set 계열의 메서드를 사용해서 물음표를 대체할 값을 지정해주어야 한다. 예를 들면 다음과 같이 각각의 물음표에 들어갈 값을 지정한다.

pstmt.setString(1, "madvirus");  // 첫 번째 물음표의 값 지정
pstmt.setString(2, "최범균"); // 두 번째 물음표의 값 지정

이때 첫 번째 물음표의 인덱스는 1이며, 이후 물음표의 인덱스는 나오는 순서대로 인덱스 값이 1씩 증가한다.

ResultSet의 get 계열의 메서드와 마찬가지로 PreparedStatement는 각각의 SQL 타입을 처리할 수 있는 set 계열의 메서드를 제공하고 있다. 이들 메서드는 다음 표와 같다.

메서드 설명
setString(int index, String x) 지정한 인덱스의 파라미터 값을 x로 지정한다.
setCharacterStream(int index, Reader reader, int length) 지정한 인덱스의 파라미터 값을 LONG VARCHAR 타입의 값으로 지정할 때 사용한다. Reader는 값을 읽어올 스트림이며, length는 지정한 문자열의 길이를 나타낸다.
setInt(int index, int x) 지정한 인덱스의 파라미터 값을 int 값 x로 지정한다.
setLong(int index, long x) 지정한 인덱스의 파라미터 값을 double 값 x로 지정한다.
setFloat(int index, float x) 지정한 인덱스의 파라미터 값을 float 값 x로 지정한다.
setTimestamp(int index, Timestamp x) 지정한 인덱스의 파라미터 값을 TIMESTAMP 타입을 타나내는 java.sql.Timestamp 타입으로 지정한다.
setDate(int index, Date x) 지정한 인덱스의 값을 SQL DATE 타입을 나타내는 java.sql.Date 타입으로 지정한다.
setTime(int index, Time x) 지정한 인덱스의 값을 SQL TIME 타입을 나타내는 java.sql.Time 타입으로 지정한다.

set 계열의 메서드를 사용하여 이 물음표에 해당하는 값들을 모두 지정했다면 다음의 두 메서드를 이용해서 쿼리를 실행할 수 있다. PreparedStatement를 생성할 때에 실행할 쿼리를 지정하기 때문에 이 메서드는 쿼리를 인자로 입력받지 않는다.

  • ResultSet executeQuery() : SELECT 쿼리를 실행할 때 사용하며 ResultSet을 결과값으로 리턴한다.
  • int executeUpdate() : INSERT, UPDATE, DELETE 쿼리를 실행할 때 사용하며, 실행 결과로 변경된 레코드의 개수를 리턴한다.

간단하게 PreparedStatement 클래스를 사용하여 MEMBER 테이블에 값을 삽입하는 예제를 작성해보자. 먼저 데이터 입력 폼은 아래 코드와 같다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>MEMBER 테이블 레코드 삽입</title>
</head>
<body>
<form action="/chap14/webContent/insert/insert.jsp" method="post" >
    <table border="1">
        <tr>
            <td>아이디</td>
            <td><input type="text" name="memberID" size="10"></td>
            <td>암호</td>
            <td><input type="text" name="password" size="10"></td>
        </tr>
        <tr>
            <td>이름</td>
            <td><input type="text" name="name" size="10"></td>
            <td>이메일</td>
            <td><input type="text" name="email" size="10"></td>
        </tr>
        <tr>
            <td colspan="4"><input type="submit" value="삽입"></td>
        </tr>
    </table>
</form>
</body>
</html>

아래 그림은 insertForm.jsp의 실행 결과이다.

위의 그림에서 [삽입] 버튼을 누르면 insert.jsp에 POST 방식으로 데이터를 전송한다. insert.jsp는 아래 예제 코드와 같은데, PreparedStatement를 사용해서 입력받은 데이터를 테이블에 저장한다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.sql.DriverManager" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.SQLException" %>

<%
    request.setCharacterEncoding("utf-8");

    String memberID = request.getParameter("memberID");
    String password = request.getParameter("password");
    String name = request.getParameter("name");
    String email = request.getParameter("email");

    Class.forName("com.mysql.jdbc.Driver");

    Connection conn = null;
    PreparedStatement pstmt = null;

    try{
        String jdbcDriver = "jdbc:mysql://localhost:3306/jsptest?"+"useUnicode=true&characterEncoding=utf8";
        String dbUser="jspexam";
        String dbPass="jsppw";

        conn = DriverManager.getConnection(jdbcDriver, dbUser, dbPass);
        pstmt = conn.prepareStatement("insert into MEMBER values (?, ?, ?, ?)");
        pstmt.setString(1, memberID);
        pstmt.setString(2, password);
        pstmt.setString(3, name);
        pstmt.setString(4, email);

        pstmt.executeUpdate();
    }finally {
        if(pstmt!=null)try{pstmt.close();}catch (SQLException ex){}
        if(conn!=null)try{conn.close();}catch (SQLException ex){}
    }
%>
<html>
<head>
    <title>삽입</title>
</head>
<body>

MEMBER 테이블에 새로운 레코드를 삽입했습니다.

</body>
</html>

insertForm.jsp에서 데이터를 알맞게 입력한 후 [삽입] 버튼을 클릭해보자. 별다른 문제 없이 쿼리를 실행했다면 아래와 같은 결과 화면이 출력될 것이다.

viewMemberList.jsp를 실행하면 다음과 같이 값이 올바르게 추가되었다.

12. PreparedStatement에서 LONG VARCHAR 타입 값 지정하기

PreparedStatement에서 LONG VARCHAR 타입의 값을 지정할 때에는 다음의 메서드를 사용한다고 위의 표에서 설명한 바 있다.

  • setCharacterStream(int index, Reader reader, int length)

setCharacterStream() 메서드는 Reader로부터 length 글자 수만큼 데이터를 읽어와 저장한다. Reader에는 String으로부터 데이터를 읽어오는 Reader와 File로부터 데이터를 읽어오는 Reader 등 다양한 Reader가 있다. 예를 들어, setcharacterStream() 메서드를 이용해서 String 타입의 값을 저장하고 싶다면 다음 코드와 같이 StringReader를 이용하면 된다.

PreparedStatement pstmt = null;
try{
    String value = "..."; // LONG VARCHAR에 넣을 값
    pstmt = conn.prepareStatement(...);
    java.io.StringReader reader = new java.io.StringReader(value);
    pstmt.setCharacterStream(1, reader, value.length());
    ...
}catch(SQLException ex){
    ...
}finally{
    ...
    if(pstmt != null) try{ pstmt.close(); }catch(SQLException ex){}
}

텍스트 파일로부터 데이터를 읽어와 저장하고 싶다면 다음과 같이 FileReader를 이용하면 된다.

PreparedStatement pstmt = null;
FileReader reader = null;
try{
    pstmt = conn.prepareStatement(...);
    reader = new java.io.FileReader(파일경로);
    pstmt.setCharacterStream(1, reader);
    ...
}catch(IOException ex){
    ...
}finally{
    ...
    if(pstmt != null) try { pstmt.close(); }catch(SQLException ex) {}
    if(reader != null) try { reader.close(); }catch(SQLException ex) {}

13. PreparedStatement 쿼리를 사용하는 이유

Statement 쿼리 대신 PreparedStatement 쿼리를 사용하는 주된 이유는 다음과 같다.

  • 값 변환을 자동으로 하기 위해
  • 간결한 코드를 위해

PreparedStatement를 사용할 때의 장점은 값 변경을 하지 않아도 된다는 점이다. 예를 들어, Statement를 사용해서 "최'범균"과 같은 중간에 작은따옴표가 포함된 값을 지정하려면 다음과 같이 작은따옴표를 두 번 사용하는 형태로 변경해야 한다.

stmt.executeQuery("select * from member where name = '" + "최'범균".replaceAll("'", "''") + "'");

하지만, PreparedStatement의 경우는 setString() 메서드를 호출할 때 알아서 값을 변경해주기 때문에 작은 따옴표를 변환할 필요가 없다.

pstmt.String(1, "최'범균");  // 알맞게 따옴표 처리

TIMESTAMP나 DATE, TIME 타입의 경우는 더욱 복잡해서 DBMS마다 날짜와 시간을 표현하는 방식이 다르기 때문에 Statement를 이용해서 직접 쿼리에 값을 지정하면 DBMS마다 코드가 달라진다. 반면에, PreparedStatement를 사용하면 DBMS에 상관없이 다음과 같은 동일한 코드를 사용하게 된다.

Timestamp time = new Timestamp(System.currentTimeMillis());
pstmt.setTimestamp(3, time);

값 변환이 쉬워지는 것과 더불어 PreparedStatement 클래스를 사용하면 코드가 깔끔해진다. 예를 들어, Statement 클래스를 사용하는 경우 UPDATE 쿼리는 다음과 같은 형태가 된다.

stmt.executeQuery("update member set NAME = '" + name + "' where " + "MEMBERID = '" + id + "'");

지정할 값이 많아질 경우 따옴표가 복잡하게 얽히기 때문에 코드 작성 과정에서 오류가 발생할 수도 있고, 나중에 코드를 수정할 때에도 조심스럽게 변경해야 한다. 하지만, PreparedStatement를 사용하면 위 코드는 다음과 같이 복잡하지 않은 코드로 바뀐다.

PreparedStatement pstmt = conn.prepareStatement("update member set NAME = ? where MEMBERID = ?");
pstmt.setString(name);
pstmt.setString(id);

이러한 PreparedStatement의 장점 때문에 검색 조건과 같이 값을 지정해야 하는 쿼리를 실행할 때에는 PreparedStatement를 선호한다.

profile
이것저것 관심많은 개발자.

0개의 댓글