HTTP 프로토콜은 무상태(stateless) 연결입니다.
이런 특징으로 인해 기존의 방문자를 기억하기 위해서는 특별한 메커니즘을 사용하게 되는데, 이러한 기법들을 세션 트랙킹(session tracking)이라고 합니다.
세션 트랙킹(session tracking)
HTTP에서 세션 트랙킹은 '쿠키(Cookie)'라는 존재를 이용합니다. '쿠키'는 문자열로 만들어진 데이터의 조각으로 서버와 브라우저 사이에서 요청(Request)이나 응답(Response)시에 주고받는 형태로 사용됩니다.
쿠키(Cookie)
쿠키는 문자열로 되어있는 정보로 가장 기본적인 형태는 '이름(name)'과 '값(value)'의 구조입니다. 브라우저에서는 개발자 도구의 '애플리케이션(application)'메뉴를 이용해서 확인할 수 있습니다.
쿠키를 주고받는 기본적인 시나리오는 다음과 같습니다.
1. 브라우저에서 최초로 서버를 호츌하는 경우에 해당 서버에서 발행한 쿠키가 없다면 보라우저는 아부것도 전송하지 않습
니다.
2. 서버에서는 응답(Response) 메시지를 보낼 때 보라우저에게 쿠키룰 보내주는데 이때 ‘set- Cookie’라는 HTTP 헤더를
이용합니다.
3. 브라우저는 쿠키를 받은 후에 이에 대한 정보를 읽고, 이를 파일 형태로 보관할 것인지 메모리상에서만 처리할 것인지
를 결정합니 다. 이 판단은 쿠키에 있는 “유효기간(만료기간)”을 보고 판단합니다 .
4. 브라우저가 보관하는 쿠키는 다음에 다시 브라우저가 서버에 요청(Request)할 때 HTTP 헤더에 ‘cookie’라는 헤더 이름
과 함께 전달합니다(쿠키에는 경로(path)를 지정할 수 있어서 해당 경로에 맞는 쿠키가 전송).
5. 서버에서는 필요에 따라서 브라우저가 보낸 쿠키를 읽고 이를 사용합니다.
쿠키를 생성하는 방법
서버에서 쿠키를 발행하는 것은 두가지 방법이 존재합니다.
1. 서버에서 자동으로 생성하는 쿠키
● 응답 메시지를 작성할 때 정해진 쿠키가 없는 경우 자동으로 발행
● WAS에서 발행되며 이름은 WAS마다 고유한 이름을 사용해서 쿠키를 생성
● 톰캣은 'JSESSIONID'라는 이름을 이용
- 기본적으로 브라우저의 메모리상에 보관하기에 브라우저를 종료하면 서버에서 발행한 쿠키는 삭제됩니다.
- 서버에서 발행하는 쿠키의 경로는 '/'로 지정됩니다.
2. 개발자가 생성하는 쿠키
● 개발자가 생성하는 쿠키는 서버에서 생성되는 쿠키와 다음과 같은 점들이 다릅니다.
- 이름을 원하는대로 지정할 수 있습니다.
- 유효기간을 지정할 수 있습니다(유효기간이 지정되면 브라우저가 이를 파일의 형태로 보관)
- 반드시 직접 응답(Response)에 추가해 주어야 합니다.
- 경로나 도메인 등을 지정할 수 있습니다.(특정한 서버의 경로를 호출하는 경우에만 쿠키를 사용)
서블릿 컨텍스트(ServletContext)와 세션 저장소(Session Repository)
하나의 톰캣은 여러 개의 웹 애플리케이션(웹 프로젝트)을 실행할 수 있습니다. 실제 운영의 경우 나의 웹 애플리케이션마다 별도의 도메인으로 분리해서 운영됩니다. (다음 그림은 4개의 웹 애플리케이션이 실행되는 톰캣 구조를 표현)
서블릿 컨텍스트(ServletContext)
각각의 웹 애플리케이션은 자신만이 사용하는 고유의 메모리 영역을 하나 생성해서 이 공간에 서블릿이나 JSP 등을 인스턴스로 만들어 서비스를 제공합니다. 이 영역을 서블릿 API에서는 서블릿 컨텍스트라고 합니다.
세션 저장소(Session Repository)
각각의 웹 애플리케이션을 생성할 때는 톰캣이 발행하는 쿠키 (개발자가 생성하는 쿠키와 구분하기 위해서 세션 쿠키라고 함)들을 관리하기 위한 메모리 영역이 하나 더 생성되는데 이 영역을 세션 저장소(Session Repository)라고 합니다.
세션 저장소는 기본적으로는 '키(key)'와 '값(value)'을 보관하는 구조입니다. 이때 키가 되는 역할을 하는 것이 톰캣에서 JSESSIONID라는 쿠키의 값이 됩니다.
서버에서는 브라우저가 가지는 JSESSIONID 쿠키의 값을 키 (Key)로 보관하게 됩니다.
역할
톰캣 내부의 세션 저장소는 발행된 쿠키들의 정보를 보관하는 역할을 하게 됩니다. 하지만 JSESSIONID 쿠키가 만들어 질때마다 메모리 공간을 차지해야 한다는 점입니다. 이 문제를 해결하기 위해서 톰캣은 주기적으로 세션 저장소를 조사하면서 더 이상 사용하지 않는 값들을 정리하는 방식으로 동작합니다.
세션 저장소 정리 방법
값을 정리하는 방식은 SESSION-TIMEOUT 설정을 이용합니다. 지정된 시간보다 오래된 값들은 주기적인 검사과정에서 삭제하는 방식입니다. 톰캣의 경우 기본은 30분 입니다.
세션을 통한 상태 유지 메커니즘
코드상에서 HttpServletRequest의 getSession()이라는 메소드를 실행하면 톰캣에서는 JSESSIONID 이름의 쿠키가 요청(Request)할 때 있었는지 확인하고 없다면 새로운 값을 만들어 세션 저장소에서 보관합니다.
다음은3개의 브라우저가 처음으로 세션이 필요한 경로를 요청했다고 가정하고, JSESSIONID 값이 각각 'A1234', 'B111', 'C333'과 같았다고 가정한 세션 저장소의 구조입니다.
세션 저장소에서는 JSESSIONID의 값마다 고유한 공간을 가지게 되는데 이 공간은 다시 '키(Key)'와 값(value)'으로 데이터를 보관할 수 있습니다. 이 공간들을 이용해서 서블릿/JSP 등은 원하는 객체들을 보관할 수 있는데 사용자들마다 다른 객체들을 다음과 같은 형태로 보관할 수 있게 됩니다.
위의 세션 내용에 'login 정보'가 존재하는데 서버에서 프로그램을 작성할 때에는 이를 이용해서 해당 사용자가 로그인했다는 것을 인정하는 방식입니다. 서블릿 API에서는 HttpServletRequest를 통해 getSession()이라는 메소드로 각 JSES-SIONID의 공간에 접근할 수 있습니다.
HttpServletRequest의 getSession()
HttpServletRequest의 getSession()은 브라우저가 보내는 정보를 이용해서 다음과 같은 작업을 수행합니다.
- JSESSIONID가 없는 경우: 세션 저장소에 새로운 번호로 공간을 만들고 해당 공간에 접근할 수 잇는 객체를 반환
새로운 번호는 브라우저에 JSESSIONID의 값으로 전송(세션쿠키)
- JSESSIONID가 있는 경우: 세션 저장소에서 JSESSIONID 값을 이용해서 할당된 공간을 찾고 이 공간에 접근할 수 있는
객체를 반환
getSession()의 결과물은 세션 저장소 내의 공간인데 이 공간을 의미하는 타입은 HttpSession 타입이라고 하고 해당 공간을 '세션 컨텍스트(Session Cotext) 혹은 세션(Session)' 이라고 합니다.
HttpSession 타입의 객체를 이용하며 현재 사용자만의 공간에 원하는 객체를 저장하거나 수정/삭제할 수 있습니다. 또한 isNew( )와 같은 메소드로 새롭게 공간을 만들어 낸것인지 기존의 공간을 재사용하는지를 구분할 수 있습니다.
필터
언제 사용하는가?
세션을 통해 로그인 정보를 저장해두면 접근권한 기능을 구현할 수 있습니다. 하지만 로그인 여부를 체크해야 하는 컨트롤러마다 동일하게 체크하는 로직을 작성하면 같은 코드를 계속 작성해야 하기 때문에 대부분은 필터(Servlet Filter)라는 것을 이용해서 처리합니다.
정의
필터는 말 그대로 특정한 서블릿이나 jsp 등에 도달하는 과정에서 필터링하는 역할을 위해서 존재하는 서블릿 API의 특별한 객체입니다. @WebFilter 어노테이션을 이용해서 특정한 경로에 접근할 때 필터가 동작하도록 설계하면 동일한 로직을 필터로 분리할 수 있습니다. 필터는 여러 개를 적용할 수 있어서 다음과 같은 형태를 구성할 수 있습니다.
사용 방법
javax.servlet.Filter 인터페이스의 doFilter( )는 HttpServletRequest/HttpServletResponse보다 상위 타입의 파라미터를 사용하므로 HTTP와 관런된 작업을 하려면 (HttpServletRequest)request와 같이 다운캐스팅 해주어야 합니다.
1. Filter 인터페이스 구현
2. doFilter 오버라이딩 (필터가 필터링이 필요한 로직을 구현하는 부분)
3. 클래스 위에 @WebFilter 어노테이션 추가
- @WebFilter 어노테이션에 특정한 경로를 지정해서 해당 경로의 요청에 대해서 doFilter()를 실행하는 구조
4. 마지막에 다음 필터나 목적지(Servlet, JSP)로 갈 수 있도록 FilterChain의 doFilter()를 실행
5. 만약 문제가 생겨서 진행 불가일 시 리다이렉트 처리 가능
UTF-8 필터 생성
인코딩 필터를 생성해 모든 페이지에 인코딩을 설정할 수 있습니다.
사용자 정의 쿠키(Cookie)
일반적으로 쿠키(Cookie)라고 하면 개발자의 필요에 의해서 생성되어 브라우저에 전송하는 '사용자 정의 쿠키'를 일겉는 경우가 많습니다.
쿠키의 생성/전송
- 사용자가 정의하는 쿠키의 경우 서버에서 자동으로 발행되는 쿠키(JSESSIONID)와 비교했을 떄 다음과 같은 점들이 다릅니다.
사용자 정의 쿠키 | WAS에서 발행하는 쿠키 (세션 쿠키) |
|
생성 | 개발자가 직접 newCookie()로 생성 경로도 지정 가능 - 이름(name)과 값(value)이 필요 - 값(value)은 URLEncoding된 문자열로 저장 (한글저장 불가) |
자동 |
전송 | 반드시 HttpServletResponse에 addCookie()를 통해야만 전송 | |
유효기간 | 쿠키 생성할 때 초 단위로 지정할 수 있음 | 지정불가 |
브라우저의 보관방식 | 유효기간이 없는 경우에는 메모리상에만 보관 유효기간이 있는 경우에는 파일이나 기타 방식으로 보관 |
메모리상에만 보관 |
쿠키의 크기 | 4kb | 4kb |
쿠키를 사용하는 경우
쿠키는 서버와 브라우저 사이를 오가기 때문에 보안에 취약한 단점이 있습니다. 이 때문에 쿠키의 용도는 상당히 제한적일 수밖에 없습니다.
오랜 시간 보관해야 하는 데이터는 항상 서버에 보관하고, 약간의 편의를 제공하기 위한 데이터는 쿠키로 보관하는 방식을 사용합니다.
예를 들어 '오늘 하루 이 창 열지 않기', '최근 본 상품 목록'과 같이 조금은 사소하고 서버에서 보관할 필요가 없는 데이터들을 쿠키를 이용해서 처리됩니다. 쿠키의 위상이 변하게 된 가장 큰 이유는 모바일에서 시작된 '자동 로그인' 입니다. 쿠키는 유효기간을 지정하는 경우 브라우저가 종료되더라도 보관하는 방식으로 동작하게 되는데 모바일에서는 매번 사용자가 로그인하는 수고로움을 덜어줄 수 있게 됩니다.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Long tno = Long.parseLong(req.getParameter("tno"));
TodoDTO todoDTO = todoService.get(tno);
// 모델 담기
req.setAttribute("dto", todoDTO);
// 쿠키 찾기
Cookie viewTodoCookie = findCookie(req.getCookies(), "viewTodos");
String todoListStr = viewTodoCookie.getValue();
boolean exist = false;
if (todoListStr != null && todoListStr.indexOf(tno + "-") >= 0) {
exist = true;
}
log.info("exist: " + exist);
if (!exist) {
todoListStr += tno + "-";
viewTodoCookie.setValue(todoListStr);
viewTodoCookie.setMaxAge(60 * 60 * 24);
viewTodoCookie.setPath("/");
resp.addCookie(viewTodoCookie);
}
req.getRequestDispatcher("/WEB_INF/todo/read.jsp").forward(req, resp);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
throw new ServletException("read error");
}
}
private Cookie findCookie(Cookie[] cookies, String cookieName) {
Cookie targetCookie = null;
if (cookies != null && cookies.length > 0) {
for (Cookie ck : cookies) {
if (ck.getName().equals(cookieName)) {
targetCookie = ck;
break;
}
}
}
if (targetCookie == null) {
targetCookie = new Cookie(cookieName, "");
targetCookie.setPath("/");
targetCookie.setMaxAge(60 * 60 * 24);
}
return targetCookie;
}
위와 같이 하루동안 조회했던 목록을 쿠키에 저장해 '조회수'를 처리하거나 '최근 본 상품 목록'을 처리할 수 있습니다.
자동로그인 기능 구현 (remem-ber-me, 쿠키와 세션을 같이 활용)
로그인한 사용자의 정보를 쿠키에 보관하고, 이를 이용해서 사용자의 정보를 HttpSession에 담는 방식
시큐리티 등 다양하게 고려해야 하지만 여기서는 간단한 자동로그인을 구현할 예정입니다.
로그인 구현
- 사용자가 로그인할 때 임의의 문자열을 생성하고 이를 데이터베이스에 보관 (UUID 적용)
- 쿠키에는 생성된 문자열을 값으로 삼고 유효기간은 1주일로 지정
로그인 체크
- 현재 사용자의 HttpSession에 로그인 정보가 없는 경우에만 쿠키를 확인
- 쿠키의 값과 데이터베이스의 값을 비교하고 같다면 사용자의 정보를 읽어와서 HttpSession에 사용자 정보를 추가
필터를 적용해 로그인 체크 시, 쿠키를 체크하도록 변경하고 저장되어 있을 경우 조회한 뒤 세션에다가 로그인 정보를 저장해둡니다.
리스너(Listener)
서블릿 API에는 리스너라는 이름이 붙은 특별한 인터페이스들이 존재합니다. 리스너 객체들은 이벤트라는 특정한 데이터가 발생하면 자동으로 실행되는 특징이 있습니다. 기존의 코드를 변경하지 않고도 추가적인 기능을 수행할 수 있습니다. 스프링 MVC가 리스너를 통해서 동작합니다.
- 해당 웹 애플리케이션이 시작되거나 종료될 때 특정한 작업을 수행
- HttpSession에 특정한 작업에 대한 감시와 처리
- HttpServletRequest에 특정한 작업에 대한 감시와 처리
ServletContextListener
- 프로젝트가 실행되자마자/종료할때 실행되었으면 하는 작업을 위해 사용
- 클래스 위에 @WebListener 어노테이션 추가
커넥션 풀 초기화, TodoService와 같은 객체들을 미리 생성해서 보관할 수 있습니다.
스프링 프레임워크를 웹 프로젝트에서 미리 로딩하는 작업을 처리할 때 ServletContextListener를 이용합니다.
HttpSessionListener/HttpSessionAttributeListener
- HttpSession 관련 작업을 감시하는 리스너
- HttpSession 이 생성되거나 setAttribute 등의 작업이 이루어질 때 이를 감지
- attributeAdded(), attributeRemoved(), attributeReplaced() 등을 통해 setAttribute(), removeAttribute() 등의 작업 감지
'웹 개발 > 웹 프레임워크' 카테고리의 다른 글
JSP에서 서버가 동작하는 순서 (0) | 2023.02.17 |
---|---|
웹 프레임워크에 입문하기 위한 기초 지식 (서블릿과 WAS) (0) | 2023.02.13 |
웹 개발자가 알아야 되는 HTTP 프로토콜 (1) | 2022.11.24 |