JSP/Servlet - 게시판 - 회원기능!

게시판 구축하기

지금까지 인증처리, 간단한 게시판 구축 등을 했는데 이번엔 진짜 회원가입부터 로그인까지 가능한 하나의 완전한 웹 어플리케이션을 만들어보자.

프로젝트명은 jspPro, 파일 위치는 board21라는 폴더와 패키지 안에 게시판 관련된 파일들을 생성하자.

DB연결

DB연결과정은 기존에 있던것을 사용하자.

/WEB-INF/lib 폴더에 ojdbc6.jar파일을 넣고 톰캣(WAS)서버가 제공하는 커넥션 풀 객체를 사용하자

public class ConnectionProvider {
	public static Connection getConncection() throws NamingException, SQLException
	{
		Context initContext = new InitialContext();
		Context envContext  = (Context)initContext.lookup("java:/comp/env");
		DataSource ds = (DataSource)envContext.lookup("jdbc/myoracle");
		Connection conn = ds.getConnection();
		return conn;
	}
}

https://kouzie.github.io/jsp/JSP-DBCP,-세션/

그리고 DB연결객체를 close, rollback할수 있도록 도와주는 JdbcUtil클래스 정의하도록 하자.

public class JdbcUtil {
	public static void close(ResultSet rs) {
		if (rs != null) {
			try { rs.close(); }
			catch (SQLException ex) { }
		}
	}
	
	public static void close(Statement stmt) {
		if (stmt != null) {
			try { stmt.close(); }
			catch (SQLException ex) { }
		}
	}
	
	public static void close(Connection conn) {
		if (conn != null) {
			try { conn.close(); }
			catch (SQLException ex) { }
		}
	}

	public static void rollback(Connection conn) {
		if (conn != null) {
			try { conn.rollback(); }
			catch (SQLException ex) { }
		}
	}
}

굳이 이런 클래스를 만든 이유는 서블릿이나 jsp에서 close할 때 항상 try, catch문으로 감싸주어야 했는데 이를 간략하기 위해 사용하는 클래스

https://kouzie.github.io/jsp/JSP-DBCP,-세션/#dbcp-database-connection-pool

먼저 회원가입을 위한 회원테이블을 생성하자.

CREATE TABLE member21 (
    memberid VARCHAR(50) PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    password VARCHAR(50) NOT NULL,
    regdate DATE DEFAULT SYSDATE
);

Member - Member21에서 사용자 데이터저장용 DTO 객체

테이블에서 가져온 데이터를 저장, 관리용 DTO객체를 정의하자.

Meber21테이블 속성에 해당하는 필드를 정의하고 getter, setter 메서드를 자동 생성.

추가적으로 회원가입시 패스워드와 확인용 패스워드가 일치하는지 검사하는 matchPassword메서드를 정의한다.

public class Member {
private String memberid;
private String name;
private String password;
private Date regdate;
public Member(String memberid, String name, String password, Date regdate) {
	this.memberid = memberid;
	this.name = name;
	this.password = password;
	this.regdate = regdate;
}
public String getMemberid() {
	return memberid;
}
public void setMemberid(String memberid) {
	this.memberid = memberid;
}
...
...
public boolean matchPassword(String pwd) {
	return password.equals(pwd);
}

MemberDao - 멤버 객체 삽입, 검색 클래스

실제 Membe21 테이블에 멤버를 추가하고 기존에 멤버가 있는지 검색하는 MemberDAO 객체를 정의한다.

public class MemberDao {

	public Member selectById(Connection conn, String memberid) {
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			pstmt = conn.prepareStatement("SELECT * FROM member21 WHERE memberid = ?");
			pstmt.setString(1, memberid);
			rs = pstmt.executeQuery();
			Member member = null;
			if (rs.next()) {
				member = new Member(
						rs.getString("memberid"),
						rs.getString("name"),
						rs.getString("password"),
						rs.getDate("regdate")
						);
				return member;
			}
		} catch (Exception e) {
			System.out.println(e);
		} finally {
			JdbcUtil.close(rs);
			JdbcUtil.close(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 (memberid, name, password) member21 value(?,?,?)");)
		{
			pstmt.setString(1, mem.getMemberid());
			pstmt.setString(2, mem.getName());
			pstmt.setString(3, mem.getPassword());
			
			pstmt.executeQuery();
		} catch (Exception e) {
			System.out.println(e);
		}
	}
}

회원가입을 위한 DTO, DAO객체를 만들었으니 이벤트 처리용 객체들을 만들어보자.

컨트롤러 정의

*.do확장자를 요청하는 모든 url은 컨트롤러의 제어를 받아 요청, 응답되어진다.

저번 url기반 컨트롤러 정의했던 것 과 전혀 달라진 것 이 없다.

https://kouzie.github.io/jsp/JSP-MVC패턴/#요청-uri명령-기반-컨트롤러

<servlet>
	<servlet-name>ControllerUsingURI</servlet-name>
	<servlet-class>board21.mvc.controller.ControllerUsingURI</servlet-class>
	<init-param>
		<param-name>configFile</param-name>
		<param-value>/WEB-INF/view/board21/commandHandler.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>
public class ControllerUsingURI extends HttpServlet{
	private Map<String, CommandHandler> commandHandlerMap = new HashMap<>();
	
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		process(request, response);
	}

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		process(request, response);
	}

