1. 무상태에서 과거를 기억하는 법
- HTTP는 무상태이므로 과거 요청 기록을 알 수 없음
- 세션 트랙킹: 과거 요청 기록을 추적하는 기법
- HTTP는 세션 트랙킹에 쿠키를 이용
- 쿠키: 문자열로 만들어진 데이터 조각
- 쿠키는 서버와 브라우저 사이에서 요청이나 응답시에 주고받는 형태로 사용됨
- 쿠키의 가장 기본적인 형태는 이름과 값으로 구성된 구조
- 브라우저에서 개발자 도구의 애플리케이션 메뉴에서 확인 가능
- 쿠키를 주고받는 기본적인 시나리오
- 브라우저에서 최초로 서버 호출 시 해당 서버에서 발행한 쿠키가 없다면 브라우저는 아무것도 전송 x
- 서버에서 응답 메세지 보낼 때, 브라우저에 쿠키를 보내는데 이때 'Set Cookie'라는 HTTP 헤더 이용
- 브라우저는 쿠키를 받은 후 이에 대한 정보를 읽고, 파일 형태로 보관할 지 메모리상에서만 처리할 지 결정(쿠키에 있는 유효기간(만료기간)을 보고 결정)
- 브라우저에 보관하는 쿠키는 다음에 다시 브라우저가 서버에 요청할 때 HTTP 헤더에 'Cookie'라는 헤더 이름과 함께 전달(쿠키에는 경로를 지정할 수 있어서 해당 경로에 맞는 쿠키 전송)
- 서버에서는 필요에 따라서 브라우저가 보낸 쿠키를 읽고 사용
1) 쿠키를 생성하는 방법
- 서버에서 자동으로 생성하는 쿠키: 응답 메세지를 작성할 때 정해진 쿠키가 없는 경우 자동으로 발행(WAS에서 발행되며 WAS마다 고유한 이름 사용(톰캣은 JSESSIONID))
- 서버에서 발행하는 쿠키는 기본적으로 브라우저 메모리 상에 보관, 브라우저 종료 시 쿠키는 삭제됨
- 서버에서 발행하는 쿠키의 경로는 '/'
- 개발자가 생성하는 쿠키: 개발자가 생성하는 쿠키는 서버에서 생성되는 쿠키와 다음이 다름
- 이름 원하는대로 지정 가능
- 유효기간 지정가능(유효기간 지정 시, 브라우저가 이를 파일 형태로 보관)
- 반드시 직접 응답에 추가해 주어야 함
- 경로나 도메인 등을 지정 가능(특정 서버의 경로를 호출하는 경우에만 쿠키 사용)
2. Servlet 컨텍스트와 세션 저장소
- 하나의 톰캣은 여러 웹 애플리케이션을 실행할 수 있음
- 실제 운영 시 웹 애플리케이션 마다 별도의 도메인으로 분리해서 운영됨
- 프로젝트 실행 경로를 '/'외에 다른 이름으로 각각 지정해서 실행하면 하나의 톰캣에서 여러 애플리케이션 실행 가능
- 각 애플리케이션은 자신만 사용하는 고유 메모리 영역을 생성하여 이 공간에 Servlet이나 JSP 등을 인스턴스로 만들어 서비스 제공, 이 영역을 Servlet API에서는 Servlet 컨텍스트라고 함
- 애플리케이션 생성 시 톰캣이 발행하는 쿠키를 관리하기 위한 메모리 영역이 하나 더 생성됨(세션 저장소)
- 세션 저장소는 기본적으로 키와 값을 보관하는 구조
- 톰캣에서 JSESSIONID가 키가 됨
- 새로운 JSESSIONID 쿠키가 만들어질 때마다 메모리 공간을 차지하므로 톰캣은 주기적으로 세션 저장소를 조사하며 사용하지 않는 값 정리(session-timeout 설정 이용, 기본 30분 주기마다 지정된 시간보다 오래된 값 삭제)
1) 세션을 통한 상태 유지 메커니즘
- HttpServletRequest의 getSession()이라는 메서드 실행
→ 톰캣에서는 JSESSIONID 이름의 쿠키가 요철할 때 있었는지 확인
→ 없다면 새로운 값을 만들어 세션 저장소에 보관

