본문 바로가기
Java/Servlet & JSP

JSP) 웹 페이지 로그인 기능 구현하기

by 박채니 2022. 6. 21.

안녕하세요, 코린이의 코딩 학습기 채니 입니다.

 

개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.


전체적인 디렉토리 구조는 아래 포스팅 참고!

https://chanychu.tistory.com/307?category=980487 

 

JSP) SQL과 함께 사용하기 (접속 테스트 및 파일 구조)

안녕하세요, 코린이의 코딩 학습기 채니 입니다. 개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다. index.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding..

chanychu.tistory.com


웹페이지의 로그인 기능 구현하기

 

header.jsp

tabindex

- 탭 순서 변경하기

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello MVC</title>
<link rel="stylesheet" href="<%=request.getContextPath() %>/css/style.css" />
</head>
<body>
<div id="container">
<header>
    <h1>Hello MVC</h1>
    <div class="login-container">
        <!-- 로그인폼 시작 -->
        <form id="loginFrm" name="loginFrm" action="<%= request.getContextPath()%>/member/login" method="POST">
            <table>
                <tr>
                    <td><input type="text" name="memberId" id="memberId" placeholder="아이디" tabindex="1"></td>
                    <td><input type="submit" value="로그인" tabindex="3"></td>
                </tr>
                <tr>
                    <td><input type="password" name="password" id="password" placeholder="비밀번호" tabindex="2"></td>
                    <td></td>
                </tr>
                <tr>
                    <td colspan="2"><input type="checkbox" name="saveId" id="saveId" /> 
                    <label for="saveId">아이디저장</label>&nbsp;&nbsp; 
                    <input type="button" value="회원가입"></td>
                </tr>
            </table>
        </form>
        <!-- 로그인폼 끝-->
    </div>
    <!-- 메인메뉴 시작 -->
    <nav>
        <ul class="main-nav">
            <li class="home"><a href="<%= request.getContextPath() %>">Home</a></li>
            <li class="notice"><a href="#">공지사항</a></li>
            <li class="board"><a href="#">게시판</a></li>
        </ul>
    </nav>
    <!-- 메인메뉴 끝-->
</header>

<section id="content">

tabindex 속성을 이용하여 로그인 폼의 탭 순서를 변경해주었으며, post 방식으로 데이터를 전송해주었습니다.

 

 

아이디와 비밀번호에 대한 유효성 검사

- 유효성 검사를 통과한 데이터만 서버로 전송

<script>
window.onload = () => {
	document.loginFrm.onsubmit = (e) => {
		const memberId = document.querySelector("#memberId");
		const password = document.querySelector("#password");
		
		if(!/^.{4,}$/.test(memberId.value)) {
			alert("유효한 아이디를 입력해주세요.");
			memberId.select();
			return false;
		}
		
		if(!/^.{4,}$/.test(password.value)) {
			alert("유효한 비밀번호를 입력해주세요.");
			password.select();
			return false;
		}
	};
};
</script>

mvc 패키지 구조

 

DTO (Data Transfer Object)

- 데이터를 전송하는 클래스

Member class

package com.ce.mvc2.member.model.dto;

import java.sql.Date;
import java.sql.Timestamp;

public class Member {
	private String memberId;
	private String password;
	private String memberName;
	private MemberRole memberRole;	// U/A
	private Gender gender;	// M/F
	private Date birthday;	// 날짜
	private String email;
	private String phone;
	private String hobby;
	private int point;
	private Timestamp enrollDate;	// 날짜, 시각
	
	public Member() {
		super();
		// TODO Auto-generated constructor stub
	}

	public Member(String memberId, String password, String memberName, MemberRole memberRole, Gender gender,
			Date birthday, String email, String phone, String hobby, int point, Timestamp enrollDate) {
		super();
		this.memberId = memberId;
		this.password = password;
		this.memberName = memberName;
		this.memberRole = memberRole;
		this.gender = gender;
		this.birthday = birthday;
		this.email = email;
		this.phone = phone;
		this.hobby = hobby;
		this.point = point;
		this.enrollDate = enrollDate;
	}
// getter, setter, toString 생략
}

memberRole과 gender는 enum으로 생성!


enum (상수클래스)

- namespace를 가진 상수 모음

- public static final보다 훨씬 안전

package com.ce.mvc2.member.model.dto;

public enum MemberRole {
	U, A;
}

 

public enum Gender {
	M, F;
}

memberRole과 gender에는 MemberRole.A/U와 Gender.M/F만 올 수 있습니다.

 

JdbcTemplate 생성

이전에 jdbc 공부를 하면서 만들어놨던 템플릿을 사용하려고 합니다.

public class JdbcTemplate {
	static String driverClass;
	static String url; // db접속프로토콜@ip:포트:db명(sid)
	static String user;
	static String password;
	
	static {
		// datasource.properties의 내용을 Properties 객체로 불러오기
		Properties prop = new Properties();
		try {
			// buildpath (/WEB-INF/classes) 하위에 있는 datasource.properties의 절대경로
			// getResource 메소드에 전달된 path의 /는 /WEB-INF/classes를 의미
			String filename = JdbcTemplate.class.getResource("/datasource.properties").getPath();
			System.out.println("filename@JdbcTemplate = " + filename);
			prop.load(new FileReader(filename));
			driverClass = prop.getProperty("driverClass");
			url = prop.getProperty("url");
			user = prop.getProperty("user");
			password = prop.getProperty("password");
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		
		// driver class 등록 - application 실행 시 최초 1회만!
		try {
			Class.forName(driverClass);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Connection 객체 생성
	 * setAutoCommit(false) - 트랙잭션을 직접 관리 (DQM, DML)
	 */
	public static Connection getConnection() {
		Connection conn = null;
		try {
			conn = DriverManager.getConnection(url, user, password);
			conn.setAutoCommit(false);
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return conn;
	}
	
	public static void close(Connection conn) {
		try {
			if(conn != null && !conn.isClosed())
				conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void close(PreparedStatement pstmt) {
		try {
			if(pstmt != null && !pstmt.isClosed())
				pstmt.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void close(ResultSet rset) {
		try {
			if(rset != null && !rset.isClosed())
				rset.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void commit(Connection conn) {
		try {
			//conn이 null이 아니고, 닫혀있지 않다면 커밋처리
			if(conn != null && !conn.isClosed())	// isClosed() : 반환되었다면 true리턴
				conn.commit();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	public static void rollback(Connection conn) {
		try {
			if(conn != null && !conn.isClosed())
				conn.rollback();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}

현재 클래스의 클래스 객체를 가져와서 (class()) buildpath에 "/datasource.properties"파일의 URL객체를 반환!(getResource()) 후 절대 경로 (getPath())를 가져와줍니다.

 

datasource.properties

###################################################
# datasource.properties
###################################################
driverClass = oracle.jdbc.OracleDriver
url = jdbc:oracle:thin:@localhost:1521:xe
user = web2
password = 비밀번호

중요한 정보들을 따로 datasource 파일로 빼놓았었습니다.

 

해당 파일을 가져와서 사용해야 하는데, 프로젝트에 source folder를 추가해줍니다. (폴더이름 - src/main/resources)

웹 프로젝트를 실행하는 것이기 때문에 tomcat 밑에서 프로젝트를 실행하게 됩니다.

따라서 tomcat 밑에서 실행할 때는 모든 파일들이 src/main/webapp 밑에 존재해야 합니다.

Navigator

buildpath인 classes 디렉토리에는 실제 src/main/java의 컴파일된 파일들의 디렉토리도 확인되며, src/main/resource에 있는 properties 파일도 확인됩니다.

source folder는 source folder 하위에 내용을 buildpath로 반영해줍니다. (따라서 tomcat에서 참조하여 실행 가능!)

즉, 실제 작업 파일 (working file)은 src/main/resources/datasource.properties이지만, 실행 파일(running file)은 src/main/webapp/WEB-INF/classes/datasource.properties입니다.

 

member-query.properties

- 쿼리 관리

#######################################
# member-query.properties
#######################################
findById = select * from member where member_id = ?

sql folder - member folder

필요한 파일들과 클래스들을 모두 생성했다면, 데이터를 가져와보도록 하겠습니다.


DQL

로그인 여부 판단

 

Controller

MemberLoginServlet

@WebServlet("/member/login")
public class MemberLoginServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private MemberService memberService = new MemberService();

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 1. 인코딩 처리
		request.setCharacterEncoding("utf-8");
		
		// 2. 사용자 입력 값 처리
		String memberId = request.getParameter("memberId");
		String password = request.getParameter("password");
		System.out.println("memberId = " + memberId);
		System.out.println("passowrd = " + password);
		
		// 3. 업무로직 : 로그인 여부 판단
		Member member = memberService.findById(memberId);
		System.out.println("member@MemberLoginServlet = " + member);
		
		
		// 4. 응답 처리
		RequestDispatcher reqDispatcher = request.getRequestDispatcher("/index.jsp");
		reqDispatcher.forward(request, response);
	}
}

 

Service

MemberService

public class MemberService {
	private MemberDao memberDao = new MemberDao();
	
	/**
	 * DQL 요청 - service
	 * 1. Connection 객체 생성
	 * 2. Dao 요청 & Connection 전달
	 * 3. Connection 반환 (close)
	 */
	public Member findById(String memberId) {
		Connection conn = getConnection();
		Member member = memberDao.findById(memberId, conn);
		close(conn);
		return member;
	}
}

 

Dao

MemberDao

public class MemberDao {
	private Properties prop = new Properties();
	
	public MemberDao() {
		String filename = MemberDao.class.getResource("/sql/member/member-query.properties").getPath();
		System.out.println("filename@MemberDao = " + filename);
		try {
			prop.load(new FileReader(filename));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * DQL 요청 - DAO
	 * 1. PreparedStatement 객체 생성 (sql 전달) & 값 대입
	 * 2. 쿼리실행 executeQuery - ResultSet 반환
	 * 3. ResultSet 처리 - dto 객체 변환
	 * 4. ResultSet, preparedStatement 객체 반환
	 */
    public Member findById(String memberId, Connection conn) {
        PreparedStatement pstmt = null;
        ResultSet rset = null;
        Member member = null;
        String sql = prop.getProperty("findById");

        try {
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, memberId);

            rset = pstmt.executeQuery();

            while(rset.next()) {
                memberId = rset.getString("member_id");
                String password = rset.getString("password");
                String memberName = rset.getString("member_name");
                MemberRole memberRole = MemberRole.valueOf(rset.getString("member_role"));
                String _gender = rset.getString("gender");
                Gender gender = _gender != null ? Gender.valueOf(_gender) : null;	// Gender.M / Gender.F로 변환
                Date birthday = rset.getDate("birthday");
                String email = rset.getString("email");
                String phone = rset.getString("phone");
                String hobby = rset.getString("hobby");
                int point = rset.getInt("point");
                Timestamp enrollDate = rset.getTimestamp("enroll_date");
                member = new Member(memberId, password, memberName, memberRole, gender, birthday, email, phone, hobby, point, enrollDate);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rset);
            close(pstmt);
        } 
        return member;
    }

sql를 모아놓은 properties 파일을 가져오기 위하여 MemberDao 생성자 안에 properties 파일을 load하는 코드를 짰습니다.

 

서버 실행 후 아이디/비밀번호 입력 - 콘솔 출력값

filename@MemberDao = /C:/Workspaces/web_server_workspace/hello-mvc2/src/main/webapp/WEB-INF/classes/sql/member/member-query.properties
memberId = honggd
passowrd = 1234
filename = /C:/Workspaces/web_server_workspace/hello-mvc2/src/main/webapp/WEB-INF/classes/datasource.properties
member@MemberLoginServlet = Member [memberId=honggd, password=1234, memberName=홍길동, memberRole=U, gender=M, birthday=2000-09-09, email=honggd@naver.com, phone=01012341234, point=1000, enrollDate=2022-06-20 14:15:57.0]

데이터들을 잘 가져오는 것을 확인할 수 있습니다.

 


로그인 성공 여부에 따른 분기처리

 

Controller

MemberLoginServlet

// 3. 업무로직 : 로그인 여부 판단
Member member = memberService.findById(memberId);
System.out.println("member@MemberLoginServlet = " + member);

HttpSession session = request.getSession();

if(member != null && password.equals(member.getPassword())) {
    // 로그인 성공
    session.setAttribute("loginMember", member);
} else {
    // 로그인 실패
    session.setAttribute("msg", "아이디 또는 비밀번호가 일치하지 않습니다.");
}

request는 매 요청이 있을 때마다 request/response 객체가 생성됩니다.

따라서 session이 아닌, request에 속성을 저장한다면 페이지를 이동 혹은 새로고침할 때마다 request 객체가 생성되어 로그인이 풀려버리는 것이죠.

따라서 request보다 생명 주기가 긴 session을 이용하여 로그인 멤버 처리를 하였습니다.

 

header.jsp

<%@page import="com.ce.mvc2.member.model.dto.Member"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
	String msg = (String)session.getAttribute("msg");
	Member loginMember = (Member)session.getAttribute("loginMember");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello MVC</title>
<link rel="stylesheet" href="<%=request.getContextPath() %>/css/style.css" />
<script>
window.onload = () => {
	<% if(msg != null) {%>
		alert("<%= msg %>");
	<% } %>
	
    <% if(loginMember == null) { %>
	document.loginFrm.onsubmit = (e) => {
		const memberId = document.querySelector("#memberId");
		const password = document.querySelector("#password");
		
		if(!/^.{4,}$/.test(memberId.value)) {
			alert("유효한 아이디를 입력해주세요.");
			memberId.select();
			return false;
		}
		
		if(!/^.{4,}$/.test(password.value)) {
			alert("유효한 비밀번호를 입력해주세요.");
			password.select();
			return false;
		}
	};
	<% } %>
};
</script>
</head>
<body>
<div id="container">
<header>
    <h1>Hello MVC</h1>
    <div class="login-container">
        <!-- 로그인폼 시작 -->
        <% if(loginMember == null) { %>
        <form id="loginFrm" name="loginFrm" action="<%= request.getContextPath()%>/member/login" method="POST">
            <table>
                <tr>
                    <td><input type="text" name="memberId" id="memberId" placeholder="아이디" tabindex="1"></td>
                    <td><input type="submit" value="로그인" tabindex="3"></td>
                </tr>
                <tr>
                    <td><input type="password" name="password" id="password" placeholder="비밀번호" tabindex="2"></td>
                    <td></td>
                </tr>
                <tr>
                    <td colspan="2"><input type="checkbox" name="saveId" id="saveId" /> 
                    <label for="saveId">아이디저장</label>&nbsp;&nbsp; 
                    <input type="button" value="회원가입"></td>
                </tr>
            </table>
        </form>
        <!-- 로그인폼 끝-->
        <% } else { %>
            <table id="login">
                <tr>
                    <td>[<%=loginMember.getMemberName() %>]님, 안녕하세요!</td>
                </tr>
                <tr>
                    <td>
                        <input type="button" value="내정보보기" />
                        <input type="button" value="로그아웃" />
                    </td>
                </tr>
            </table>
    	<% } %>
    </div>
<!-- 메인메뉴 시작 -->
<nav>
    <ul class="main-nav">
        <li class="home"><a href="<%= request.getContextPath() %>">Home</a></li>
        <li class="notice"><a href="#">공지사항</a></li>
        <li class="board"><a href="#">게시판</a></li>
    </ul>
</nav>
<!-- 메인메뉴 끝-->
</header>

<section id="content">

session에 속성 등록을 하였기 때문에, 페이지를 이동하거나 새로고침을 해도 로그인 상태가 잘 유지 되어있습니다.

 

다만, 로그인 시 UML이 그대로 남아있기 때문에 새로고침을 하면 또 로그인 요청이 들어가게 됩니다.

 

따라서 UML을 필히 변경해줘야 합니다. (DML도!)

 

UML 변경

Controller

MemberLoginServlet

response.sendRedirect()

// 4. 응답 처리 : 로그인 후 url변경을 위해 리다렉트 처리
response.sendRedirect(request.getContextPath() + "/");	// /mvc/

/mvc/로 url을 변경하였습니다.

그렇다면, 응답 메세지에 302 redirect를 전송하고 브라우저에게 location (/mvc/)으로 재요청을 명령합니다.

 

브라우저의 네트워크 탭을 확인해보겠습니다.

기존 login은 302 상태, mvc2/는 200(정상처리) 상태로 되어있는 것을 확인할 수 있습니다.

 

login의 헤더를 확인해보겠습니다.

location에 요청한 '/mvc2/'가 정의 되어있는 것을 확인할 수 있습니다.

즉, login이 redirect 처리(302)가 되면 location에 정의된 UML로 이동해달라(재요청)! 라고 명령하는 것이죠.

redirect 흐름

※ redirect는 반드시 GET 방식!!

 

로그인을 실패했을 때도 /mvc2/로 잘 이동하는 것을 확인할 수 있습니다.

 

다만, 로그인 실패 후 새로고침을 해도 해당 alert가 계속 나타납니다.

그 이유는 msg 속성 또한 session에 저장해두었기 때문이죠. session이 제거되지 않는 한 계속해서 해당 메세지가 나올 것입니다. (심지어 로그인 성공을 해도!!!)

로그인 실패 시 redirect 흐름

header.jsp

removeAttribute()

<%
	String msg = (String)session.getAttribute("msg");
	if(msg != null) {
		session.removeAttribute("msg");	// 한번만 사용 후 제거
	}

	Member loginMember = (Member)session.getAttribute("loginMember");
%>

removeAttribute를 이용하여 한 번만 사용하고 제거해줍니다.

로그인 실패 후 최초 1회만 alert가 실행되고 이후로는 실행되지 않는 것을 확인할 수 있습니다.