오늘은 Session과 Cookie에 대해 공부하고, 로그인 기능을 직접 구현해 보면서 해당 내용이 어떻게 활용되는지 익혀보는 시간을 갖도록 하겠습니다.

공부하기에 앞서 "session과 cookie는 무엇이고 왜 써야하는가?"에 대한 의문이 생기게 됩니다.
이를 설명하기 위해서는 HTTP 프로토콜의 stateless, connectionless 성질에 대해서 살펴보아야 합니다.

HTTP 프로토콜 특징
1. Stateless(무상태)
HTTP는 각 요청 간에 서버가 클라이언트의 상태를 유지하지 않는 "무상태" 특징을 가집니다. 이는 각 요청이 독립적이며 서로 관련이 없다는 것을 의미합니다. 따라서 이전 요청과 관련된 정보를 서버가 기억하지 않습니다.
2. Connectionless (비연결)
HTTP는 각 요청과 응답 간에 지속적인 연결을 유지하지 않고, 클라이언트의 요청에 맞는 응답을 보내고 연결을 끊는 "연결 끊김" 특징을 가집니다. 이것은 요청과 응답 사이에 연결을 끊고 연결을 다시 설정해야 한다는 것을 의미합니다.

클라이언트와 처음 통신으로 요청과 응답을 주고 받았어도 그 다음 통신에서는 이전 데이터를 유지하지 못합니다. 하지만, 데이터를 유지하지 못한다면 사이트에서 어떠한 요청을 보낼때마다 로그인을 해야하는 아이러니한 상황이 발생하게 됩니다.

💡 이러한 stateful한 상황을 위해 session과 cookie를 활용하게 됩니다.

Session이란?

세션은 서버 측에 상태 정보를 저장하고 클라이언트와 관련된 *세션 식별자를 사용하여 해당 상태를 관리합니다. 연결이 끊길 때마다 세션 정보를 잃지 않도록 쿠키에 세션 식별자를 저장하여 사용자의 상태를 계속 추적할 수 있습니다.

*세션 식별자란?
세션을 관리하기 위해 클라이언트와 서버 간에 고유한 세션 식별자를 사용합니다. 클라이언트가 서버에 요청할 때 세션 식별자를 함께 전송하여, 서버는 이를 사용하여 해당 클라이언트의 세션을 식별합니다.

Session 특징

  1. 서버쪽에 저장 : 세션 데이터는 서버쪽에 저장됩니다. 클라이언트에서 직접 액세스할 수 없으며, 서버에서만 관리됩니다. 이로 인해 민감한 정보(ex.로그인 정보)를 안전하게 저장할 수 있습니다.
  2. 상태 관리 : 세션을 사용하면 서버는 클라이언트의 상태를 관리할 수 있습니다. 사용자가 로그인하고 웹 애플리케이션을 탐색하는 동안 세션을 통해 사용자의 인증 상태, 장바구니 내용, 사용자 환경 설정 등을 추적할 수 있습니다.
  3. 고유한 세션 식별자: 각 세션은 고유한 식별자를 가지고 있습니다. 일반적으로 이 식별자는 쿠키 또는 URL 매개변수를 통해 클라이언트에게 제공되고, 클라이언트는 이 식별자를 사용하여 서버의 해당 세션을 식별합니다.
  4. 상태 유지: 세션은 연결이 끊겨도 상태를 유지합니다. 사용자가 웹 페이지를 이동하거나 새로 고침하더라도 세션 데이터는 보존됩니다. 이로 인해 사용자의 활동을 지속적으로 추적하고 상태를 유지할 수 있습니다.
  5. 유효 기간: 세션은 일반적으로 사용자의 브라우저 세션 동안 유지됩니다. 즉, 사용자가 브라우저를 닫을 때 혹은 로그아웃할 때 세션이 종료됩니다. 그러나 세션을 만료 기간 설정을 통해 일정 시간 동안 유지할 수도 있습니다.

Cookie란?

쿠키는 클라이언트 측에 저장되고 요청과 응답 사이에 데이터를 유지하는 데 사용됩니다. 각 요청에서 쿠키는 클라이언트에서 서버로 자동으로 전송되어 이전 요청과 관련된 정보를 서버에 제공합니다.

  1. 클라이언트 쪽에 저장: 쿠키는 클라이언트 쪽(웹 브라우저)에 저장됩니다. 이로 인해 클라이언트에서 서버로 데이터를 전송할 때 쿠키가 함께 전송됩니다.
  2. 텍스트 기반: 쿠키는 작은 텍스트 파일로 저장되며, 주로 key-value 쌍의 형식을 갖습니다. 이러한 key-value 쌍은 클라이언트와 서버 간에 데이터를 교환하는 데 사용됩니다. value에는 문자열만 들어갈 수 있습니다.
  3. 유효 기간: 쿠키에는 만료 기간을 설정할 수 있습니다. 만료 기간을 설정하면 쿠키가 일정 기간 동안 유지되고 그 이후에 자동으로 삭제됩니다. 일부 쿠키는 브라우저 세션 동안만 유지되며 브라우저를 닫으면 삭제됩니다.
  4. 사용자 지정 데이터 저장: 쿠키는 사용자 선호 설정, 로그인 상태, 장바구니 내용 등과 같은 사용자 지정 데이터를 저장하는 데 사용됩니다. 이를 통해 사용자 경험을 개선하고 사용자가 웹 사이트를 더 효과적으로 사용할 수 있게 됩니다.
  5. 요청과 응답 간 데이터 전송: 클라이언트가 웹 서버에 요청을 보낼 때 쿠키는 해당 요청과 함께 서버로 전송됩니다. 이렇게 서버는 클라이언트의 이전 활동과 상태를 알 수 있습니다.
  6. 크기 제한: 클라이언트에 총 300개의 쿠키를 저장할 수 있으며, 하나의 도메인 당 20개의 쿠키를 가질 수 있습니다.