	@Override
	public void init() throws ServletException {
		String configFile = getInitParameter("configFile");
		Properties prop = new Properties();
		String configFilePrath = getServletContext().getRealPath(configFile);
		System.out.println(configFile);
		try(FileInputStream fis = new FileInputStream(configFilePrath))
		{
			prop.load(fis);
		} catch (IOException e) {
			throw new ServletException(e);
		}
		Iterator<Object> keyiter = prop.keySet().iterator();
		while (keyiter.hasNext()) {
			String command = (String) keyiter.next();
			String handlerClassName = prop.getProperty(command);
			try {
				Class<?> handlerClass = Class.forName(handlerClassName);
				CommandHandler handlerInstance = (CommandHandler) handlerClass.newInstance();
				commandHandlerMap.put(command, handlerInstance);
			}
			catch (ClassNotFoundException | InstantiationException | IllegalAccessException  e) {
				throw new ServletException(e);
			}
		}
	}

	private void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String command = request.getRequestURI();
		if (command.indexOf(request.getContextPath()) == 0) {
			command = command.substring(request.getContextPath().length());
		}
		System.out.println(command);
		CommandHandler handler = commandHandlerMap.get(command);
		if (handler == null) {
			handler = new NullHandler(); //404에러를 응답하는 핸들러 클래스
		}
		String viewPage = null;
		try {
			viewPage = handler.process(request, response);
		}
		catch (Exception e) {
			throw new ServletException(e);
		}
		if(viewPage != null) {
			String prefix = "/WEB-INF/view/board21";
			viewPage = prefix + viewPage;
			RequestDispatcher dispatcher = request.getRequestDispatcher(viewPage);
			System.out.println(viewPage);
			dispatcher.forward(request, response);
		}
	}
}

서버 시작시 commandHandler.properties파일을 열어 url명령과 처리할 서블릿 객체를 Map객체를 통해 목록화 시켜 가지고 있고
명령이 들어올 때 마다 Map에서 알맞은 서블릿 객체를 꺼내 process(request, response)메더스들 호출한다.

그리고 서블릿 객체가 반환한 View페이지로 포워딩 시키면 끝!

viewPagenot null일 경우에만 포워딩 되기 때문에 null이 반환된 경우 아무것도 하지 않는다.(그냥 response객체를 반환할뿐….)

이벤트 처리 서블릿

commandHandler.properties설정 파일에 따라 이벤트가 처리되는데 회원가입의 경우 join.do라는 url을 서버에 요청한다.

위의 컨트롤러가 관리하는 이벤트 처리목록용 Map객체 안의 요소가 CommandHandler이다.

private Map<String, CommandHandler> commandHandlerMap = new HashMap<>();

CommandHandler는 모든 이벤트 처리용 서블릿 객체(Model)이 상속하는 인터페이스로 다형성을 활용하여 하나의 Map객체로 다양한 이벤트 처리 클래스를 관리할 수 있다.

