[JSP] 회원제 게시판 구현 1 : 회원 관련 기능
회원제 게시판은 크게 회원 관련 기능과 게시판 기능으로 구성된다.
- 회원 가입
- 회원 정보 수정하기
회원 정보를 수정하려면 사용자가 누군지 알아야 한다. 따라서 다음 기능도 필요하다.
- 로그인 하기
- 로그아웃 하기
회원 정보는 로그인한 사람만 수정할 수 있어야 하므로, 다음 기능도 필요하다.
- 로그인한 사람만 특정 기능 실행하기
1.1 데이터 베이스 생성
create database board default character set utf8; |
1.2 이클립스 프로젝트 생성
- [File]-[New]-[Dynamic Web Project] 메뉴를 실행한다.
- New Dynamic Web Project 대화창에서 다음과 같이 설정후 [Finish] 버튼을 클릭해서 프로젝트를 생성한다.
a. Project name : board
b. Target runtime, web module version : Apache Tomcat v9.0 / 4.0
board 프로젝트의 WebcContent/WEB-INF/lib 폴더에 파일을 복사한다.
2.1 커넥션 관련 코드
DB 연동을 하므로 커넥션 관련 코드를 작성해야 한다. 커넥션 관련코드는 앞서 공부했던 코드와 동일하며 커넥션 풀을 초기화 하기위한 DBCPInitListener 코드는 다음과 같다.
package jdbc;
import java.io.IOException;
import java.io.StringReader;
import java.sql.DriverManager;
import java.util.Properties;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.commons.dbcp2.ConnectionFactory;
import org.apache.commons.dbcp2.DriverManagerConnectionFactory;
import org.apache.commons.dbcp2.PoolableConnection;
import org.apache.commons.dbcp2.PoolableConnectionFactory;
import org.apache.commons.dbcp2.PoolingDriver;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
public class DBCPInitListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
// 583쪽 23~32줄
String poolConfig = sce.getServletContext().getInitParameter("poolConfig");
Properties prop = new Properties();
try {
prop.load(new StringReader(poolConfig));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("config load fail", e);
}
loadJDBCDriver(prop);
initConnectionPool(prop);
}
// 35~85줄
private void initConnectionPool(Properties prop) {
try {
String jdbcUrl = prop.getProperty("jdbcUrl");
String username = prop.getProperty("dbUser");
String pw = prop.getProperty("dbPass");
ConnectionFactory connFactory = new DriverManagerConnectionFactory(jdbcUrl, username, pw);
PoolableConnectionFactory poolableConnFactory = new PoolableConnectionFactory(connFactory, null);
String validationQuery = prop.getProperty("validationQuery");
if (validationQuery != null && !validationQuery.isEmpty()) {
poolableConnFactory.setValidationQuery(validationQuery);
}
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setTimeBetweenEvictionRunsMillis(1000 * 60 * 5);
poolConfig.setTestWhileIdle(true);
int minIdle = getIntProperty(prop, "minIdle", 5);
poolConfig.setMinIdle(minIdle);
int maxTotal = getIntProperty(prop, "maxTotal", 50);
poolConfig.setMaxTotal(maxTotal);
GenericObjectPool<PoolableConnection> connectionPool = new GenericObjectPool<>(poolableConnFactory,
poolConfig);
poolableConnFactory.setPool(connectionPool);
Class.forName("org.apache.commons.dbcp2.PoolingDriver");
PoolingDriver driver = (PoolingDriver) DriverManager.getDriver("jdbc:apache:commons:dbcp:");
String poolName = prop.getProperty("poolName");
driver.registerPool(poolName, connectionPool);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private int getIntProperty(Properties prop, String propName, int defaultValue) {
String value = prop.getProperty(propName);
if (value == null) {
return defaultValue;
}
return Integer.parseInt(value);
}
private void loadJDBCDriver(Properties prop) {
String driverClass = prop.getProperty("jdbcDriver");
try {
Class.forName(driverClass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new RuntimeException("fail to load JDBC Driver", e);
}
}
}
기존의 DBCPInitListener과 유사하지지만
- 커넥션 풀의 커넥션 검사쿼리
- 최소 유휴 커넥션
- 최대크기 설정 가능하도록 추가
- 최소 유휴커넥션 개수가 5로 고정되어 있던 반면 설정 프로퍼티 값을 사용한다.
2.1.1
DBCPInitListener는 서블릿 컨텍스트 리스너이므로 web.xml에 등록하자.
<listener>
<listener-class>jdbc.DBCPInitListener</listener-class>
</listener>
<context-param>
<param-name>poolConfig</param-name>
<param-value>
jdbcDriver=com.mysql.cj.jdbc.Driver
jdbcUrl=jdbc:mysql://localhost/board?serverTimezone=Asia/Seoul
dbUser=root
dbPass=1234
validationQuery=SELET 1
minIdle=3
maxTotal=30
poolName=board
</param-value>
</context-param>
2.1.2
커넥션을 구할 때 사용할 ConnectionProvider이다.
JDBC URL을 보면 web.xml에서 지정한 poolName 값인 board를 풀 이름으로 사용한 것을 알 수 있다.
package jdbc.connection;
import java.sql.*;
;
public class ConnectionProvider {
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:apache:commons:dbcp:board");
}
}
DB 연결이 올바르게 되는지 확인할 용도로 JSP 코드를 작성했다.
<%@page import="jdbc.connection.*"%>
<%@page import="java.sql.*"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
<script src="https://kit.fontawesome.com/a076d05399.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
<title>Insert title here</title>
</head>
<body>
<%
try (Connection conn = ConnectionProvider .getConnection()) {
out.println("커넥션 연결 성공");
} catch (SQLException e) {
out.println("커넥션 연결 실패 : " + e.getMessage());
application.log("커넥션 연결 실패", e);
}
%>
</body>
</html>
결과 :
2.1.3 커넥션 관련 코드 작성을 편리하게 하도록 jdbc.JdbcUtil 소스도 패키지에 복사한다.
package jdbc;
import java.sql.Connection;
public class JdbcUtil {
public static void close(AutoCloseable... resource) {
try {
for (AutoCloseable res : resource) {
res.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void rollback(Connection conn) {
if (conn != null) {
try {
conn.rollback();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3. 캐릭터 인코딩 필더 설정
package util;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
/**
* Servlet Filter implementation class SetRequestCharEncodingFilter
*/
//@WebFilter("/SetRequestCharEncodingFilter")
public class CharacterEncodingFilter implements Filter {
/**
* Default constructor.
*/
public CharacterEncodingFilter() {
// TODO Auto-generated constructor stub
}
/**
* @see Filter#destroy()
*/
public void destroy() {
// TODO Auto-generated method stub
}
/**
* @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// TODO Auto-generated method stub
// place your code here
request.setCharacterEncoding("utf-8");
// pass the request along the filter chain
chain.doFilter(request, response);
}
/**
* @see Filter#init(FilterConfig)
*/
public void init(FilterConfig fConfig) throws ServletException {
// TODO Auto-generated method stub
}
}
추가로 web.xml에 다음 코드를 복사한다.
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>util.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
4.1 MVC 컨트롤러 코드
MVC 컨트롤러를 위한 코드를 작성할 차례이다.
- mvc.command.CommandHandler
- mvc.command.NullHandler
- mvc.controller.ControllerUsingURI
4.2 web.xml에 ControllerUsingURI를 위한 설정을 추가한다.
<servlet>
<servlet-name>ControllerUsingURI</servlet-name>
<servlet-class>mvc.controller.ControllerUsingURI</servlet-class>
<init-param>
<param-name>configFile</param-name>
<param-value>
/WEB-INF/commandHandlerURI.properties
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ControllerUsingURI</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
아직까지 구현된 핸들러 코드는 없으므로 commandHandlerURI.propertie 파일은 설정정보를 담지 않은 빈 파일로 작성한다.
# comment
5.0 회원가입 기능 구현
회원 가입 기능의 명세는 다음과 같다.
- 회원 가입 요청을 하면 입력을 위한 폼을 보여준다.
- 입력 폼에 아이디, 이름, 암호, 암호 확인을 입력하고 전송하면 가입에 성공한다.
- 동일한 아이디를 가진 회원이 존재하면 에러 메시지와 함께 다시 폼을 보여준다.
- 입력한 암호와 암호 확인이 일치하지 않으면 에러 메시지와 함께 다시 폼을 보여준다.
이 기능을 구현하기 위해 구현할 코드는 다음과 같다.
각 코드의 역할은 다음과 같다.
JoinHandler : 사용자의 요청을 받는다. 사용자가 폼을 요청한 경우 폼을 보여준다. 폼데이터를 전송한 경우 joinService를 이용해서 회원 가입을 처리한다.
-joinForm.jsp : 회원 가입 폼을 보여준다.
-joinSuccess.jsp: 회원 가입 처리에 성공한 경우 결과를 보여준다.
JoinService : 회원 가입 기능을 구현한다.
-JoinRequest : 회원 가입할 때 필요한 데이터를 담는다. 폼에 입력한 값을 이 객체에 담아 joinService에 전달한다.
MemberDao : member 테이블과 관련된 쿼리를 실행한다.
Member : member 테이블과 관련된 클래스로서 회원 데이터를 담는다.
5.1 회원 정보 보관을 위한 DB 테이블과 관련 Member 클래스
앞서 생성한 board 데이터베이스에 member 테이블을 생성한다. 테이블 생성 쿼리는 다음과 같다.
create database board;
use board;
create table board.member(
memberid varchar(50) primary key,
name varchar(50) not null,
password varchar(10) not null,
regdate datetime not null
)
5.2 member 테이블의 데이터를 담을 때 사용할 Member 클래스는 다음과 같다.
package member.model;
import java.util.Date;
public class Member {
private String id;
private String name;
private String password;
private Date regDate;
public Member(String id, String name, String password, Date regDate) {
this.id = id;
this.name = name;
this.password = password;
this.regDate = regDate;
}
public boolean matchPassword(String pwd) {
return password.equals(pwd);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Date getRegDate() {
return regDate;
}
public void setRegDate(Date regDate) {
this.regDate = regDate;
}
}
5.3 MemberDao 구현
package member.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import jdbc.JdbcUtil;
import member.model.Member;
public class MemberDao {
public Member selectById(Connection conn, String id) throws SQLException {
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = conn.prepareStatement(
"SELECT * FROM member where memberid=?");
pstmt.setString(1, id);
rs = pstmt.executeQuery();
Member member = null;
if (rs.next()) {
member = new Member(
rs.getString("memberid"),
rs.getString("name"),
rs.getString("password"),
toDate(rs.getTimestamp("regdate"))
);
}
return member;
} finally {
JdbcUtil.close(rs, pstmt);
}
}
private Date toDate(Timestamp date) {
return date == null ? null : new Date(date.getTime());
}
public void insert(Connection conn, Member mem) throws SQLException {
try (PreparedStatement pstmt =
conn.prepareStatement("INSERT INTO member"
+ " VALUES (?, ?, ?, ?) ")) {
pstmt.setString(1, mem.getId());
pstmt.setString(2, mem.getName());
pstmt.setString(3, mem.getPassword());
pstmt.setTimestamp(4, new Timestamp(mem.getRegDate().getTime()));
pstmt.executeUpdate();
}
}
}
가입 기능을 구현하는 데 필요한 메서드만 추가했다. 회원 정보 수정 기능을 구현할 때 다른 메서드를 추가할 것이다.
5.4 JoinService와 JoinRequest구현
MemberDao를 이용해서 실제로 회원 가입 기능을 처리하는 코드를 만들어 보자.
먼저 만들 코드는 JoinRequest 클래스이다. JoinRequest 클래스는 JoinService가 회원 가입 기능을 구현할 때 필요한 요청 데이터를 담는 클래스로서 다음과 같다.
joinRequest
package member.service;
import java.util.Map;
public class JoinRequest {
private String id;
private String name;
private String password;
private String confirmPassword;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getConfirmPassword() {
return confirmPassword;
}
public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}
public boolean isPasswordEqualToConfirm() {
return password != null && password.equals(confirmPassword);
}
public void validate(Map<String, Boolean> errors) {
checkEmpty(errors, id, "id");
checkEmpty(errors, name, "name");
checkEmpty(errors, password, "password");
checkEmpty(errors, confirmPassword, "confirmPassword");
if (!errors.containsKey("confirmPassword")) {
if (!isPasswordEqualToConfirm()) {
errors.put("notMatch", true);
}
}
}
private void checkEmpty(Map<String, Boolean> errors, String value, String fieldName) {
if (value == null || value.isEmpty()) {
errors.put(fieldName, true);
}
}
}
joinService
package member.service;
import java.sql.Connection;
import java.util.Date;
import jdbc.JdbcUtil;
import jdbc.connection.ConnectionProvider;
import member.dao.MemberDao;
import member.model.Member;
public class JoinService {
private MemberDao memberDao = new MemberDao();
public void join(JoinRequest joinReq) {
Connection conn = null;
try {
conn = ConnectionProvider.getConnection();
conn.setAutoCommit(false);
Member member = memberDao
.selectById(conn, joinReq.getId());
if (member != null) {
JdbcUtil.rollback(conn);
throw new DuplicateIdException();
}
memberDao.insert(conn, new Member(
joinReq.getId(),
joinReq.getName(),
joinReq.getPassword(),
new Date()));
conn.commit();
} catch (Exception e) {
e.printStackTrace();
JdbcUtil.rollback(conn);
throw new RuntimeException(e);
} finally {
JdbcUtil.close(conn);
}
}
}
DuplicateIdException.java
package member.service;
public class DuplicateIdException extends RuntimeException {
}
5.5 JoinHandler와 jsp구현
JoinHandler는 다음과 같이 구현한다.
- Get 방식으로 요청이 오면 폼을 보여주는 뷰인 joinForm.jsp를 리턴한다.
- Post 방식으로 요청이 오면 회원 가입을 처리하고 결과를 보여주는 뷰를 리턴한다.
-입력 데이터가 잘못될 경우 다시 joinForm.jsp를 뷰로 리턴한다.
-회원 가입에 성공한 경우 joinSuccess.jsp를 뷰로 리턴한다.
MVC에서 CommandHandler를 구현한 클래스로 코드는 다음과 같다.
package member.command;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import member.service.DuplicateIdException;
import member.service.JoinRequest;
import member.service.JoinService;
import mvc.command.CommandHandler;
public class JoinHandler implements CommandHandler {
private static final String FORM_VIEW = "/WEB-INF/view/joinForm.jsp";
private JoinService joinService = new JoinService();
@Override
public String process(HttpServletRequest req,
HttpServletResponse res) {
if (req.getMethod().equalsIgnoreCase("GET")) {
return processForm(req, res);
} else if (req.getMethod().equalsIgnoreCase("POST")) {
return processSubmit(req, res);
} else {
res.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return null;
}
}
private String processSubmit(HttpServletRequest req,
HttpServletResponse res) {
JoinRequest joinReq = new JoinRequest();
joinReq.setId(req.getParameter("id"));
joinReq.setName(req.getParameter("name"));
joinReq.setPassword(req.getParameter("password"));
joinReq.setConfirmPassword(req.getParameter("confirmPassword"));
Map<String, Boolean> errors = new HashMap<>();
req.setAttribute("errors", errors);
joinReq.validate(errors);
if (!errors.isEmpty()) {
return FORM_VIEW;
}
try {
joinService.join(joinReq);
return "/WEB-INF/view/joinSuccess.jsp";
} catch (DuplicateIdException e) {
e.printStackTrace();
errors.put("duplicateId", true);
return FORM_VIEW;
}
}
private String processForm(HttpServletRequest req,
HttpServletResponse res) {
return FORM_VIEW;
}
}
processForm() 메서드는 데이터가 올바르지 않을 경우 errors 맵 객체에(키, TRUE) 쌍을 추가한다. 예를 들어 JoinRequest의 validate() 메서드는 데이터가 올바르지 않으면 다음과 같이 errors 맵 객체에 잘못된 데이터와 관련된 키를 추가했다.
중간 errors 맵 객체의 request의 "errors" 속성에 저장하는데 그 이유는 폼을 위한 jsp 코드에서 발생한 에러에 따라 알맞은 에러 메시지를 보여주기위함이다. 예를 들어 jsp 코드는 다음과 같은 코드를 이용해서 특정 에러가 발생하는 지 확인할 수 있다.
<c:if test ="${errors.name}">이름을 입력하세요. </c:if>
6. 폼을 보여줄 때 사용할 joinForm.jsp 코드는 다음과 같다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
<script src="https://kit.fontawesome.com/a076d05399.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
<title>Insert title here</title>
</head>
<body>
<form action="join.do" method="post">
<p>
아이디 : <br />
<input type="text" name="id" value="${param.id }"/>
<c:if test="${errors.id }">ID를 입력하세요.</c:if>
<c:if test="${errors.duplicateId }">이미 사용중인 아이디입니다.</c:if>
</p>
<p>
이름: <br />
<input type="text" name="name" value="${param.name }" />
<c:if test="${errors.name }">이름을 입력하세요.</c:if>
</p>
<p>
암호: <br />
<input type="password" name="password"/>
<c:if test="${errors.password }">암호를 입력하세요.</c:if>
</p>
<p>
확인: <br />
<input type="password" name="confirmPassword"/>
<c:if test="${errors.confirmPassword }">확인을 입력하세요.</c:if>
<c:if test="${errors.notMatch }">암호와 확인이 일치하지 않습니다.</c:if>
</p>
<input type="submit" value="가입" />
</form>
</body>
</html>