학원을 다닌 지 한 달 반이 다 되어갈 무렵 JSP와 서블릿을 거의 마치게 되면서 여태껏 배운 내용을 토대로 간단한 웹 홈페이지를 제작하게 되었습니다. 제작을 해보면서 느낀 점은 "근본이 중요하구나." 라는 점이었는데, 신기술을 배우기에 앞서 그 기술들의 시작점의 원리를 안다는 것은 앞으로 배워나갈 웹페이지 제작 신기술을 보다 더 잘 이해할 수 있다고 생각되기에 해당 결론에 도달하지 않았나 생각됩니다.
아무튼 해당 홈페이지는 Model1 방식 (JSP에서 프론트, 백, DB를 한꺼번에 처리하는 방식) 으로 제작되었으며 다음과 같은 서블릿 기술들이 사용되었습니다.
이번 홈페이지 로직에 사용된 JSP 모델 1 그림과 설명(자료 출처 : https://johnmarc.tistory.com/6)
- 사용자가 JSP 페이지에 요청을 보냄
- JSP에서 받은 요청 정보를 세팅한 뒤 직접 DB에 쿼리문을 전달하고 결과값을 반환 받음
- 반환 받은 결과값을 토대로 스크립틀릿으로 직접 출력하거나, 따로 출력만 담당하는 페이지를 리다이렉트, 혹은 인클루드(액션 태그)를 이용해 페이징 처리
우선 프로젝트는 다음과 같이 구성되어 있으며, 라이브러리리는 DB 연동을 위한 OJDBC6 버전을 사용했습니다.
오라클 DB의 경우 멤버 테이블과 시퀀스 하나씩 만들어 줬습니다.
create table member( no number primary key, id varchar2(20) not null, pw varchar2(20) not null, name varchar2(20) not null, age number not null, addr varchar2(50), reg_date Date ); create sequence member_seq start with 1 increment by 1;
가장 기본적인 메인 페이지이며, 스타일은 table을 중심으로 제작되었습니다. 각 버튼을 클릭하면 예외 처리 함수들이 실행된 뒤 각 JSP 페이지로 이동 처리를 하였습니다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> // 스타일 <style type="text/css"> table { margin: 0 auto; border: 1px solid lightseagreen; } thead { background-color: lightseagreen; color: white; } tfoot{ background-color: lightseagreen; } td{ text-align: center; padding: 3px; } th{ height: 30px; padding: 5px; } input[type=button], input[type=reset]{ background-color: lightseagreen; border: 1px solid white; padding: 5px; color: white; } input[type=button]:hover { background-color: white; color: lightseagreen; font-weight: bold; cursor: pointer; } </style> <script type="text/javascript" defer> function view_all(){ location.href = "view_all.jsp"; } // 예외 처리 function view_one(f){ let regId = /^[0-9a-zA-Z]{8,16}$/; if (!regId.exec(f.id.value)) { alert("대소문자와 소문자를 혼합한 8~16자의 아이디를 입력해 주세요."); f.id.value = ""; f.id.focus(); return; } f.action = "view_one.jsp"; f.submit(); } function insert_one(f){ let regId = /^[0-9a-zA-Z]{8,16}$/; if (!regId.exec(f.id.value)) { alert("대소문자와 소문자를 혼합한 8~16자의 아이디를 입력해 주세요."); f.id.value = ""; f.id.focus(); return; } let regPw = /^[0-9a-zA-Z]{8,16}$/; if (!regPw.exec(f.pw.value)) { alert("대소문자와 소문자를 혼합한 8~16자의 비밀번호를 입력해 주세요."); f.pw.value = ""; f.pw.focus(); return; } let koreanRegName = /^[가-힣]{2,6}$/; if (!koreanRegName.exec(f.name.value)) { alert("올바른 이름을 입력해 주세요."); f.name.value = ""; f.name.focus(); return; } let regAge = /^[0-9]{1,3}$/; if (!regAge.exec(f.age.value)) { alert("올바른 나이를 입력해 주세요."); f.age.value = ""; f.age.focus(); return; } let regAddr = /^[가-힣0-9\s]{5,30}$/; if (!regAddr.exec(f.addr.value)) { alert("올바른 주소를 5자 이상 입력해 주세요"); f.addr.value = ""; f.addr.focus(); return; } f.action = "insert_one.jsp"; f.submit(); } function remove_one(f){ let regId = /^[0-9a-zA-Z]{8,16}$/; if (!regId.exec(f.id.value)) { alert("대소문자와 소문자를 혼합한 8~16자의 아이디를 입력해 주세요."); f.id.value = ""; f.id.focus(); return; } let regPw = /^[0-9a-zA-Z]{8,16}$/; if (!regPw.exec(f.pw.value)) { alert("대소문자와 소문자를 혼합한 8~16자의 비밀번호를 입력해 주세요."); f.pw.value = ""; f.pw.focus(); return; } f.action = "remove_one.jsp"; f.submit(); } function update_one(f){ let regId = /^[0-9a-zA-Z]{8,16}$/; if (!regId.exec(f.id.value)) { alert("대소문자와 소문자를 혼합한 8~16자의 아이디를 입력해 주세요."); f.id.value = ""; f.id.focus(); return; } let regPw = /^[0-9a-zA-Z]{8,16}$/; if (!regPw.exec(f.pw.value)) { alert("대소문자와 소문자를 혼합한 8~16자의 비밀번호를 입력해 주세요."); f.pw.value = ""; f.pw.focus(); return; } f.action = "update_one.jsp"; f.submit(); } </script> </head> HTML 코드 <body> <form method="post"> <table> <thead> <tr> <th colspan="2">회원 정보 관리</th> </tr> </thead> <tbody> <tr> <td>아이디</td> <td><input type="text" name="id"></td> </tr> <tr> <td>비밀번호</td> <td><input type="password" name="pw"></td> </tr> <tr> <td>이름</td> <td><input type="text" name="name"></td> </tr> <tr> <td>나이</td> <td><input type="number" name="age"></td> </tr> <tr> <td>주소</td> <td><input type="text" name="addr"></td> </tr> </tbody> <tfoot> <tr> <th colspan="2"> <input type="button" value="전체보기" onclick="view_all()"> <input type="button" value="검색" onclick="view_one(this.form)"> <input type="button" value="삽입" onclick="insert_one(this.form)"> <input type="button" value="삭제" onclick="remove_one(this.form)"> <input type="button" value="수정" onclick="update_one(this.form)"> <input type="reset" value="다시 작성"> </th> </tr> </tfoot> </table> </form> </body> </html>
DB에 등록된 모든 사용자의 정보를 볼 수 있는 곳으로 전체보기 버튼을 눌러서 확인할 수 있을 뿐만 아니라 사용자가 삽입 및 삭제 행위를 할 때에도 해당 페이지로 리다이렉트 되게 되어있는데요.
이때 해당 페이지는 index.js를 include 액션 태그로 포함시켜 등록을 마친 사용자 정보를 바로 한 번에 확인할 수 있도록 구성했으며, 그래서 사용자는 실제로 index.js (정보 입력창)이 포함된 결과 창을 같이 확인할 수 있게 됩니다.
<%@page import="org.joonzis.db.DBConnect"%> <%@page import="java.sql.ResultSet"%> <%@page import="java.sql.PreparedStatement"%> <%@page import="java.sql.Connection"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> // index.jsp 페이지 포함 <jsp:include page="index.jsp"/> <br> <hr> <br> <% // DB 사용 준비 Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBConnect.getConnection(); String sql = "select * from member"; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); %> // HTML 화면 처리 <h1 style = "text-align:center" >member 테이블의 전체 데이터</h1> <table> <thead> <tr> <th>회원번호</th> <th>아이디</th> <th>비밀번호</th> <th>이름</th> <th>나이</th> <th>주소</th> <th>가입일</th> </tr> </thead> <tbody> <tr> // 조건문으로 처리할 데이터가 없다면 없음을, 있으면 데이터를 가져와 출력 <%if(!rs.next()){ %> <td colspan="7">member 데이터가 없습니다.</td> <%}else{ do{%> <tr> <td><%= rs.getInt(1) %> </td> <td><%= rs.getString(2) %> </td> <td><%= rs.getString(3) %> </td> <td><%= rs.getString(4) %> </td> <td><%= rs.getInt(5) %> </td> <td><%= rs.getString(6) %> </td> <td><%= rs.getDate(7) %> </td> </tr> <%}while(rs.next());%> <%}%> </tr> </tbody> </table> <% // 예외 처리 } catch (Exception e) { e.printStackTrace(); try { conn.rollback(); } catch (Exception e2) { e2.printStackTrace(); } }finally { try { if(ps != null) {ps.close();} if(conn != null) {conn.close();} } catch (Exception e2) { e2.printStackTrace(); } } %> </body> </html>
해당 페이지의 경우 '회원가입' 으로 보시는 것도 맞으며, 해당 예외처리를 마치고 submit 되어 넘어온 정보를 토대로 파라미터를 저장한 뒤 오라클 DB와 연결하는 작업을 거쳐 해당 쿼리에 저장문을 작성함으로 DB 등록을 마치도록 했는데요.
리다이렉트로는 view_all 페이지로 이동시켜 그 결과값을 확인할 수 있도록 하였습니다.
<%@page import="org.joonzis.db.DBConnect"%> <%@page import="java.sql.PreparedStatement"%> <%@page import="java.sql.Connection"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% // 정보 전달 값 획득 request.setCharacterEncoding("UTF-8"); String id = request.getParameter("id"); String pw = request.getParameter("pw"); String name = request.getParameter("name"); int age = Integer.parseInt(request.getParameter("age")); String addr = request.getParameter("addr"); // DB 사용 준비 Connection conn = null; PreparedStatement ps = null; try { conn = DBConnect.getConnection(); // 쿼리값 설정 String sql = "insert into member(no, id, pw, name, age, addr, reg_date)" + "values(member_seq.nextval, ?, ?, ?, ?, ?, sysdate)"; 오라클 DB의 해당 쿼리(테이블의 행)에 전달 받은 파라미터들을 등록 ps = conn.prepareStatement(sql); ps.setString(1, id); ps.setString(2, pw); ps.setString(3, name); ps.setInt(4, age); ps.setString(5, addr); // 쿼리 실행 int result = ps.executeUpdate(); // 콘솔로 결과 출력 후 view_all 페이지 리다이렉트 if(result > 0) { System.err.println("데이터 추가 완료"); }else { System.out.println("데이터 추가 실패"); } response.sendRedirect("view_all.jsp"); } catch (Exception e) { e.printStackTrace(); try { conn.rollback(); } catch (Exception e2) { e2.printStackTrace(); } response.sendRedirect("view_all.jsp"); }finally { try { if(ps != null) {ps.close();} if(conn != null) {conn.close();} } catch (Exception e2) { e2.printStackTrace(); } } %>
해당 설명은 받아오는 파라미터의 개수와 쿼리 부분만 다르고 나머지는 insert.jsp와 동일합니다.
<%@page import="org.joonzis.db.DBConnect"%> <%@page import="java.sql.PreparedStatement"%> <%@page import="java.sql.Connection"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% request.setCharacterEncoding("UTF-8"); String id = request.getParameter("id"); String pw = request.getParameter("pw"); Connection conn = null; PreparedStatement ps = null; try { conn = DBConnect.getConnection(); String sql = "delete from member where id = ? AND pw = ?"; ps = conn.prepareStatement(sql); ps.setString(1, id); ps.setString(2, pw); int result = ps.executeUpdate(); if(result > 0) { System.err.println("데이터 삭제 완료"); }else { System.out.println("데이터 추가 실패"); } response.sendRedirect("view_all.jsp"); } catch (Exception e) { e.printStackTrace(); try { conn.rollback(); } catch (Exception e2) { e2.printStackTrace(); } response.sendRedirect("view_all.jsp"); }finally { try { if(ps != null) {ps.close();} if(conn != null) {conn.close();} } catch (Exception e2) { e2.printStackTrace(); } } %>
이 부분은 설명할 부분이 좀 긴데요. 일단 사용자한테 전달 받는 값은 아이디와 비밀번호로 동일한데, 해당 값을 받아 DB에 등록되어 있는 해당 사용자의 정보(로우, 행)를 받아옵니다. (select 부분)
그리고 받아온 정보를 토대로 사용자에게 HTML 부분에서 입력을 할 수 있게끔 해당 정보들(rs의 값들)을 스크립틀릿 형태로 input에 뿌려 놓는데요. 변경을 불가능하게 만들 값은 hidden 속성을 주거나, 아니면 그냥 input만 빼거나, readOnly 속성을 주는 방법으로 사용자가 수정할 값을 변경 못하게끔 막아놓습니다.
그 나머지 변경 가능한 정보는 사용자가 인풋 창에 입력하게끔 해놓고 버튼을 누르면 update 함수가 실행되며, 해당 정보들이 모두 존재해 있는 상태인지 아닌지를 예외처리 한 뒤 정보들이 빠짐 없이 입력된 상태라면 (누락이 되지 않은 상태라면) update.jsp를 실행시켜서 insert, remove_one.jsp 와 동일하게 해당 행을 업데이트 시켜주도록 정보를 넘겨줍니다. (action으로 경로를 지정하고 submit으로 정보를 넘겨줌)
이 부분은 주석으로 이해하기에는 순서가 좀 섞여있어서 제가 위에서 설명 드린 내용들을 번호로 지정해서 순서 순으로 보시도록 해놓겠습니다.
<%@page import="org.joonzis.db.DBConnect"%> <%@page import="java.sql.ResultSet"%> <%@page import="java.sql.PreparedStatement"%> <%@page import="java.sql.Connection"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> // ★ 3번 순서 ★ <script type="text/javascript"> function update(f){ if(f.pw.value == '' || !f.name.value || f.age.value === null || f.addr.value === null){ alert("정보가 비어있습니다. 확인해 주세요.") return; } f.action = "update.jsp"; f.submit(); } </script> <body> <jsp:include page="index.jsp"/> <br> <hr> <br> <% // ★ 1번 순서 ★ request.setCharacterEncoding("utf-8"); response.setContentType("text/html; charset=UTF-8"); String id = request.getParameter("id"); String pw = request.getParameter("pw"); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBConnect.getConnection(); String sql = "select * from member where id = ? and pw = ?"; ps = conn.prepareStatement(sql); ps.setString(1, id); ps.setString(2, pw); rs = ps.executeQuery(); %> // ★ 2-1번 순서 ★ <h1 style = "text-align:center"><%=id %> 회원의 데이터</h1> <form method = "post"> <table> <thead> <tr> <th>회원번호</th> <th>아이디</th> <th>비밀번호</th> <th>이름</th> <th>나이</th> <th>주소</th> <th>가입일</th> </tr> </thead> <tbody> <tr> // ★ 2-2번 순서 ★ <%if(!rs.next()){ %> <td colspan="7">member 데이터가 없습니다.</td> <%}else{%> <tr> <td> <%= rs.getInt(1) %> <input type = "hidden" name = "idx" value = "<%= rs.getInt(1) %>"> </td> <td> <%= rs.getString(2) %> </td> <td> <input type = "password" name = "pw" value = "<%= rs.getString(3) %>"> </td> <td> <input type = "text" name = "name" value = "<%= rs.getString(4) %>"> </td> <td> <input type = "number" name = "age" value = "<%= rs.getInt(5) %>"> </td> <td> <input type = "text" name = "addr" value = "<%= rs.getString(6) %>"> </td> <td> <input type = "text" name = "reg_date" value = "<%= rs.getDate(7) %>" readonly> </td> </tr> <%}%> </tr> </tbody> <tfoot> <tr> <td colspan = "7"> <input type = button value = "수정" onclick = "update(this.form)"> </td> </tr> </tfoot> </table> </form> <% } catch (Exception e) { e.printStackTrace(); try { conn.rollback(); } catch (Exception e2) { e2.printStackTrace(); } }finally { try { if(ps != null) {ps.close();} if(conn != null) {conn.close();} } catch (Exception e2) { e2.printStackTrace(); } } %> </body> </html>
특정 사용자를 검색합니다. 설명은 ResertSet 부분(쿼리 셀렉트를 위한 객체)과 executeQuery 부분을 제외한 insert_one.jsp 등과 같은 페이지 설명과 동일합니다.
<%@page import="org.joonzis.db.DBConnect"%> <%@page import="java.sql.ResultSet"%> <%@page import="java.sql.PreparedStatement"%> <%@page import="java.sql.Connection"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <jsp:include page="index.jsp"/> <br> <hr> <br> <% String id = request.getParameter("id"); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBConnect.getConnection(); String sql = "select * from member where id = ?"; ps = conn.prepareStatement(sql); ps.setString(1, id); rs = ps.executeQuery(); %> <h1 style= "text-align:center">member 테이블의 전체 데이터</h1> <table> <thead> <tr> <th>회원번호</th> <th>아이디</th> <th>비밀번호</th> <th>이름</th> <th>나이</th> <th>주소</th> <th>가입일</th> </tr> </thead> <tbody> <tr> <%if(!rs.next()){ %> <td colspan="7">member 데이터가 없습니다.</td> <%}else{%> <tr> <td><%= rs.getInt(1) %> </td> <td><%= rs.getString(2) %> </td> <td><%= rs.getString(3) %> </td> <td><%= rs.getString(4) %> </td> <td><%= rs.getInt(5) %> </td> <td><%= rs.getString(6) %> </td> <td><%= rs.getDate(7) %> </td> </tr> <%}%> </tr> </tbody> </table> <% } catch (Exception e) { e.printStackTrace(); try { conn.rollback(); } catch (Exception e2) { e2.printStackTrace(); } }finally { try { if(ps != null) {ps.close();} if(conn != null) {conn.close();} } catch (Exception e2) { e2.printStackTrace(); } } %> </body> </html>
프로젝트를 진행하면서 참... MVC1 패턴이 저에게는 되게 힘들다는 생각이 많이 들었습니다. (특히 스크립틀릿 언어를 사용하면서....) 무엇보다도 모듈화가 되어있지 않고 단순히 JSP to JSP 형식으로 페이지 이동이 되다보니 프로젝트를 하기 전에는 "깔끔해서 괜찮네?" 라고 생각했는데 막상 하고보니 코드가 막 뒤섞여 있어서 차라리 모듈화를 하는 MVC2 패턴이 훨씬 낫다는 생각이 들었습니다.
예전에 공부 했던 내용들 중에 리다이렉트나 스크립틀릿의 사용에 대해서 예제만 풀어서 정리하다보니 이해가 아쉬웠는데, 이번에 간단한 프로젝트를 해보면서 보다 더 와닿게 알 수 있었습니다. 물론 JSP의 근본을 배우고 있다보니 이것보다 더 훨씬 쉬운 기술들이 깔려있어서 얼른 배워보고 싶다는 느낌도 들었습니다.