public interface CommandHandler {
	public String process(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

아무런 *.do url패턴이지만 등록하지 않은 이벤트가 요청 될 수 있다.

위의 컨트롤러에서도 Map에서찾는 이벤트 객체가 없어 null을 반환했을 때 예외처리를 하였는데

NullHandler로 이벤트를 처리하는 것이다.

CommandHandler handler = commandHandlerMap.get(command);
if (handler == null) {
	handler = new NullHandler(); //404에러를 응답하는 핸들러 클래스
}

등록되지 않는 이벤트를 처리하는 Modal 객체인 NullHandler를 아래와 같이 정의하자.

public class NullHandler implements CommandHandler {
	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
		response.sendError(HttpServletResponse.SC_NOT_FOUND);
		return null;
	}
}

그냥 404에러를 response객체에 저장.

회원가입 기능 구현

DB 유틸과 컨트롤러를 위한 기본적인 객체들 정의가 끝났으니 회원가입 기능을 구현하도록 해보자.

이번 게시판 구축에서 MVC패턴은 모두 아래와 같은 형식으로 흘러간다.

Controlloer - Handler - service - dao(sql쿼리)

컨트롤러에 요청을 받아 요청에 맞는(url패턴으로 요청 구분) 핸든러의 처리메서드(process())를 호출한다.

Handler는 실직적으로 요청을 처리(sql문 실행)하는 service클래스를 만들어 각 기능을 가진 메서드를 호출한다.

왜 굳이 service객체를 중간에 끼워두는지 궁금하다면 아래 링크 참조

https://kouzie.github.io/jdbc/JDBC.-4일차/#게시판-mvc-패턴으로-구성하기

service클래스는 요청에 대한 응답정보(회원가입 성공/실패, 게시글 목록리스트 등)을 Handler에게 반환하고

Handler는 요청에 대한 응답데이터를 뿌려줄 View (응답데이터를 뿌릴 jsp) 주소를 Controller에게 반환한다.

Controller는 View 주소로 포워딩 시켜 클라이언트에게 jsp페이지로 안내한다.

회원가입, 로그인, 게시글목록보기 등 가릴 것 없이 모두 위와 같은 MVC패턴으로 진행시킨다.

회원가입(Join)역시 Controller - JoinHandler - JoinService - MemberDao 형식으로 진행한다.

JoinHandler - 회원가입 이벤트처리

이벤트 처리 클래스를 등록하기 위해 commandHandler.properties에 아래처럼 추가하자.
/join.do=board21.mvc.commmand.JoinHandler

이제 컨트롤러는 join.do라는 url패턴으로 요청이 들어오면 JoinHandler객체의 process()메서드를 호출하게 될 것이다.

아래 회원가입 링크를 누르면

<a href="<%= request.getContextPath() %>/join.do">회원가입</a>

a태그를 눌러 요청하는 것은 당연히 get방식으로 요청이 서버로 전송될 것이고 서버는 회원가입 페이지로 포워딩 시킨다.

회원가입 데이터를 입력하고 submit 버튼을 누르면 post방식으로 요청이 서버로 전송될 것이고 서버는 회원가입 결과페이지로 포워딩 시킨다.

모든것이 포워딩 작업으로 이루어지기 때문에 같은 join.do url로 get, post요청을 한다.

JoinHandlerget방식요청일 경우 joinForm.jsp페이지 url을 반환하고
post방식요청일 경우 joinSuccess.jsp페이지 url을 반환한다.

먼저 joinForm.jsp을 만들어보자.

<!-- 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" %>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8">
<title>회원가입</title>
</head>
<body>
<h3>JoinForm.jsp</h3>
<form action="join.do" method="post">
	<p>
		아이디: <input type="text" name="memberid" value="${ param.id }" />
		<c:if test="${ errors.memberid }">
			ID를 입력하세요
		</c:if>
		<c:if test="${ errors.duplicateId }">
			이미 사용중인 ID입니다.
		</c:if>
	</p>
	<p>
		이름: <input type="text" name="name" value="${ param.name }"/>
		<c:if test="${ errors.name }">
			이름을 입력하세요
		</c:if>
	</p>
	<p>
		암호: <input type="password" name="password" value="${ param.password }"/>
		<c:if test="${ errors.password }">
			암호를 입력하세요
		</c:if>
	</p>
	<p>
		확인: <input type="password" name="comfirmPassword" value="${ param.comfirmPassword }" />
		<c:if test="${ errors.confirmPassword }">
			확인을 입력하세요
		</c:if>
		<c:if test="${ errors.notMatch }">
			암호와 확인이 일치하지 않습니다.
		</c:if>
	</p>
	<input type="submit" value="가입" />
</form>
</body>
</html>

중간중간에 errors객체의 각종 속성을 검사하여 true일 경우 오류문구를 출력한다.

먼저 이 error객체에 설명하고 가면 Map<String, Boolean> errors = new HashMap<>(); 인런식으로 정의되어 있다.
key로 문자열 notMatch를, valueboolean값을 두어 각종 오류가 발생했는지 발생하지 않았는지 errors라는 Map객체 하나로 확인할 수 있다.

오늘 알게된 사실: 코어태그로 Map객체의 name에 해당하는 value도 가져올 수 있다.

image34

그럼 이제 가입결과 페이지 joinSuccess.jsp를 만들자.

<!-- joinSuccess.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입 - 결과</title>
</head>
<body>
	<h3>joinSuccess.jsp</h3>
	${ param.name }님, 가입을 환영합니다.<br>
</body>
</html>

사실 회원가입 실패의 경우 errors객체를 가지고 joinForm.jsp로 포워딩 시킬 것이기 때문에 성공할 경우에만 joinSuccess.jsp로 이동시킨다.

JoinHandler Model객체는 get방식일때, 혹은 에러가 발생했을 경우 joinForm.jsp문자열을 반환하고
post방식일 때 joinSuccess.jsp 문자열을 반환한다.

public class JoinHandler implements CommandHandler{