- A1234와 B111은 해당 공간에 login 정보가 존재하며, 서버에서 프로그램 작성 시 이를 이용해서 해당 사용자가 로그인했다는 것을 인정하는 방식
2) HttpServletRequest의 getSession()
- HttpServletRequest의 getSession()은 브라우저가 보내는 정보를 이용해 다음의 작업 수행
- JSESSIONID가 없는 경우: 세션 저장소에 새로운 번호로 공간을 만들고 해당 공간에 접근할 수 있는 객체 반환(새로운 번호는 브라우저에 JSESSIONID의 값으로 전송(세션 쿠키))
- JSESSIONID가 있는 경우: 세션 저장소에 JSESSIONID 값을 이용해서 할당된 공간을 찾고 이 공간에 접근할 수 있는 객체 반환
- getSession()의 결과물은 세션 저장소 내의 공간, 이 공간을 의미하는 타입은 HttpSession타입이며 해당 공간은 세션 컨텍스트 또는 세션이라고 함

- HttpSession 타입의 객체를 이용하면 현재 사용자만의 공간에 원하는 객체를 저장 / 수정 / 삭제할 수 있음
- isNew() 메서드 등으로 새롭게 공간을 만들 것인지 기존 공간 재사용할 것인지 구분 가능
3. 세션을 이용한 로그인 체크
- 세션을 이용한 로그인 체크 시나리오
- 사용자가 로그인에 성공하면 HttpSession을 이용해 해당 사용자의 공간(세션 컨텍스트)에 특정 객체를 이름과 함께 저장
- 로그인 체크가 필요한 컨트롤러에서 현재 사용자의 공간에 지정된 이름으로 객체가 저장되었는지 확인
→ 객체가 존재한다면 해당 사용자는 로그인된 사용자로 간주
→ 아니면 로그인 페이지로 이동
실습
1) 등록할 때, 로그인 체크 하기
- 로그인한 사용자만 Todo를 등록할 수 있다고 가정
- TodoRegisterController에서 doGet() 수정
// TodoRegisterController
...
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("/todo/register GET .......");
HttpSession session = req.getSession();
// 기존에 JSESSIONID가 없는 새로운 사용자
if (session.isNew()) {
log.info("User with new JSESSIONID cookie");
resp.sendRedirect("/login");
return;
}
// JSESSIONID는 있지만 해당 세션 켄텍스트에 loginInfo라는 이름으로 저장된 객체가 없는 경우
if (session.getAttribute("loginInfo") == null) {
log.info("User without login information");
resp.sendRedirect("/login");
return;
}
// 정상적인 경우
req.getRequestDispatcher("/WEB-INF/todo/register.jsp").forward(req, resp);
}
...

- 브라우저는 '/login'으로 redirect 됨
- HttpServletRequest의 getSession()을 호출하여 새로운 값이 생성되어 브라우저로 전송되어 저장
(JSESSIONID가 없는 채로 'todo/register' 경로에 접속했을 때, '/login'으로 redirect되고 '/login'에서 개발자 도구를 통해 register의 응답 헤더를 보면 'Set-Cookie' 헤더가 전송된 것을 확인할 수 있음 ↓)

