[Servelt] Session, 세션 저장소, 세션 컨텍스트
HTTP는 무상태
http는 기본적으로 클라이언트의 상태를 저장하지 않는 무상태이다. 그렇기에 적은 자원으로 여러 요청으로 처리가 가능한데
대부분의 서비스들의 서버에서는 클라이언트의 상태를 기억해야 하기 때문에 과거의 방문기록을 추적하는 세션 트래킹(Session Tracking) 기법을 사용해야 한다.
세션 트래킹(Session Tracking)
http에서 세션 트래킹은 쿠키(Cookie)를 이용하는데 쿠키는 서버와 브라우저간에 주고 받는 일종의 문자열 데이터이다.
key와 value값으로 이루어져 있다.
쿠키를 주고 받는 시나리오.

쿠키를 생성하는 방법
서버에서 쿠키를 생성할 땐 서버에서 자동으로 발행되는 것과 개발자가 짠 코드를 통해 발행하는 두가지의 방식이 존재
- 서버에서 자동으로 발행되는 쿠키 : 응답 메시지를 작성할 때 정해진 쿠키가 없으면 자동으로 WAS에서 발행되며 WAS마다 고유한 이름의 쿠키를 생성하는데 톰캣은 JSESSIONID 라는 이름을 사용한다. 이런 쿠키 들은 만료일이 없어 브라우저 종료시 자동으로 삭제되며 기본적으로 경로는 '/' 로 지정 된다.
- 개발자가 생성하는 쿠키 : 이름을 개발자가 원하는대로 생성 할 수 있고 또한 브라우저가 종료되어도 쿠키가 유지되도록 유효기간을 설정할 수 있다. 또한 경로도 자유롭게 지정할 수 있다.
참고로 위해서 경로란 해당 경로에서만 브라우저에서 서버로 쿠키가 전송된다는 의미이다.
세션 저장소(Session Repository)
하나의 톰캣으로 여러 웹 프로젝트를 실행할 수 있는데 프로젝트의 실행 경로를 '/'외에 다른 이름으로 각각 지정해서 실행하면 하나의 톰캣 내에서도 여러 웹 프로젝트를 실행할 수 있다.
각각의 웹 프로젝트(애플리케이션)은 자신이 사용하는 고유의 메모리 영역을 하나 생성해서 이 공간에 서블릿이나 JSP 등의 인스턴스를 만들어 제공하는데 이 영역을 서블릿 컨텍스트라고 한다. 서블릿 컨텍스트는 각각의 서블릿들을 관리하고 정보를 공유할 수 있게 하는 역할인다.
즉 하나의 프로젝트가 실행되면 서블릿 컨텍스트가 하나씩 있고 그 안에 또 각각의 서블릿이 있는 것이다.
서블릿 컨텍스트 내의 서블릿들은 웹 어플리케이션에서 공유하는 공유 자원들을 사용할 수 있다.

각각의 웹 애플리케이션을 생성할 때는 톰캣이 발행하는 쿠키들을 관리하기 위한 메모리 영역이 더 생성되는데 이 영역을 세션 저장소(Session Repository)라고 한다.
세션 저장소에서는 각 JSESSIONID마다 고유한 공간을 가지게 되고 이 공간은 key와 value로 데이터를 보관할 수 있다.

HttpServletRequset의 getSession()
HttpServletRequset의 getSession()은 브라우저가 보내는 정보를 이용해서 다음과 같은 작업을 수행한다.
- JSESSIONID가 없는 경우: 세션 저장소에 새로운 번호로 공간을 만들고 해당 공간에 접근할 수 있는 객체를 반환, 새로운 번호는 브라우저에 JSESSIONID의 값으로 전송(세션 쿠키)
- JSESSIONID가 있는 경우: 세션 저장소에서 JSESSIONID 값을 이용해서 할당된 공간을 찾고 이 공간에 접근할 수 있는 객체를 반환
getSession()의 결과물은 세션 저장소 내의 공간인데 이 공간을 의미하는 타입은 HttpSession타입이라고 하고 해당 공간은 세션 컨텍스트(Session Context)혹은 세션(Session)이라고 한다.

HttpSession타입의 객체를 이용하며 현재 사용자만의 공간에 원하는 객체를 저장하거나 수정/삭제 할 수 있다.
아래 예제는 todolist를 등록하는 화면에 접속하기 위한 컨트롤러의 get요청 처리 코드이다.
만약 로그인한 사용자가 아니라면 todo를 추가할 수 없게 만들 수 있다.
- 로그인을 통해 JSESSIONID의 세션 컨텍스트에 로그인 정보 추가
- 사용자 인증이 필요한 컨트롤러에 getSession()을 통해 요청으로 받은 JSESSIONID의 세션 컨텍스트에 로그인 정보(아래 예제에서는 loginInfo)가 있는 지 확인
- 로그인 정보가 없다면 작업 거부 및 로그인 화면으로 redirect (로그인 화면에서 정상 로그인시 setAttribute로 세션 컨텍스트의 로그인 정보 추가)
- 로그인 정보가 있다면 작업 수행
사용자 인증이 필요한 게시물(todo)등록 컨트롤러(TodoRegisterController.java)
@Log4j2
@WebServlet(name="todoRegisterController", urlPatterns = "/todo/register")
public class TodoRegisterController extends HttpServlet {
private ToDoService toDoService = ToDoService.INSTANCE;
private final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("/todo/remove GET ....");
HttpSession session = req.getSession();
if(session.isNew()){
log.info("JSESSION ID 가 새로 만들어진 경우");
resp.sendRedirect("/login");
return;
}
//JSESSIONID는 있지만 해당 세션 컨텍스트에 loginInfo라는 이름으로 저장된 객체가 없는 경우 (로그인 안된 경우)
if (session.getAttribute("loginInfo") == null) {
log.info("로그인 정보가 없는 사용자");
resp.sendRedirect("/login");
return;
}
//정상적인 경우라면 입력 화면으로
RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/todo/register.jsp");
dispatcher.forward(req,resp);
}
아래 코드는 로그인을 통해 사용자의 JSESSIONID의 세션 컨텍스트에 login 정보를 등록하는 부분이다.
로그인 컨트롤러(TodoLoginController.java),로그아웃 컨트롤러(LogoutController.java)
@Log4j2
@WebServlet(name="todoLoginController", urlPatterns = "/todo/login")
public class TodoLoginController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("/todo/login GET");
req.getRequestDispatcher("/WEB-INF/todo/login.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("/todo/login POST");
String mid = req.getParameter("mid");
String mpw = req.getParameter("mpw");
// 사용자 마다 고유의 문자열 하나 생성,
String mix = mid + mpw;
HttpSession session = req.getSession();
session.setAttribute("loginInfo", mix);
resp.sendRedirect("/todo/list");
}
}
@Log4j2
@WebServlet("/logout")
public class LogoutController extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.info("/logout POST ...");
HttpSession session = req.getSession();
session.removeAttribute("loginInfo");
session.invalidate();
resp.sendRedirect("/login");
}
}