	private static final String FORM_VIEW = "/joinForm.jsp";
	
	private JoinService joinService = new JoinService();
	
	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (request.getMethod().equalsIgnoreCase("GET")) {
			return processForm(request, response);
		}
		else if (request.getMethod().equalsIgnoreCase("POST")) {
			return processSubmit(request, response);
		}
		else {
			response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
			return null;
		}
	}

	private String processForm(HttpServletRequest request, HttpServletResponse response) {
		return FORM_VIEW;
	}
	private String processSubmit(HttpServletRequest request, HttpServletResponse response) {
		JoinRequest joinReq = new JoinRequest();
		//받은 요청을 유효한 값인지 확인하는 JoinRequest 객체
		joinReq.setMemberid(request.getParameter("memberid"));
		joinReq.setName(request.getParameter("name"));
		joinReq.setPassword(request.getParameter("password"));
		joinReq.setConfirmPassword(request.getParameter("comfirmPassword"));
		Map<String, Boolean> errors = new HashMap<>();
		request.setAttribute("errors", errors);
		joinReq.validate(errors);

		if (!errors.isEmpty()) {
			//errors안에 값이 있다면
			return FORM_VIEW;
		}
		try {
			joinService.join(joinReq);
			//INSERT작업 수행
			return "/joinSuccess.jsp";
		} catch (DuplicationException e) {
			errors.put("duplicateId", Boolean.TRUE);
			return FORM_VIEW;
		}
	}
}

살펴보아야 할것은 post방식에서 호출되는 processSubmit()메서드

JoinRequest객체는 사용자가 joinForm.jsp에서 입력할 값들이 유효한지 확인하는 객체이다.
validate()메서드를 통해 errors객체에 유효한지 결과를 추가한다.

public class JoinRequest {
	private String memberid;
	private String name;
	private String password;
	private String confirmPassword;
	
	public boolean isPasswordEqualToConfirm()
	{
		return password != null && password.equals(confirmPassword);
	}
	public void validate(Map<String, Boolean> errors) {
		checkEmpty(errors, memberid, "memberid");
		checkEmpty(errors, name, "name");
		checkEmpty(errors, password, "password");
		checkEmpty(errors, confirmPassword, "confirmPassword");
		//비어있거나 null값이면 FALSE, 
		//comfirmPassWord가 TRUE, 서로 일치한다면 notMatch에 TRUE 
		if (!errors.containsKey("confirmPassword")) {
			if(!isPasswordEqualToConfirm()) {
				errors.put("notMatch", Boolean.TRUE);
			}
		}
	}
	
	private void checkEmpty(Map<String, Boolean> errors, String value, String fieldName) {
		if (value == null || value.isEmpty()) {
			errors.put(fieldName, Boolean.TRUE);
		}
	}
	public String getMemberid() {
		return memberid;
	}
	public void setMemberid(String memberid) {
		this.memberid = memberid;
	}
	...
	...
}

JoinRequestMember DTO객체와 유사한데 회원가입을 위한 DTO라고 볼수 있다.

회원데이터는 회원가입 뿐 아니라 다방면에서 쓰여야 하는데 회원가입 때문에 여러 메서드를 Member DTO객체에 주렁주렁 달고 있을 필요는 없기 때문에 JoinRequest를 별도로 생성한것.

그리고 JoinRequest객체에서 ID를 SELECT해와서 ID중복체크를 하지 않는데 중복체크는 joinService.join() 메서드에서 발생한다.

다시 JoinHandlerprocessSubmit()메서드를 살펴보면 joinService.join()메서드에서 중복예외객체인 DuplicationException를 throw한다.

/* JoinHandler */
...
try {
	joinService.join(joinReq);
	//INSERT작업 수행
	return "/joinSuccess.jsp";
} catch (DuplicationException e) {
	errors.put("duplicateId", Boolean.TRUE);
	return FORM_VIEW;
}
...

실제 JoinServiceMemberDAO 사용해 INSERT 쿼리를 수행하기 전 SELECT쿼리를 수행해 중복체크를 하는데
만약 클라이언트가 넘긴 ID값이 DB에 이미 존재한다면 직접 정의한 예외 DuplicationException객체가 throw된다.

public class DuplicationException extends RuntimeException{
}

그냥 구분하기 위해 만들어둔 깡통 예외객체…

JoinService INSERT하기 전 memberDao.selectById()메서드를 통해 이미 ID가 존재하는지 체크하고 DuplicationException예외를 throw할지 말지 결정한다.

ID가 존재하지 않을경우 memberDao.insert()메서드로 입력받은 JoinRequest객체로 Member DTO객체를 생성하여 INSERT하고 끝