▪️ 가장 큰 차이점은 사용자의 정보가 저장되는 위치입니다. 쿠키는 클라이언트 쪽에 저장되어 서버의 자원을 전혀 사용하지 않는 반면, 세션은 서버의 자원을 사용하여 저장하게 됩니다.
▪️ 쿠키는 클라이언트 측에 저장되기 때문에 클라이언트에서 데이터를 조작하거나 변조할 수 있습니다. 따라서 민감한 정보를 쿠키에 저장할 때 데이터 무결성을 보장하기 위한 추가 보안 메커니즘이 필요합니다. 하지만, 세션 데이터는 서버에 저장되므로 클라이언트 측에서 직접 변경할 수 없습니다. 이로 인해 민감한 정보를 세션에 저장하는 데 더 안전하며, 데이터 무결성을 더 쉽게 보장할 수 있습니다.
▪️ 쿠키는 만료기간을 따로 지정해 쿠키를 삭제할 때까지 유지할 수 있으며, 파일로 저장되기 때문에 브라우저를 종료해도 정보가 유지될 수 있습니다. 반면, 세션은 서버에 객체 형태로 저장되고 만료기간을 정할 수 있으나, 브라우저가 종료되면 만료기간에 상관 없이 삭제되게 됩니다.

그렇다면 이렇게 정리한 개념들을 가지고 어떻게 세션과 쿠키를 활용할 수 있을지 로그인 기능을 구현해보면서 익혀보도록 하겠습니다.

로그인 기능 구현

이번에 사용할 DB는 다음과 같습니다.

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `member`
--