- 로그에는 '기존에 JSESSIONID가 없는 사용자'에 대한 처리를 했을 때 나오도록 설정한 메세지가 출력됨
2) 로그인 처리 Controller 작성
- 로그인은 '/login' 경로에서 GET방식으로 로그인 화면 보여주고, POST 방식으로 실제 로그인 처리하도록 구성
- controller 패키지 내에 LoginController 클래스와 WEB-INF 폴더 내에 로그인 화면을 나타낼 login.jsp 파일 생성
// LoginController
package org.zerock.w2.controller;
import lombok.extern.java.Log;
import lombok.extern.log4j.Log4j2;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/login")
@Log
public class LoginController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("login get.............");
req.getRequestDispatcher("/WEB-INF/login.jsp").forward(req, resp);
}
}
<!-- login.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action = "/login" method = "post">
<input type = "text" name = "mid">
<input type = "text" name = "mpw">
<button type = "submit">LOGIN</button>
</form>
</body>
</html>
- 로그인 처리와 HttpSession의 setAttribute()
- LoginController에서 POST 방식으로 파라미터 수집, HttpSession에 'loginInfo' 이름을 이용해서 간단한 문자열을 저장하도록 구성
// LoginController
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("login post.............");
String mid = req.getParameter("mid");
String mpw = req.getParameter("mpw");
String str = mid + mpw;
HttpSession session = req.getSession();
// HttpSession을 이용하여 setAttribute() 메서드를 사용자 공간에 loginInfo 라는 이름으로 문자열을 보관
session.setAttribute("loginInfo", str);
resp.sendRedirect("/todo/list");
}
- 결과