public class JoinService {
	private MemberDao memberDao = new MemberDao();
	public void join(JoinRequest joinReq)
	{
		Connection conn = null;
		try {
			conn = ConnectionProvider.getConncection();
			conn.setAutoCommit(false);
			//자동 커밋 X
			Member member = memberDao.selectById(conn, joinReq.getMemberid());
			if (member != null) {
				// 이미 MemberId값이 존재한다면
				JdbcUtil.rollback(conn);
				throw new DuplicationException();
			}
			memberDao.insert(conn, new Member(
					joinReq.getMemberid(),
					joinReq.getName(),
					joinReq.getPassword(),
					new Date()
					));
			conn.commit();
		} catch (SQLException | NamingException e) {
			System.out.println("joinSerivce");
			JdbcUtil.rollback(conn);
			throw new RuntimeException(e);
		} finally {
			JdbcUtil.close(conn);
		}
	}
}

회원가입 요약

commandHandler.propertiesJoinHandler 등록

get방식일 때 joinForm.jsp반환, post방식일 때 processSubmit()메서드 호출

processSubmit()에서 errors객체로 유효값 확인 및 SQL쿼리 수행

joinServiec에서 SELECT쿼리 수행 후 ID가 이미 존재한다면 DuplicationException예외 발생

ID중복 없을경우 INSERT쿼리 수행

INSERT쿼리까지 수행되면 JoinHandlerjoinSuccess.jsp 반환.

image35

로그인 기능 구현

회원가입을 끝냈으니 로그인 기능을 구현해보자.

로그인을 진행하기 전에 메인페이지인 index.jsp를 소개하겠다.

<!-- index.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>INDEX</title>
</head>
<body>
<h3>index.jsp</h3>
<c:if test="${ !empty authUser }">
	${ authUser.name }님 안녕하세요 
	<br>
	<a href="<%= request.getContextPath() %>/logout.do">로그아웃</a>
	<a href="<%= request.getContextPath() %>/changePwd.do">비밀번호 변경</a>
</c:if>
<c:if test="${ empty authUser }">
	<a href="<%= request.getContextPath() %>/join.do">회원가입</a>
	<a href="<%= request.getContextPath() %>/login.do">로그인</a>
	<a href="<%= request.getContextPath() %>/changePwd.do">비밀번호 변경</a>
</c:if>
</body>
</html>

image37

아직 로그인 하지 않았기 때문에 로그아웃은 출력되지 않는다.

index.jsp역시 /WEB-INF/view/board21 폴더 안에 넣을 것 이기 때문에 핸들러가 포워딩을 시켜주어야 index.jsp로 이동 가능하다.

포워딩 시켜주는 IndexHandlerController에 등록시키고 이벤트 처리시키도록 설정하다.

index.do라는 url패턴이 요청되면 index.jsp로 포워딩 시키는 핸들러이다.

public class IndexHandler implements CommandHandler {

	private static final String FORM_VIEW = "/index.jsp";
	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
		return FORM_VIEW;
	}
}

commandHandler.properties파일에 아래와 같이 추가

/index.do=borad21.command.IndexHandler

LoginHandler - 로그인 이벤트 처리 Model

로그인 기능 역시 회원가입과 같은 방향으로 흘러갈 것이다.

이번엔 먼저 로그인 전체과정을 보고 시작하자.
image36

ControllerLoginHandler를 호출하고 get, post방식에 따라 loginForm.jsp로 바로 이동시킬 것 인지, LoginService객체의 login과정을 진행시킬 것 인지 결정한다.

LoginService의 로그인 과정에선 ID, PW가 일치하는지 검사하고 일치한다면 User객체를 request객체에 저장하여 index.jsp

로그인 유지는 session으로 유지할 것이다.

로그인한다면 authUser라는 세션속성에 인증관련 값을 보관, 관련값은 User라는 객체… 로그아웃시 세션을 종료시킨다.

commandHandler.properties파일에 아래와 같이 추가
/login.do=board21.auth.command.LoginHandler

public class LoginHandler implements CommandHandler{

	private static final String FORM_VIEW = "/loginForm.jsp";
	private LoginService loginService = new LoginService();
	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (request.getMethod().equalsIgnoreCase("GET")) {
			System.out.println("LoginHandler preocess GET");
			return processForm(request, response);
		}
		else if (request.getMethod().equalsIgnoreCase("POST")) {
			System.out.println("LoginHandler preocess POST");
			return processSubmit(request, response);
		}
		else {
			response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
			return null;
		}
	}
	private String processForm(HttpServletRequest request, HttpServletResponse response) {
		return FORM_VIEW;
	}
	...
	...

