ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Camel][JSP] JDBC 프로그래밍 2부 in JSP
    JSP 2020. 3. 15. 22:52

    이 포스팅은 이전 포스팅과 이어지는 내용입니다. 

    2020/03/14 - [JSP] - [Camel][JSP] JDBC 프로그래밍 1부 in JSP

     

    [Camel][JSP] JDBC 프로그래밍 1부 in JSP

    JDBC 프로그래밍이 대해 설명하기에 앞서 저는 MySQL을 사용하며, 데이터 베이스와 통신을 하기위해서는 JDBC 브라이버와의 연동이 필요합니다. JDBC 드라이버가 준비되어 있지 않으신 분들은 이전 포스팅을 참고..

    cameldev.tistory.com

     

    1-5. ResultSet에서 값 읽기

     이전 포스팅에서 설명했던 Statement의 excuteQuery 메소드는 SELECT 쿼리를 실행할 때 사용했습니다. 그런데 이 excuteQuery 메소드의 반환형은 ResultSet 객체 입니다. 그렇기 때문에 excuteQuery 메소드의 반환 값을 읽어오기 위해서는 ResultSet 클래스에서 제공하는 메소드를 활용해야 합니다. 

     

     우선 ResultSet 클래스의 next() 메소드는 SELECT한 결과의 존재 여부를 확인하는 메소드입니다. excuteQuery 메소드의 결과 값은 행의 형태로 ResultSet에 저장되며, 저장된 값에 접근하기 위해서 커서를 사용합니다. 최초 커서의 위치는 첫 번째 행의 이전에 위치하과 있고 next 메소드를 호출할 때마다 다음 행으로 이동하게됩니다. 이때 next() 메소드는 다음 행이 존재하면 true를 반환하고, 다음 행이 존재하지 않는다면 false를 반환합니다. 

     

     ResultSet은 현재 커서의 위치에 있는 행에서 데이커를 읽어오기 위해 아래와 같은 메소드를 사용합니다.

    Type and Method Description
    String getString( String name ) 지정한 칼럼의 값을 String으로 읽는다.
    String getString( int index )
    Reader getCharacterStream( String name ) 지정한 칼럼의 값을 Stream 형태로 읽는다. LONG VARCHAR 타입을 읽어올 때 사용.
    Reader getCharacterStream( int index )
    int getInt( String name ) 지정한 칼럼의 값을 int형으로 읽는다.
    int getInt( int index )
    long getLong( String name ) 지정한 칼럼의 값을 long형으로 읽는다.
    long getLong( int index )
    double getDouble( String name ) 지정한 칼럼의 값을 double형으로 읽는다.
    double getDouble( int index )
    float getFloat( String name ) 지정한 칼럼의 값을 float형으로 읽는다.
    float getFloat( int index )
    TimeStamp getTimeStamp( String name ) 지정한 칼럼의 값을 TimeStamp형으로 읽는다. SQL TIMESTAMP 타입을 읽을 때 사용
    TimeStamp getTimeStamp( int index )
    Date getDate( String name ) 지정한 칼럼의 값을 Data형으로 읽는다. SQL DATE 타입을 읽을 때 사용
    Date getDate( int index )
    Time getTime( String name ) 지정한 칼럼의 값을 Time형으로 읽는다. SQL TIME 타입을 읽을 때 사용
    Time getTime( int index )

     위의 메소드들은 현재 커서의 위치의 데이터를 읽어오며, 첫 번째 행에 저장된 데이터를 읽어오기 위해서는 사전에 next 메소드를 사용해 커서를 이동시켜 줘야합니다. 

     

     ResultSet 메소드 예제 코드

    <%@ page contentType = "text/html; charset=utf-8" %>
    
    <%@ page import = "java.sql.DriverManager" %>
    <%@ page import = "java.sql.Connection" %>
    <%@ page import = "java.sql.Statement" %>
    <%@ page import = "java.sql.ResultSet" %>
    <%@ page import = "java.sql.SQLException" %>
    
    <%
        String memberID = request.getParameter("memberID");
    %>
    <html>
    <head><title>Member Info</title></head>
    <body>
    
    <%
        Class.forName("com.mysql.jdbc.Driver");
        
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        
        try {
            String jdbcDriver = "jdbc:mysql://localhost:3306/chap12?" +
                                "useUnicode=true&characterEncoding=utf8";
            String dbUser = "camel";
            String dbPass = "1234";
            String query = 
               "select * from MEMBER where MEMBERID = '"+memberID+"'";
            
            conn = DriverManager.getConnection(jdbcDriver, dbUser, dbPass);
            stmt = conn.createStatement();
            
            rs = stmt.executeQuery(query);
            if( rs.next() ) {
    %>
    <table border="1">
    <tr>
        <td>ID</td><td><%= memberID %></td>
    </tr>
    <tr>
        <td>Pasword</td><td><%= rs.getString("PASSWORD") %></td>
    </tr>
    <tr>
        <td>Name</td><td><%= rs.getString("NAME") %></td>
    </tr>
    <tr>
        <td>E-mail</td><td><%= rs.getString("EMAIL") %></td>
    </tr>
    </table>
    <%
            } else {
    %>
    <%= memberID %> Information is not exist
    <%
            }
        } catch(SQLException ex) {
    %>
    ERROR : <%= ex.getMessage() %>
    <%
        } finally {
            if (rs != null) try { rs.close(); } catch(SQLException ex) {}
            if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
            if (conn != null) try { conn.close(); } catch(SQLException ex) {}
        }
    %>
    
    </body>
    </html>
    

    만약 JDBC MySQL 5.1.23 이상 버전을 사용하시는 경우 serverTimeZone 관련 에러가 발생할 수 있습니다. 그럴 경우 getConnection 메소드의 첫 파라미터로 지정되는 JDBC URL을 아래와 같이 변경하면 제대로 실행됩니다. 

    String jdbcDriver = "jdbc:mysql://localhost:3306/ch2?" +
                                "useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&characterEncoding=utf8";
    String jdbcDriver = "jdbc:mysql://localhost:3306/ch2?" +
                        "useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&characterEncoding=utf8";

    이 문제가 발생하는 이유는 MySQL 5.1.23 이상의 버전을 사용할 경우 MySQL TimeZone의 시간표현 포맷이 달라졌기 때문에 connector 에서 인식을 못하기 때문이라고 합니다.  

     

     

     

    1.6 ResultSet에서 LONG VARCHAR 형의 값 읽기

     위의 ResultSet의 메소드를 설명해 놓은 표를 확인해보면 반환형이 Reader인 getCharacterStream 메소드를 확인할 수 있습니다. 이 메소드는 LONG VARCHAR 형의 값을 읽어오기 위해 사용되는 메소드입니다.

     

     이를 확인하기 위해서는 우선 LONG VARCHAR 형의 데이터를 가지고 있는 테이블을 생성해주시길 바랍니다. LONG VARCHAR 형의 값을 읽는 예제 코드는 아래와 같습니다.

     

     하지만 다수의 드라이버는 getString 메소드를 사용해서 LONG VARCHAR 형을 읽어올 수 있도록 되어 있습니다. 그렇기 때문에 getString 메소드를 사용해 LONG VARCHAR 형을 읽어올 수 있다면 getString 메소드를 사용하는 것이 편리하고 간단합니다. 

    <%@ page contentType = "text/html; charset=utf-8" %>
    
    <%@ page import = "java.sql.DriverManager" %>
    <%@ page import = "java.sql.Connection" %>
    <%@ page import = "java.sql.Statement" %>
    <%@ page import = "java.sql.ResultSet" %>
    <%@ page import = "java.sql.SQLException" %>
    <%@ page import = "java.io.Reader" %>
    <%@ page import = "java.io.IOException" %>
    
    <%
    	String memberID = request.getParameter("memberID");
    %>
    <html>
    <head><title>Member Info</title></head>
    <body>
    
    <%
    	Class.forName("com.mysql.jdbc.Driver");
    	
    	Connection conn = null;
    	Statement stmt = null;
    	ResultSet rs = null;
    	
    	try {
    		String jdbcDriver = "jdbc:mysql://localhost:3306/ch2?" +
    							"useUnicode=true&characterEncoding=uft8";
    		String dbUser = "camel";
    		String dbPass = "1234";
    		String query =  "select * from MEMBER_HISTORY "+
    						"where MEMBERID = '"+memberID+"'";
    		
    		conn = DriverManager.getConnection(jdbcDriver, dbUser, dbPass);
    		stmt = conn.createStatement();
    		
    		rs = stmt.executeQuery(query);
    		if(rs.next()) {
    %>
    <table border="1">
    <tr>
    	<td>ID</td><td><%= memberID %></td>
    </tr>
    <tr>
    	<td>LVARCHAR</td>
    	<td>
    <%
    			String lVarChar = null;
    			Reader reader = null;
    			try {
    				reader = rs.getCharacterStream("LVARCHAR");
    				
    				if (reader != null) {
    					StringBuffer buff = new StringBuffer();
    					char[] ch = new char[512];
    					int len = -1;
    					
    					while( (len = reader.read(ch)) != -1) {
    						buff.append(ch, 0, len);
    					}
    					
    					lVarChar = buff.toString();
    				}
    			} catch(IOException ex) {
    				out.println("ERROR :"+ex.getMessage());
    			} finally {
    				if (reader != null) try { reader.close(); } catch(IOException ex) {}
    			}
    %>
    	<%= lVarChar %>
    	</td>
    </tr>
    </table>
    <%
    		} else {
    %>
    <%= memberID %> long VarChar is not exist
    <%
    		}
    	} catch(SQLException ex) {
    %>
    ERROR : <%= ex.getMessage() %>
    <%
    	} finally {
    		if (rs != null) try { rs.close(); } catch(SQLException ex) {}
    		if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
    		if (conn != null) try { conn.close(); } catch(SQLException ex) {}
    	}
    %>
    
    </body>
    </html>
    

     

     

    1-6. PreparedStatement를 사용한 Query 실행

    앞서 이전 포스팅에서 Statement에 대한 설명을 했습니다. PrepareStatement는 Statement와 동일한 기능을 제공합니다. 그렇다면 이 둘의 차이점은 무엇일까? 이 둘의 차이점은 SQL Query의 틀을 미리 생성하느냐 마냐 입니다. 

     

     PrepareStatement의 경우는 SQL Query의 틀을 미리 생성한 후 값을 나중에 지정합니다. PrepareStatement을 사용하기 위해서는 아래와 같은 과정을 필요로 합니다. 

    1. Connection의 prepareStatement 메소드를 사용해 PrepareStatement를 생성해야합니다.
    2. 그리고 난 뒤 PrepareStatement의 set 메소드를 사용해서 필요한 값을 지정할 수 있습니다.
    3. PrepareStatement를 실행하기 위해서는 excuteQuery, excuteUpdate 메소드를 사용해야합니다. 

     

    PrepareStatement를 생성하고 값을 지정하는 자세한 방법은 아래의 코드와 같이 구성하면 됩니다. 

    PrepareStatement prstmt = null;
    ....
    prstmt = conn.prepareStatement("insert into MEMNER (MEMBERID, NAME, EMAIL) values (?,?,?)");
    prstmt = setString(1, "camel"); // value의 첫번째 인덱스 값 지정
    prstmt = setString(2, "낙타");

    PrepareStatement는 각각의 SQL 타입을 처리할 수 있는 set 계열의 메소드를 제공하고 있으며 그 목록은 아래의 표와 같습니다. 

    Method Description 
    setString( int index, String a ) 지정한 index의 파라미터 값을 String 값 a로 지정.
    setCharacterStream( int index, Reader reader, int length ) 지정한 index의 값을 LONG VARCHAR 형으로 지정. reader는 읽어올 스트림을 의미하고, length는 지정할 문자열의 길이를 의미. 
    setInt( int index, int a) 지정한 index의 파라미터 값을 int 값 a로 지정.
    setLong( int index, long a ) 지정한 index의 파라미터 값을 long 값 a로 지정.
    setDouble( int index, double a ) 지정한 index의 파라미터 값을 double 값 a로 지정.
    setFloat( int index, float a ) 지정한 index의 파라미터 값을 float 값 a로 지정.
    setTimestamp( int index, Timestamp a ) 지정한 index의 값을 Timestamp 값 a로 지정.
    setDate( int index, Date a ) 지정한 index의 값을 Date 값 a로 지정.
    setTime( int index, Time a ) 지정한 index의 값을 Time 값 a로 지정.

     

    PrepareStatement를 생성했다면 실행하기 위해서 excuteQuery 또는 excuteUpdate 메소드를 사용해 PrepareStatement를 실행할 수 있습니다. 두 메소드의 특징은 아래와 같습니다. 

    • excuteQuery( ) - SELECT Query를 실행할 때 사용되며, 반환형은 ResultSet이다. 
    • excuteUpdate( ) - INSERT, UPDATE, DELETE Query를 실행할 때 사용되며, 반환형은 int로 실행으로 인해 변경된 레코드의 개수를 반환.

     

    PrepareStatement 사용 예제 코드

    PrepareStatement를 사용해 MEMBER Table에 값을 Insert하는 예제이며, insertForm.jsp를 실행하여 값을 입력한 뒤 INSERT 버튼을 누르면 post 방식으로 데이터가 insertMember.jsp로 전달되게 됩니다. 

     

    insertForm.jsp

    <%@ page contentType = "text/html; charset=utf-8" %>
    <html>
    <head><title>MEMBER INSERT FORM</title></head>
    <body>
    
    <form action="/ch2/insertMember.jsp" method="post">
    <table border="1">
    <tr>
    	<td>ID</td>
    	<td><input type="text" name="memberID" size="10"></td>
    	<td>PASSWORD</td>
    	<td><input type="text" name="password" size="10"></td>
    </tr>
    <tr>
    	<td>NAME</td>
    	<td><input type="text" name="name" size="10"></td>
    	<td>EMAIL</td>
    	<td><input type="text" name="email" size="10"></td>
    </tr>
    <tr>
    	<td colspan="4"><input type="submit" value="INSERT"></td>
    </tr>
    </table>
    </form>
    </body>
    </html>
    

    insertMember.jsp

    <%@ page contentType = "text/html; charset=utf-8" %>
    
    <%@ 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 prstmt = null;
    
    	try {
    		String jdbcDriver = "jdbc:mysql://localhost:3306/ch2?" +
    							"useUnicode=true&characterEncoding=utf8";
    		String dbUser = "camel";
    		String dbPass = "1234";
    		
    		conn = DriverManager.getConnection(jdbcDriver, dbUser, dbPass);
    		prstmt = conn.prepareStatement(
    			"insert into MEMBER values (?, ?, ?, ?)");
    		prstmt.setString(1, memberID);
    		prstmt.setString(2, password);
    		prstmt.setString(3, name);
    		prstmt.setString(4, email);
    		
    		prstmt.executeUpdate();
    	} finally {
    		if (prstmt != null) try { prstmt.close(); } catch(SQLException ex) {}
    		if (conn != null) try { conn.close(); } catch(SQLException ex) {}
    	}
    
    %>
    <html>
    <head><title>Insert</title></head>
    <body>
    Record Inserted into MEMBER Table
    </body>
    </html>
    

     

     

     

    1-7. PrepareStatement를 사용하는 이유

     위에서 PrepareStatement를 설명하면서 PrepareStatement는 Statement와 거의 동일하다고 언급했습니다. 그렇다면 Statement가 있는데 왜 PrepareStatement를 사용하는 것일까? 그 이유는 아래와 같습니다. 

    • 반복해서 실행되는 동일한 Query의 속도를 빠르게 하기 위해서 사용
    • 값 변환을 자동으로 하기 위해 사용
    • 코드의 간결화

     

     첫 번째 이유로, PrepareStatement와 Statement의 차이점은 SQL Query를 미리 분석하느냐 마냐입니다. 반복해서 동일한 Query가 실행된다면 Statement의 경우 Query를 실행할 때마다 SQL Query의 분석이 발생하는 반면, PrepareStatement는 미리 SQL Query를 분석해 놓기 때문에 SQL Query분석을 한번만 하면 됩니다. 이러한 차이는 처리 속도의 문제로 직결되게 되는 것입니다. 

     

     두 번째 이유로, Statement를 사용할 때 "낙'타"와 같이 작은따옴표(')가 포함된 값을 지정할 경우 작은 따옴표를 두 번 사용하는 형태로 변경하는 과정이 필요합니다. 하지만 PrepareStatement의 경우는 setString 메소드를 호출할 때 알아서 값의 변경이 일어나기 때문에 작은 따옴표를 직접 처리해 줄 필요가 없습니다. 

     PrepareStatement의 이러한 특징은 따옴표가 많아질 경우 더 더욱 유용하게 느껴지며, 코드의 간결화를 이끌어 낼 수 있습니다. 그렇기 때문에 값의 지정이 필요한 Query를 실행할 때에는 PrepareStatement가 유용한 경우가 많습니다. 

     

    1-8. 웹 어플리케이션 구동 시 JDBC드라이버 로딩

     앞서 설명을 위해서 모든 예제 코드의 처음부분에서 JDBC 드라이버를 로딩하는 것을 확인할 수 있습니다. 하지만 실제 JDBC 드라이번은 한번만 로딩을 해주면 이후 계속 사용할 수 있기 때문에 모든 JSP 페이지에서 JDBC 드라이버를 로딩해 줄 필요가 없습니다. 

     

     일반적으로 웹 어플리케이션이 시작될 때만 JDBC 드라이버를 로딩해주는 것이 좋습니다. 그렇기 때문에, 웹 컨테이너가 시작될 때 자동으로 JDBC 드라이버가 로딩되도록 지정하는 것이 좋습니다. 웹 어플리케이션이 시작될 때 자동으로 JDBC 드라이버를 로딩하도록 하기 위해서는 아래와 같은 서블릿 클래스를 사용하는 방법이 있습니다.

     

    package jdbc.loader;
    
    import javax.servlet.http.HttpServlet;
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import java.util.StringTokenizer;
    
    public class Loader extends HttpServlet {
    
    	public void init(ServletConfig config) throws ServletException {
    		try {
    			String drivers = config.getInitParameter("jdbcdriver");
    			StringTokenizer st = new StringTokenizer(drivers, ",");
    			while (st.hasMoreTokens()) {
    				String jdbcDriver = st.nextToken();
    				Class.forName(jdbcDriver);
    			}
    		} catch(Exception ex) {
    			throw new ServletException(ex);
    		}
    	}
    }
    

    위의 코드는 서블릿이 초기화 될 때 호출되는 init 메소드를 구현한 것입니다. 

     

    web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <web-app version="3.0" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee">
    
    
    	<servlet>
    
    		<servlet-name>JDBCDriverLoader</servlet-name>
    
    		<servlet-class>jdbc.loader.Loader</servlet-class>
    
    
    		<init-param>
    
    			<param-name>jdbcdriver</param-name>
    
    			<param-value>com.mysql.jdbc.Driver</param-value>
    
    		</init-param>
    
    		<load-on-startup>1</load-on-startup>
    
    	</servlet>
    
    </web-app>

    Loader 클래스에 대한 추가 설정을 위해 web.xml 파일을 변경해 주었습니다. 위와 같이 <servlet> 태그를 사용하면 웹 어플리케이션 구동 시 자동으로 Loader 클래스의 init 메소드가 실행되게 되면서 JDBC 드라이버가 로딩되는 것입니다. 

     

     이와 같이 JDBC 드라이버를 로딩하면 JSP 페이지에서는 JDBC 드라이버를 로딩할 필요가 없어지게됩니다. web.xml 파일을 수정했다면 웹 컨테이너를 재시작해주는 것이 좋습니다. Tomcat 버전에 따라 자동으로 web.xml 파일의 변경 내용을 다시 읽어오는 경우도 있지만, 그렇지 못하는 경우도 있기 때문에 web.xml 파일을 수정했다면 웹 컨테이너를 재시작하는 것을 권해드립니다. 

    댓글

Camel`s Tistory.