4. 필터를 이용한 로그인 체크
- 로그인 여부를 체크해야 하는 Controller마다 동일하게 체크하는 로직을 작성하면 같은 코드를 계속 작성해야 하므로 대부분 필터를 이용해 처리
- 필터: 특정 Servlet이나 JSP 등에 도달하는 과정에서 필터링하는 역할을 위해 존재하는 Servlet API
- @WebFilter 어노테이션을 이용해 특정 경로에 접근 시 필터가 동작하도록 설계하면 동일 로직을 필터로 분리
- 필터는 한 개 이상, 여러 개 적용 가능
- 프로젝트에 filter 패키지 구성하고 LoginCheckFilter 클래스 추가
// LoginCheckFilter
package org.zerock.w2.filter;
import lombok.extern.log4j.Log4j2;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
// '/todo..'로 시작하는 모든 경로에 필터링 시도
@WebFilter(urlPatterns = {"/todo/*"})
@Log4j2
public class LoginCheckFilter implements Filter {
// doFilter()는 필터가 필터링이 필요한 로직을 구현하는 부분
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("Login check filter....");
// 다음 필터나 목적지(Servlet, JSP)로 갈 수 있도록 FilterChain의 doFilter()를 실행
// 문제가 생겨서 더 이상 진행할 수 없다면 다음 단계로 진행하지 않고 다른 방식으로 redirect 처리
chain.doFilter(request, response);
}
}
실습
3) 로그인 체크 구현
- LoginCheckFilter에서 '/todo/...'로 시작하는 모든 경로에 접근 시 동작하도록 설정(위의 코드 구체화)
// LoginCheckFilter
package org.zerock.w2.filter;
import lombok.extern.log4j.Log4j2;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
// '/todo..'로 시작하는 모든 경로에 필터링 시도
@WebFilter(urlPatterns = {"/todo/*"})
@Log4j2
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("Login check filter....");
// doFilter에서 HttpServletRequest와 HttpServletResponse보다 상위 타입의 파라미터 사용
// 따라서 HTTP 관련 작업을 위해 (HttpServlerRequest)request처럼 다운캐스팅 필요
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse resp = (HttpServletResponse)response;
HttpSession session = req.getSession();
// LoginController에서 로그인했을 때 세션에 loginInfo라는 이름으로 저장하도록 하였음
// 이 loginInfo가 없다면 로그인하지 않은 것으로 판단되므로 LoginCheckFilter에 의해 '/login' 경로로 이동
if (session.getAttribute("loginInfo") == null) {
resp.sendRedirect("/login");
return;
}
// 어떤 값이든 '/login'에서 로긍니 정보를 전달해서 로그인이 처리되면 그 이후에는 '/todo/...' 경로 이용가능
chain.doFilter(request, response);
}
}
4) UTF-8 처리 필터
- POST 방식으로 '/todo/register'를 통해 전달되는 문자열은 한글이 깨지므로 HttpServletRequest의 데이터를 SetCharacterEncoding("UTF-8")을 적용해 해결
- filter 패키지에 UTF8Filter 추가
// UTF8Filter
package org.zerock.w2.filter;
import lombok.extern.log4j.Log4j2;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(urlPatterns = {"/*"})
@Log4j2
public class UTF8Filter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("UTF8 filter....");
HttpServletRequest req = (HttpServletRequest)request;
req.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}
5. 세션을 이용하는 로그아웃 처리
- HttpSession을 이용하는 경우 로그아웃 처리는 로그인 확인 시 사용했던 정보를 삭제시키는 방식 또는 현재 HttpSession이 더이상 유효하지 않다고 invalidate() 시키는 방식 이용
- 프로젝트에 LogoutController 추가, '/logout' 경로를 처리하도록 구성
// LogoutController
package org.zerock.w2.controller;
import lombok.extern.log4j.Log4j2;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/logout")
@Log4j2
public class LogoutController extends HttpServlet {
// 로그아웃 처리는 POST 방식으로만 처리되도록 함
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("log out...................");
HttpSession session = req.getSession();
// loginInfo라는 이름을 삭제
session.removeAttribute("loginIngo");
// 현재 HttpSession이 더 이상 유효하지 않은 것으로 만들기
session.invalidate();
// 로그아웃 이후에는 "/"경로로 redirect
resp.sendRedirect("/");
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri = "http://java.sun.com/jsp/jstl/core" prefix ="c" %>
<html>
<head>
<title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>
<ul>
<c:forEach items = "${dtoList}" var = "dto">
<li>
<span><a href = "/todo/read?tno=${dto.tno}">${dto.tno}</a></span>
<span>${dto.title}</span>
<span>${dto.dueDate}</span>
<span>${dto.finished? "DONE": "NOT YET"}</span>
</li>
</c:forEach>
</ul>
<!-- 목록화면에서 logout을 post 방식으로 처리할 버튼 추가 -->
<form action = "/logout" method = "post">
<button>LOGOUT</button>
</form>
</body>
</html>


6. 데이터베이스에서 회원 정보 이용하기
- 데이터베이스 연결 후 회원 정보를 저장한 tbl_member 테이블 생성
- 회원정보도 임의로 생성
create table tbl_member(
mid varchar(50) primary key,
mpw varchar(50) not null,
mname varchar(100) not null
);
insert into tbl_member (mid, mpw, mname) values ('user00', '1111', '사용자0');
insert into tbl_member (mid, mpw, mname) values ('user01', '1111', '사용자1');
insert into tbl_member (mid, mpw, mname) values ('user02', '1111', '사용자2');
실습
5) 자바에서 회원 데이터 처리하기
- 자바에서 회원 정보를 객체로 처리하도록 VO / DAO 등을 구현
- MemberVO와 MemberDAO 구현
// MemberVO
package org.zerock.w2.domain;
import lombok.*;
@Getter
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberVO {
private String mid;
private String mpw;
private String mname;
}
// MemberDAO
package org.zerock.w2.dao;
import lombok.Cleanup;
import org.zerock.w2.domain.MemberVO;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class MemberDAO {
public MemberVO getWithPassword (String mid, String mpw) throws Exception {
String query = "select mid, mpw, mname from tbl_member where mid = ? and mpw = ?";
MemberVO memberVO = null;
@Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement preparedStatement = connection.prepareStatement(query);
preparedStatement.setString(1, mid);
preparedStatement.setString(2, mpw);
@Cleanup ResultSet resultSet = preparedStatement.executeQuery();
resultSet.next();
memberVO = MemberVO.builder()
.mid(resultSet.getString(1))
.mpw(resultSet.getString(2))
.mname(resultSet.getString(3))
.build();
return memberVO;
}
}
- 서비스 계층과 Controller에서 사용할 MemberDTO와 MemberService 구현
// MemberDTO
package org.zerock.w2.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberDTO {
private String mid;
private String mpw;
private String mname;
}
- MemberService는 여러 곳에서 동일한 객체를 사용할 수 있도록 enum으로 하나의 객체만 구성하고 MemberDAO를 이용하도록 구성
// MemberService
package org.zerock.w2.service;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.zerock.w2.dao.MemberDAO;
import org.zerock.w2.domain.MemberVO;
import org.zerock.w2.dto.MemberDTO;
import org.zerock.w2.util.MapperUtil;
@Log4j2
public enum MemberService {
INSTANCE;
private MemberDAO dao;
private ModelMapper modelMapper;
MemberService() {
dao = new MemberDAO();
modelMapper = MapperUtil.INSTANCE.get();
}
// 로그인 처리를 위한 login() 메서드 작성
public MemberDTO login(String mid, String mpw) throws Exception {
MemberVO vo = dao.getWithPassword(mid, mpw);
MemberDTO memberDTO = modelMapper.map(vo, MemberDTO.class);
return memberDTO;
}
}
6) Controller에서 로그인 연동
- LoginController의 doPost()에 MemberService를 연동해서 실제 로그인 되도록 수정
// loginController
...
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("login post.............");
String mid = req.getParameter("mid");
String mpw = req.getParameter("mpw");
// 정상적으로 로그인 된 경우, HttpSession을 이용해서 'loginInfo'라는 이름으로 객체 저장
try {
MemberDTO memberDTO = MemberService.INSTANCE.login(mid, mpw);
HttpSession session = req.getSession();
session.setAttribute("loginInfo", memberDTO);
resp.sendRedirect("/todo/list");
} catch (Exception e) { // 예외 발생 시 '/login'으로 이동, result라는 파라미터를 전달해서 문제가 발생했다는 사실 전달
resp.sendRedirect("/login?result=error");
}
...
- EL에서 쿼리 스트링 처리
- '/WEB-INF/login.jsp'에는 EL에서 기본으로 제공하는 param이라는 객체를 이용해서 result라는 이름으로 전달한 값 확인 가능
<!-- login.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!-- JSTL을 이용 -->
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix = "c" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<!-- ${param.result}를 이용해서 에러 발생 시 다른 메세지 출력 -->
<c:if test = "${param.result == 'error'}">
<h1>로그인 에러</h1>
</c:if>
<form action = "/login" method = "post">
<input type = "text" name = "mid">
<input type = "text" name = "mpw">
<button type = "submit">LOGIN</button>
</form>
</body>
</html>
- tbl_member 테이블에 없는 id와 pw 입력시