여기까지는 다른 핸들러 객체들과 다른점이 없다.

맨 처음 get방식으로 login.do url패턴이 요청되면 아래 jsp페이지로 포워딩한다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
<h3>loginForm.jsp</h3>
<form action="/jspPro/login.do" method="post">
	<c:if test="${ errors.idOrPwNotMatch }">
		아이디와 암호가 일치하지 않습니다.
	</c:if>
	<p>
		아이디: <input type="text" name="id" value="${ param.id }"/>
		<c:if test="${ errors.id }">ID를 입력하세요</c:if>
	</p>
	<p>
		암호: <input type="password" name="password"/>
		<c:if test="${ errors.password }">암호를 입력하세요</c:if>
	</p>
	<input type="submit" value="로그인" />
</form>
</body>
</html>

image38

이 상태에서 idpassword를 입력후 submit버튼을 누르면 processSubmit호출과정에서 입력받은 id, password가 유효한 값인지 확인하고

loginService.login(memberid, password)를 통해 맞는 idpassword인지 확인하면 된다.

	...
	...
	private String processSubmit(HttpServletRequest request, HttpServletResponse response) {
		String memberid = trim(request.getParameter("id"));
		String password = trim(request.getParameter("password"));
		Map<String, Boolean> errors = new HashMap<>();
		request.setAttribute("errors", errors);
		if (memberid == null || memberid.isEmpty()) {
			errors.put("id", Boolean.TRUE);
		}
		if (password == null || password.isEmpty()) {
			errors.put("password", Boolean.TRUE);
		}
		if (!errors.isEmpty()) {
			return FORM_VIEW;
		}
		try {
			User user = loginService.login(memberid, password);
			request.getSession().setAttribute("authUser", user);
			response.sendRedirect(request.getContextPath()+"/index.do");
			return null;
		} catch (LoginFailException e) {
			errors.put("idOrPwNotMatch", Boolean.TRUE);
			return FORM_VIEW;
		} catch (IOException e) {
			e.printStackTrace();
			return FORM_VIEW;
		}
	}
	private String trim(String str) {
		return str == null ? null : str.trim();
	}
}

위 jsp페이지의 오류 확인을 위해 여기서도 errors객체를 초기화한다.

LoginService - 실제 SELECT처리 호출 클래스

LoginServicelogin메서드를 통해 인증객체인 User를 가져오는데
MemberDAOselectById메서드를 통해 Member객체를 가져오고
Member객체로 User객체를 초기화한다.

public class LoginService {
	private MemberDao memberDAO = new MemberDao();
	
	public User login(String id, String password) throws LoginFailException
	{
		System.out.println("login");
		try(Connection conn = ConnectionProvider.getConncection();)
		{
			Member member = memberDAO.selectById(conn, id);
			if (member == null) {
				System.out.println("login faile");
				throw new LoginFailException();
			}
			if (!member.matchPassword(password)) {
				System.out.println("login password failed");
				throw new LoginFailException();
			}
			return new User(member.getMemberid(), member.getName());
		} catch (SQLException | NamingException e) {
			throw new RuntimeException(e);
		}
	}
}

selectById()안의 SELECT쿼리로 가져온 값이 없다면 LoginFailException예외를 LoginHandlerthrow하여 loginForm.jsp로 다시 포워딩 된다.

로그인 과정은 SELECT문 하나로 Member가 있는지 없는지만 검사하면 되기 때문에 회원가입 과정보다 더 쉽다.

다른 직접 정의한 예외처럼 LoginFailException역시 깡통 예외객체이다.

public class LoginFailException extends RuntimeException {
}

비밀번호 때문에 틀렸는지, ID 때문인지 ERRCODE를 필드로 갖는 것 도 나쁘지 않다.

인증 객체 User는 다음과 같다.

public class User {
	private String id;
	private String name;
	public User(String id, String name) {
		this.id = id;
		this.name = name;
	}
	public String getId() {
		return id;
	}
	public String getName() {
		return name;
	}
}

LoginHandler에서 login과정이 예외발생 없이 무사히 끝나면 인증객체를 request에 저장한후 index.do로 리다이렉션 수행된다.

/* LoginHandler.processSubmit() */
...
User user = loginService.login(memberid, password);
request.getSession().setAttribute("authUser", user);
response.sendRedirect(request.getContextPath()+"/index.do");
...

이번엔 당연히 인증객체인 authUser가 존재함으로 환영문구와 로그아웃, 비밀번호 변경 링크가 출력된다.

image39

로그아웃 처리하기

로그아웃 링크는 아래와 같다.

<a href="<%= request.getContextPath() %>/logout.do">로그아웃</a>

.../logout.do를 컨트롤러에 요청하기 때문에 새로운 핸들러 등록을 해주자.

