Statement의 executeQuery() 메서드는 SELECT 쿼리를 실행할 때 사용되며, SELECT 쿼리의 실행 결과를 java.sql.ResultSet 객체에 담아서 리턴한다. 따라서, ResultSet이 제공하는 메서드를 사용해서 결과값을 읽어올 수 있다.
ResultSet 클래스는 next() 메서드를 제공하는데, next() 메서드를 사용해서 SELECT 결과의 존재 여부를 확인할 수 있다.
ResultSet은 SELECT 쿼리 결과를 위의 그림과 같은 행으로 저장하며 커서를 통해서 각 행의 데이터에 접근한다. 최초에 커서는 1행 이전에 존재한다. ResultSet.next() 메서드는 커서의 다음 행이 존재하는 경우 true를 리턴하고 커서를 그 행으로 이동시킨다. next() 메서드를 계속해서 호출하면 위의 그림과 같이 커서는 순차적으로 다음 행으로 이동한다. 마지막 행에 커서가 도달하면 next() 메서드는 false를 리턴한다.
SELECT 쿼리에 따라서 행의 내용이 결정되는데, 예를 들어 다음과 같은 SELECT 쿼리를 생각해보자.
select NAME, MEMBERID, EMAIL from MEMBER;
위 쿼리의 실행 결과는 다음과 같은 형태를 취한다. 칼럼의 순서가 SELECT 쿼리에서 지정한 칼럼의 순서와 동일한 것을 알 수 있다. 또한, 각 칼럼은 SELECT 쿼리에서 지정한 칼럼의 이름과 동일한 이름을 갖는다.
구분 | 칼럼1(NAME) | 칼럼2(MEMBERID) | 칼럼3(EMAIL) |
행1 | 값 | 값 | 값 |
행2 | 값 | 값 | 값 |
... | ... | ... | ... |
만약 select * from MEMBERID와 같이 칼럼 이름을 일일이 적지 않고 '*'를 사용하여 모든 칼럼을 읽어올 경우에는 테이블 칼럼의 순서와 이름이 곧 칼럼의 순서와 이름이 된다.
ResultSet은 현재 커서 위치에 있는 행으로부터 데이터를 읽어오기 위해 getOOOO() 형태의 메서드를 제공하는데, 이들 메서드 중에서 자주 사용되는 메서드는 아래 표와 같다.
ResultSet 클래스의 주요 데이터 읽기 메서드
메서드 | 리턴 타입 | 설명 |
getString(String name) getString(int index) |
String | 지정한 칼럼 값을 String으로 읽어온다. |
getCharacterStream(String name) getCharacterStream(int index) |
java.io.Reader | 지정한 칼럼 값을 스트림 형태로 읽어온다. LONG VARCHAR 타입을 읽어올 때 사용한다. |
getInt(String name) getInt(int index) |
int | 지정한 칼럼 값을 int 타입으로 읽어온다. |
getLong(String name) getLong(int index) |
long | 지정한 칼럼 값을 long 타입으로 읽어온다. |
getDouble(String name) getDouble(int index) |
double | 지정한 칼럼 값을 double 타입으로 읽어온다. |
getFloat(String name) getFloat(int index) |
float | 지정한 칼럼 값을 float 타입으로 읽어온다. |
getTimestamp(String name) getTimestamp(int index) |
java.sql.Timestamp | 지정한 칼럼 값을 Timestamp 타입으로 읽어온다. SQL TIMESTAMP 타입을 읽어올 때 사용한다. |
getDate(String name) getDate(int index) |
java.sql.Date | 지정한 칼럼 값을 Date 타입으로 읽어온다. SQL DATE 타입으로 읽어올 때 사용한다. |
getTime(String name) getTime(int index) |
java.sql.Time | 지정한 칼럼 값을 Time 타입으로 읽어온다. SQL TIME 타입을 읽어올 때 사용한다. |
ResultSet의 get 계열의 메서드는 현재 커서에서 데이터를 읽어온다. ResultSet은 처음에 첫 번째 행 이전에 커서가 위치하기 때문에, 첫 번째 행에 저장된 데이터를 읽으려면 다음과 같이 next() 메서드를 사용해서 커서를 이동시켜야 한다.
rs = stmt.executeQuery("select * from member");
if(rs.next()){ // 다음 행(첫 번째 행)이 존재하면 rs.next()는 true를 리턴
String name = rs.getString("NAME");
...
}else{
// 첫 번째 행이 존재하지 않는다. 즉, 결과가 없다.
...
}
한 개 이상의 행을 처리할 때에는 while 구문이나 do-while 구문을 사용한다. 먼저 while 구문을 사용할 때에는 다음과 같은 형태를 띠게 된다.
rs = stmt.executeQuery(...);
while(rs.next()){
// 한 행씩 반복 처리
String name = rs.getString(1);
}
do-while 구문을 사용할 때에는 다음과 같이 if 문을 사용해서 먼저 데이터가 존재하는지 여부를 확인한 후에 루프를 반복해야 한다.
rs = stmt.executeQuery(...);
if(rs.next()){
do{
String name = rs.getString("NAME");
...
}while(rs.next());
}
간단하게 파라미터로 아이디를 전달받으면 MEMBER 테이블로부터 해당 회원 정보를 읽어와 출력하는 JSP를 작성해보자. JSP 페이지의 코드는 아래와 같다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ 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>회원 정보</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/jsptest?"+"useUnicode=true&characterEncoding=utf8";
String dbUser="jspexam";
String dbPass="jsppw";
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>아이디</td><td><%=memberID%></td>
</tr>
<tr>
<td>암호</td><td><%=rs.getString("PASSWORD")%></td>
</tr>
<tr>
<td>이름</td><td><%=rs.getString("NAME")%></td>
</tr>
<tr>
<td>이메일</td><td><%=rs.getString("EMAIL")%></td>
</tr>
</table>
<%
}else {
%>
<%=memberID%>에 해당하는 정보가 존재하지 않습니다.
<%
}
}catch (SQLException ex){
%>
에러 발생 : <%=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>
웹 브라우저에 다음과 같은 URL을 입력해서 viewMember.jsp를 실행해보자.
http://localhost:8080/chap14/viewMember.jsp?memberID=아이디
MEMBER 테이블의 MEMBERID 칼럼 값이 입력한 아이디와 일치하는 레코드가 존재한 다음 아래와 같은 결과가 출력되고, 존재하지 않으면 다음과 같은 화면이 출력될 것이다.
SQL의 LONG VARCHAR 타입은 대량의 텍스트를 저장할 때 사용된다. ResultSet에서 LONG VARCHAR 타입의 데이터를 읽어오려면 getCharacterStream() 메서드를 사용해야 한다. ResultSet.getCharacterStream() 메서드는 리턴 타입이 java.io.Reader이기 때문에 사용방법을 잘 모를 수 있을 것 같아 별도로 정리했다.
LONG VARCHAR 타입의 값을 읽어오는 코드는 다음과 같다.
String data = null; // 스트림으로 읽어온 데이터를 저장한다.
java.io.Reader reader = null; // LONG VARCHAR 데이터를 읽어올 스트림
try {
// 1. ResultSet의 getCharacterStream()으로 Reader 구함
reader = rs.getCharacterStream("FIELD"); // 스트림 일거옴
if(reader != null){
// 2. 스트림에서 읽어온 데이터를 저장할 버퍼를 생성한다.
StringBuffer buff = new StringBuffer();
char[] ch = new Char[512];
int len = -1;
// 3. 스트림에서 데이터를 읽어와 버퍼에 저장한다.
while((len = reader.read(ch)) != -1){
buff.append(ch, 0, len);
}
// 4. 버퍼에 저장한 내용을 String으로 변환한다.
data = buff.toString();
}
}catch(IOException ex){
// 5. IO 관련 처리 도중 문제가 있으면 IOException이 발생한다.
// 익셉션 발생
}finally{
// 6. Reader를 종료한다.
if(reader != null) try {reader.close();} catch(IOException ex){}
}
// ... data를 사용
이 코드를 사용하면 LONG VARCHAR로 되어 있는 SQL 타입의 값을 읽어올 수 있다. IO 처리가 포함되어 있어서 코드가 다소 길다.
LONG VARCHAR 타입의 값을 읽어와 출력하는 JSP 페이지를 작성해보자. 먼저, LONG VARCHAR 타입을 포함하는 테이블이 필요하다. 다음은 LONG VARCHAR 타입의 칼럼을 포함하고 있는 간단한 테이블의 생성 쿼리이다. 참고로 다음 쿼리에서 default character set 부분은 MySQL에서만 올바르게 동작한다.
create table MEMBER_HISTORY(
MEMBERID VARCHAR(10) NOT NULL PRIMARY KEY,
HISTORY LONG VARCHAR
)
default character set=utf8;
테이블을 생성했다면 HITORY 칼럼 부분에 길이가 긴 문자열을 값으로 입력하자.
insert into MEMBER_HISTORY values('madvirus',
concat(
'2015 스프링4 프로그래밍입문<br>'
'2013 Spring4.0프로그래밍<br>'
'2012 객체 지향과 디자인 패턴<br>'
'2012 JSP 2.2웹프로그래밍\n'
)
);
concat() 함수는 MySQL에서 문자열을 연결할 때 사용한다. 실제로 위 쿼리는 다음 쿼리와 동일한 문자열을 추가하는데, 쿼리가 너무 길어져서 알아보기 좋게 하려고 concat 함수를 사용해서 문자열을 연결했다.
insert into MEMBER_HISTORY values('madvirus',
'2015 스프링4 프로그래밍입문<br> 2013 Spring4.0프로그래밍<br>2012 객체 지향과 디자인 패턴<br>2012 JSP 2.2웹프로그래밍\n'
);
viewMember.jsp와 비슷하게, memberID 파라미터를 입력받은 값을 사용하여 정보를 검색해서 출력하는 JSP 페이지는 아래와 같다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ 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>회원 정보</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/jsptest?"+"useUnicode=true&characterEncoding=utf8";
String dbUser="jspexam";
String dbPass="jsppw";
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>아이디</td><td><%=memberID%></td>
</tr>
<tr>
<td>히스토리</td>
<td>
<%
String history = null;
Reader reader = null;
try{
reader = rs.getCharacterStream("HISTORY");
if(reader != null){
StringBuilder buff = new StringBuilder();
char[] ch = new char[512];
int len = -1;
while((len = reader.read(ch)) != -1){
buff.append(ch, 0, len);
}
history = buff.toString();
}
}catch (IOException ex){
out.print("익셉션 발생:" + ex.getMessage());
}finally {
if(reader != null) try{ reader.close();}catch (IOException ex){}
}
%>
<%=history%>
</td>
</tr>
</table>
<%
}else {
%>
<%=memberID%>에 해당하는 정보가 존재하지 않습니다.
<%
}
}catch (SQLException ex){
%>
에러 발생 : <%=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>
웹 브라우저에서 다음과 같이 URL에 memberID 파라미터 값을 붙여서 실행해보자.
http://localhost:8080/chap14/viewMemberHistory.jsp?memberID=madvirus
MEMBER_HISTORY 테이블이 memberID 파라미터와 같은 값을 갖는 MEMBERID 칼럼이 존재한다면 아래와 같은 결과 화면이 출력될 것이다.
LONG VARCHAR 타입과 DBMS의 타입
오라클에서는 LONG VARCHAR는 LONG으로 표시하며, MySQL에서는 MEDIUMTEXT로 표시하고 있다. 간혹 오라클의 LONG 타입 칼럼을 읽을 때 타입 이름만 보고서 ResultSet의 getLong() 메서드를 사용하는 실수를 하는데, 오라클의 LONG은 LONG VARCHAR이므로 getCharacterStream() 메서드를 사용해서 읽어와야 한다. MySQL의 경우 MEDIUMTEXT 타입과 함께 TINYTEXT, TEXT, LONGTEXT의 네 가지 TEXT 타입을 지원하고 있으며, 이 네 가지 타입의 값을 읽어올 때에는 getCharacterStream() 메서드를 사용하면 된다.
LONG VARCHAR 타입의 칼럼은 getCharacterStream() 메서드를 사용해서 읽어오는 것이 원칙이지만 다수의 JDBC 드라이버는 getString() 메서드를 사용해서 읽어올 수 있도록 하고 있다. 예를 들어, MySQL의 JDBC 드라이버를 사용하면 getString() 메서드를 사용해서 LONG VARCHAR 타입을 읽어올 수 있다.
getString() 메서드를 사용해서 LONG VARCHAR 타입을 읽어올 수 있는 JDBC 드라이버는 앞서 살펴본 복잡한 코드를 사용하지 않아도 되므로 스트림을 사용해야 할 이유가 없다면 getString() 메서드를 사용하는 것이 편리하다.
SQL 쿼리를 실행할 때 값에 작은따옴표가 들어가면 작은따옴표 두 개를 사용하는 형태로 변경해야 한다. 예를 들어, "king's choice"와 같이 작은 따옴표가 들어간 값으로 칼럼 값을 변경해야 할 경우 다음처럼 작은따옴표를 두 개 사용해야 한다.
update TABLENAME set SOMEFIELD = 'king''s choice' where ...
String 클래스의 replaceAll() 메서드를 사용하면 문자열에 포함된 특정 문자나 단어를 손쉽게 변경할 수 있다. 예를 들어, 문자열에 포함되어 있는 작은따옴표 한 개를 두 개로 변경하고 싶다면 다음과 같이 String 클래스의 replaceAll() 메서드를 사용하면 된다.
String value = "king's choice";
String replaced = value.replaceAll("'", "''");
replaceAll()로 문자열을 치환해주기 어렵지는 않지만, 매번 치환 처리를 하는 것은 귀찮기도 하고 놓치기도 쉽다. PreparedStatement를 사용하면 이런 치환 없이 따옴표가 포함된 것을 사용할 수 있다.
참고