- EL의 Scope와 HttpSession 접근하기
- EL을 이용해서 HttpServletRequest에 setAttribute()로 저장한 객체를 사용할 수 있음
- EL이 HttpServletRequest에 저장된 객체를 찾을 수 없다면, 자동으로 HttpSession에 저장된 객체를 찾아냄(EL의 Scope)
- EL의 Scope를 이용해서 접근하는 변수의 종류
- Page Scope: JSP에서 EL을 이용해 <c:set>으로 저장한 변수
- Request Scope: HttpServletRequest에 setAttribute()로 저장한 변수
- Session Scope: HttpSession을 이용해서 setAttribute()로 저장한 변수
- Application Scope: ServletContext를 이용해서 setAttribute()로 저장한 변수
ex) EL로 ${obj}라고 하면 앞의 Scope들이 순차적으로 page → request → session → application 순으로 'obj' 라는 이름의 객체 찾는 방식
- 위의 예제에서 'loginInfo'라는 이름으로 MemberDTO를 저장했다면 JSP에는 기존 방식대로 ${loginInfo}라는 이름으로 접근 가능
- '/WEB-INF/todo/list.jsp'는 로그인 한 사용자만 접근 가능한 경로이므로 다음과 같은 코드로 현재 로그인한 사용자의 이름 출력가능
<h2>${loginInfo}</h2>
<h3>${loginInfo.mname}</h3>

'back-end > Java' 카테고리의 다른 글
| [자바 웹 개발 워크북] 3.3 - 리스너 (0) | 2023.01.11 |
|---|---|
| [자바 웹 개발 워크북] 3.2 - 사용자 정의 쿠키 (1) | 2023.01.11 |
| [자바 웹 개발 워크북] 2.3 - 웹 MVC와 JDBC의 결합 (1) | 2023.01.06 |
| [자바 웹 개발 워크북] 2.2 - 프로젝트 내 JDBC 구현 (0) | 2023.01.05 |
| [자바 웹 개발 워크북] 2.1 - JDBC 프로그래밍 준비 (0) | 2023.01.04 |