commandHandler.properties에 아래와 같이 등록 /logout.do=board21.auth.command.LogoutHandler

로그아웃의 경우 세션만 삭제하면 되기 때문에 DB연결 작업이 필요 없다.

public class LogoutHandler implements CommandHandler{

	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) throws IOException {
		HttpSession session = request.getSession();
		if (session != null) {
			session.invalidate();
		}
		response.sendRedirect(request.getContextPath()+"/index.do");
		return null;
	}
}

session.invalidate()를 통해 세션을 삭제하고 index.do로 리다이렉트 시킨다.

비밀번호 변경 기능 구현

index.jsp에서 비밀번호 변경을 위해 아래와 같은 링크를 추가했다.

<a href="<%= request.getContextPath() %>/changePwd.do">비밀번호 변경</a>

.../changePwd.do이벤트를 commandHandler.properties파일에 새로 등록하고

/changePwd.do=board21.member.command.ChangePasswordHandler

이벤트 처리용 객체 ChangePasswordHandler, ChangePasswordService 를 만들고
MemberDao에도 비밀번호 변경을 위한 update()메서드를 구현해야 한다.

LoginCheckFilter - 로그인 하였는지 검사하는 필터

비밀번호 변경, 글 쓰기 등과 같은 작업을 할 때 먼저 로그인 했는지 체크해야 한다.

<filter>
	<filter-name>loginCheckFilter</filter-name>
	<filter-class>board21.filter.LoginCheckFilter</filter-class>
</filter>
<filter-mapping>
	<filter-name>loginCheckFilter</filter-name>
	<url-pattern>/changePwd.do</url-pattern>
</filter-mapping>

web.xml에 필터를 등록하고 LoginCheckFilter 클래스 정의

public class LoginCheckFilter implements Filter {

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		System.out.println("LoginCheckFilter doFilter");
		HttpServletRequest filterRequest = (HttpServletRequest) request;
		HttpSession session = filterRequest.getSession();
		
		if (session == null || session.getAttribute("authUser") == null) {
			HttpServletResponse filterResponse = (HttpServletResponse) response;
			session.setAttribute("nextLink", filterRequest.getRequestURI());
			System.out.println("doFilter nextLink: " + filterRequest.getRequestURI());
			filterResponse.sendRedirect(filterRequest.getContextPath() + "/login.do");
		}
		else
			chain.doFilter(request, response);
	}
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
	}
	@Override
	public void destroy() {
	}
}

만약 session값이 아예 없거나 authUser라는 속성값이 session에 없다면 /jspPro/login.do로 리다이렉트 시킨다.

sessionnextLink로 기존에 접근하려 했던 url (비밀번호 변경이나 글쓰기 등)를 백업해 두고

따라서 LoginHandler클래스에도 index.do로 바로 가지 않고 sessionnextLink속성이 있으면 거기로 이동하도록 설정하자.

/* LoginHandler.processSubmit() */
...
...
String nextLink = (String) request.getSession().getAttribute("nextLink");
System.out.println("nextLink: " + nextLink);
if (nextLink == null || nextLink.isEmpty()) {
	nextLink = "/jspPro/index.do";
}
response.sendRedirect(nextLink);
return null;
...
...

ChangePasswordHandler - 비밀번호 변경 핸들러

어째건 비밀번호 변경 링크를 클릭히 .../changePwd.do url을 요청한다면 컨트롤러에 의해 ChangePasswordHandler객체가 이벤트를 처리한다.

image40

전체적 처리는 위 그림과 같다.

Handler에 get, post방식으로 요청이 들어오면

get방식일 경우 changePasswordForm.jsp으로 포워딩 시켜 현재비밀번호, 바꿀 비밀번호를 입력받고

post방식일 경우 현재 비밀번호를 체크, 바꿀 비밀번호로 update 시킨후 changePasswordSuccess.jsp로 이동시킨다.

<!-- changePasswordSuccess.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비밀번호 변경 완료</title>
</head>
<body>
<h3>changePwdSuccess.jsp</h3>
암호를 변경했습니다.
<a href="/jspPro/index.do">home</a>
</body>
</html>

image42

<!-- changePasswordForm.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비밀번호 변경</title>
</head>
<body>
<h3>changePwdForm.jsp</h3>
<form action="/jspPro/changePwd.do" method="post">
	<p>
		현재암호: <input type="password" name="curPwd" />
		<c:if test="${ errors.curPwd }">현재 암호를 입력하세요</c:if>
		<c:if test="${ errors.badCurPwd }">현재 암호가 일치하지 않습니다</c:if>
	</p>
	<p>
		새 암호: <input type="password" name="newPwd" />
		<c:if test="${ errors.newPwd }">현재 암호를 입력하세요</c:if>
	</p>
	<input type="submit" value="암호 변경"/>