DROP TABLE IF EXISTS `member`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `member` (
  `member_id` varchar(20) NOT NULL,
  `password` varchar(20) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  `register_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

LOCK TABLES `member` WRITE;
/*!40000 ALTER TABLE `member` DISABLE KEYS */;
INSERT INTO `member` VALUES ('admin','1234','관리자','2023-09-21 02:32:00');
/*!40000 ALTER TABLE `member` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
  1. Member(DTO)를 만듭니다.
package member;

public class Member {
	private String memberId;
	private String password;
	private String name;
	private String registerDate;

	public Member() {}

	public Member(String memberId, String password, String name, String registerDate) {
		super();
		this.memberId = memberId;
		this.password = password;
		this.name = name;
		this.registerDate = registerDate;
	}

	public String getMemberId() {
		return memberId;
	}
	public void setMemberId(String memberId) {
		this.memberId = memberId;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getRegisterDate() {
		return registerDate;
	}
	public void setRegisterDate(String registerDate) {
		this.registerDate = registerDate;
	}

	@Override
	public String toString() {
		return "Member [memberId=" + memberId + ", password=" + password + ", name=" + name + ", registerDate="
				+ registerDate + "]";
	}
}
  1. 이전 시간에 배웠던 DAO를 활용하여 MemberDao(Interface)와 MemberDaoImpl를 만듭니다.
package member.model.dao;

import java.sql.SQLException;
import member.Member;

public interface MemberDao {
	Member login(Member member) throws SQLException;
}
package member.model.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import member.Member;
import member.util.DBUtil;

public class MemberDaoImpl implements MemberDao {

private DBUtil dbUtil = DBUtil.getInstance();
	
	@Override
	public Member login(Member member) throws SQLException{
		// 1. sql 작성 
		String sql = "select member_id,name,register_date\n" + 
				"\n" + 
				"from member\n" + 
				"\n" + 
				"where member_id=? and password=?;";
		
		Connection conn =null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		
		try {
			// 2. DB 연결 
			conn = dbUtil.getConnection();
			
			// 3. 쿼리 실행 
			pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, member.getMemberId());
			pstmt.setString(2, member.getPassword());
			rs= pstmt.executeQuery();
			
			// 4. 조회 결과 파싱 
			if(rs.next()) {
				String name = rs.getString("name");
				String registerDate = rs.getString("register_date");
				
				Member loginMember = new Member(member.getMemberId(), null, name, registerDate);
				return loginMember;
				
			}
			return null;
					
		} finally {
			// 5. 자원반납 
			dbUtil.close(conn,pstmt,rs);
		}
	}
}
  1. memberController(Servlet)을 만듭니다.
package com.ssafy.controller;

import java.io.IOException;
import java.sql.SQLException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import member.model.Member;
import member.model.service.MemberService;
import member.model.service.MemberServiceImpl;


@WebServlet("/member")
public class MemberController extends HttpServlet {
	private static final long serialVersionUID = 1L;
    private MemberService memberServiceImpl = MemberServiceImpl.getInstance();
   
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String action = request.getParameter("action");
		System.out.println("들어온 요청 : " + action);
		try {
			switch (action) {
			case "mvLogin":
				System.out.println("mvLogin 실행");
				request.getRequestDispatcher("/member/login.jsp").forward(request, response);
				break;
				
			case "login":
				login(request,response);
				break;
				
			case "logout":
				logout(request,response);
				break;

			default:
				request.getRequestDispatcher("/error/error404.jsp").forward(request, response);
				break;
			}
		} catch (Exception e) {
			e.printStackTrace();
			request.getRequestDispatcher("/error/error.jsp").forward(request, response);
		}
	}

	private void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
		System.out.println("로그아웃 요청 수신");
		// 세션 무효화 
		HttpSession session = request.getSession();
		session.invalidate();
		
		// 재요청할 메인 페이지 url 응답 
		response.sendRedirect(request.getContextPath());		
	}

	private void login(HttpServletRequest request, HttpServletResponse response) throws SQLException, IOException, ServletException {
		System.out.println("로그인 요청 시작");
		// 1. 아이디, 비밀번호 값 추출 
		String memberId = request.getParameter("memberId");
        String password = request.getParameter("memberPassword");
        Member member = new Member();
        member.setMemberId(memberId);
        member.setPassword(password);
        
        System.out.println(member);
        //2. DB에 해당 멤버 조회
		Member memberInfo = memberServiceImpl.login(member);
		
		// 로그인 성공시
		 if(memberInfo!=null) {
	            System.out.println("로그인한 유저 : "+memberInfo);
	            
	            //3. 해당 멤버가 존재한다면 세션에 유저 정보 저장
	            HttpSession session = request.getSession();
	            session.setAttribute("memberInfo", memberInfo);
	            
	            String isRemember = request.getParameter("isRemember");
	            System.out.println("isRemember : "+isRemember);
	            
	            //아이디 저장 했을 경우 
	            if(isRemember!=null) {
	                Cookie cookie = new Cookie("rememberId", memberId);
	                cookie.setMaxAge(60*60); 
	                response.addCookie(cookie);
	            }
	            
	            //아이디 저장하지 않았을 경우 
	            else {
	                //기존 쿠키를 유효기간 0인 쿠키로 대체함 - 삭제 
	                Cookie cookie = new Cookie("rememberId", memberId);
	                cookie.setMaxAge(0);
	                response.addCookie(cookie);
	            }
	            //4. 클라이언트가 새로요청할 주소값 넘김
	            request.getRequestDispatcher("/").forward(request, response);
	        }
	        //로그인 실패시
	        else {
	            System.out.println("로그인 실패");
	            response.sendRedirect(request.getContextPath()+"/member?action=mvLogin");
	        }
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		request.setCharacterEncoding("utf-8");
		doGet(request, response);
	}
}
  1. WebContent/member에 login.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>
</head>
<body>
	<form action="${pageContext.request.contextPath}/member" method="post">
		<fieldset>
		<h2>로그인</h2>
		<input type="hidden" name="action" value="login" />
		<div>아이디</div>
		<input type="text" name="memberId" placeholder="아이디" value="${cookie.rememberId.value}" /> <br/>
		<input type="checkbox" name="isRemember" ${empty cookie.rememberId?"":"checked"} /> 아이디 저장 <br/>
		<div>비밀번호</div>
		<input type="text" name="memberPassword" placeholder="비밀번호" />
		<br/>
		<button>로그인</button>
		<button>취소</button>
		</fieldset>
	</form>
</body>
</html>
  1. WebContent/에 index.jsp, WebContent/include/에 nav.jsp를 만듭니다.

WebContent/index.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>
</head>
<body>
<%@ include file="/include/nav.jsp"%>
<h2>안녕하세요. 상품 관리 사이트 입니다.</h2>
</body>
</html>

WebContent/include/nav.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<nav>
	<ul>
		<%-- 로그인 하지 않은 경우 메뉴 --%>
		<c:if test="${empty sessionScope.memberInfo}">

			<li><a
				href="${pageContext.request.contextPath}/member?action=mvLogin">로그인</a></li>
		</c:if>

		<c:if test="${not empty sessionScope.memberInfo}">
			<li><a
				href="${pageContext.request.contextPath}/
			product?action=list">상품
					목록</a></li>
			<li><a
				href="${pageContext.request.contextPath}/product?action=mvRegist">상품
					정보 등록</a></li>
			<li>${memberInfo.memberId}님 로그인</li>
			<li><a
				href="${pageContext.request.contextPath}/member?action=logout">로그아웃</a></li>
		</c:if>
	</ul>
</nav>

참고
https://dev-coco.tistory.com/61

profile
누군가에게 도움을 주기 위한 개발자로 성장하고 싶습니다.

0개의 댓글