</form>
</body>
</html>

image41

errors 객체가 있는 것으로 봐서 각 input태그에 미입력, 현재 비밀번호가 틀릴경우 오류를 출력하는 것을 알 수 있다.

public class ChangePasswordHandler implements CommandHandler{

	private static final String FORM_VIEW = "/changePwdForm.jsp";
	private ChangePasswordService changePwdSvc = new ChangePasswordService();
	
	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (request.getMethod().equalsIgnoreCase("GET")) {
			System.out.println("ChangePasswordHandler preocess GET");
			return processForm(request, response);
		}
		else if (request.getMethod().equalsIgnoreCase("POST")) {
			System.out.println("ChangePasswordHandler preocess POST");
			return processSubmit(request, response);
		}
		else {
			response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
			return null;
		}
	}
	private String processForm(HttpServletRequest request, HttpServletResponse response) throws IOException {
		return FORM_VIEW;
	}
	private String processSubmit(HttpServletRequest request, HttpServletResponse response) throws IOException {
		User user = (User) request.getSession().getAttribute("authUser");
		Map<String, Boolean> errors = new HashMap<>();
		request.setAttribute("errors", errors);
		
		String curPwd = request.getParameter("curPwd");
		String newPwd = request.getParameter("newPwd");
		if (curPwd == null || curPwd.isEmpty()) {
			errors.put("curPwd", Boolean.TRUE);
		}
		if (newPwd == null || newPwd.isEmpty()) {
			errors.put("newPwd", Boolean.TRUE);
		}
		if (!errors.isEmpty()) {
			return FORM_VIEW;
		}
		try {
			changePwdSvc.changePassword(user.getId(), curPwd, newPwd);
			return "/changePwdSuccess.jsp";
		}
		catch (InvalidPasswordException e) {
			System.out.println("InvalidPasswordException");
			errors.put("badCurPwd", Boolean.TRUE);
			return FORM_VIEW;
		} catch (MemberNotFoundException e) {
			System.out.println("MemberNotFoundException");
			response.sendError(HttpServletResponse.SC_BAD_REQUEST);
			return null;
		}
	}
}

ChangePasswordHandlerprocessSubmit가 약간 다를뿐 회원가입이나, 로그인 과정과 다를 것 이 없다.

입력값이 유효한지 확인하고 유효하지 않다면 오류를 담은 Map객체를 가지고 FORM_VIEW로 다시 이동시키고

입력값이 모두 유효하다면 changePwdSvc.changePassword(user.getId(), curPwd, newPwd)메서드를 호출한다.

만약 현재비밀번호가 일치하지 않는다면 InvalidPasswordException라는 예외를 throw

만약 session에서 가져온 authUser속성의 User객체안 id값이 DB에 없는 id라면 MemberNotFoundException라는 예외를 throw한다.

두 예외 모두 직접 정의한 RunableException을 상속하는 예외객체이다.

ChangePasswordService - 비밀번호 변경 서비스

실직적으로 MemberDao안의 각종 쿼리를 수행하여 ID가 있는지, 기존 PW가 입력한 PW와 일치하는지 검사후
MemberDaoupdate(conn, member)메서드를 호출한다.

ChangePasswordServicechangePassword메서드는 아래와 같다.

public class ChangePasswordService {
	private MemberDao memberDao = new MemberDao();
	
	public void changePassword(String userId, String curPwd, String newPwd)
	{
		Connection conn = null;
		try {
			conn = ConnectionProvider.getConncection();
			conn.setAutoCommit(false);
			
			Member member = memberDao.selectById(conn, userId);
			if (member == null) {
				throw new MemberNotFoundException();
			}
			if (!member.matchPassword(curPwd)) {
				throw new InvalidPasswordException();
			}
			member.changePassword(newPwd);
			memberDao.update(conn, member);
			conn.commit();
		}
		catch (SQLException | NamingException e) {
			JdbcUtil.rollback(conn);
			throw new RuntimeException();
		}
		finally {
			JdbcUtil.close(conn);
		}
	}
}

새로 만든 update메서드는 Member객체를 매개변수로 받아 UPDATE sql쿼리를 수행하는 메서드이다.

public void update(Connection conn, Member member) throws SQLException 
{
try (PreparedStatement pstmt = conn.prepareStatement(
		"UPDATE member21 SET name = ?, password = ? WHERE memberid = ?"
		))
	{
		pstmt.setString(1, member.getName());
		pstmt.setString(2, member.getPassword());
		pstmt.setString(3, member.getMemberid());
		pstmt.executeUpdate();
	}
}

카테고리:

업데이트: