딸기말차
[Java web] 10. Cookie, Session, Filter, Listener 본문

엔코아 플레이데이터(Encore Playdata) Backend 2기 백엔드 개발 부트캠프 (playdata.io)
백엔드 개발 부트캠프
백엔드 기초부터 배포까지! 매력있는 백엔드 개발자 포트폴리오를 완성하여 취업하세요.
playdata.io
1. Session Tracking
1. 세션 트래킹?
사용자 입장에서 봤을 때, 로그인만 하면 다른 페이지에 들어가도 로그인 상태가 유지 되기에 로그인 상태를 각각의 웹 페이지들이 자동적으로 알고 있을 것이라 생각한다.
그러나 실제 HTTP 프로토콜 방식으로 통신하는 웹 페이지들은 서로 어떤 정보도 공유하지 않는다.
때문에, 웹 페이지 사이의 상태나 정보를 공유하려면 세션 트래킹(Session Tracking)이라는 웹 페이지 연결 기능을 구현해야 한다.
* 웹 페이지 연결 방법
1. <hidden> 태그: HTML의 <hidden> 태그를 이용해 웹 페이지들 사이의 정보를 공유
2. URL Rewriting: GET 방식으로 URL 뒤에 정보를 붙여서 다른 페이지로 전송
3. 쿠키: 클라이언트 PC의 Cookie 파일에 정보를 저장한 후 웹 페이지들이 공유
4. 세션: 서버 메모리에 정보를 저장한 후 웹 페이지들이 공유
2. 웹 브라우저 및 어플리케이션 내 저장소
보통 웹에서 사용되는 정보는 servlet의 비즈니스 로직 처리 기능을 이용해 데이터베이스에서 가져온다.
그러나 동시 사용자 수가 많아지면 데이터베이스 연동 속도도 영향을 받게 된다.
때문에 필요한 정보를 클라이언트의 PC나 서버의 메모리에 저장해두고 사용하면, 프로그램의 실행속도가 빨라진다.
대표적인 브라우저 및 어플리케이션 내 저장소
1. 쿠키
2. 세션
3. servletContext
2. Cookie
1. 쿠키?
웹 페이지들 사이의 공유 정보를 클라이언트 PC에 저장해 놓고 필요할 때 여러 웹 페이지들이 공유해서 사용할 수 있도록 하는 저장공간이다.
* 쿠키의 특징
1. 공유할 정보가 클라이언트 PC에 저장된다.
2. 저장 정보 용량에 제한이 있다. (약 4kb)
3. 보안이 취약하다. 때문에 유출되도 상관없는 정보만 쿠키를 사용한다.
4. 유출되면 안되는 정보는 서버 내부에 저장하면 된다.
5. 클라이언트의 브라우저에서 사용 유무를 설정할 수 있다.
6. 도메인당 쿠키가 생성된다. 즉, 웹 사이트당 하나의 쿠키 생성된다.
7. 쿠키는 동일 브라우저 상 여러 탭에서 공유 가능하다. 반면 다른 브라우저와는 공유되지 않는다.
8. 쿠키는 브라우저 상에 저장되기 때문에 기본적으로 접속 주소가 쿠키명이 된다.
2. 쿠키의 종류
1) Persistence 쿠키
클라이언트 PC에 파일로 정보를 저장하는 기능으로, 파일로 생성된 쿠키는 사용자가 만료 시간을 지정할 수 있다.
즉, 쿠키의 수명을 정해 브라우저를 종료해도 남아있을 수 있는 쿠키를 persistence 쿠키라 한다.
2) Session 쿠키
브라우저가 사용하는 메모리에 생성되는 쿠키로, 브라우저가 종료되면 메모리의 session 쿠키도 자동으로 소멸된다.
해당 쿠키는 session 기능과 같이 사용할 수 있다.
3. 쿠키 기능 실행 과정
1. 브라우저에 최초 접속 시 웹 서버에서 쿠키를 생성해 클라이언트로 전송
2. 브라우저 내부에 쿠키가 파일 형태로 저장
3. 이 후 다시 접속하면 서버가 브라우저에게 쿠키 전송을 요청, 서버에 쿠키 전송
4. 서버는 쿠키 정보를 이용해 작업
3. Session
1. 세션 ?
웹 페이지들 사이의 공유 정보를 서버에 저장해 두고 필요할 때 여러 웹 페이지들이 공유해서 사용할 수 있도록 하는 저장공간이다.
* 세션의 특징
1. 브라우저(사용자)당 한 개의 세션(세션 id)이 생성된다. 이 때 세션 id는 16진수로 서버에서 만들어준다.
2. 정보를 서버의 메모리에 저장한다.
3. 브라우저의 세션 연동은 세션 쿠키를 이용한다.
4. 쿠키에 비해 보안에 유리하다.
5. 유효 시간을 보유하고 있다. (기본 유효 시간 30분)
세션은 기본적으로 서버 내에 존재한다. 때문에 브라우저 상에서 세션 내의 데이터를 이용하고 싶다면, 서로 연결이 되어 있어야 한다.
이를 위해 톰캣과 같은 컨테이너는, jsessionId 라는 세션의 ID를 브라우저로 보내 쿠키로 등록한다.
2. 세션 기능 실행 과정
1. 클라이언트의 브라우저가 서버에 최초 접속
2. 서버의 서블릿은 세션 객체를 생성한 후 세션 객체에 대한 세션 id(jsessionId)를 톰캣을 통해 브라우저에 전송
3. 브라우저는 이 세션 id를 브라우저가 사용하는 세션 쿠키에 저장
4. 세션 쿠키에 저장된 세션 id(jsessionId)를 다시 서버로 전송
5. 서버에서는 전송된 세션 id(jsessionId)를 이용해 브라우저의 세션 객체에 접근
6. 브라우저에 대한 작업을 수행
4. 쿠키가 막혀있는 상태에서의 세션 실습
쿠키 사용이 막혀있는 상태에서 세션에 저장 된 로그인 정보를 사용하는 실습을 진행하였다.
해당 Servlet의 진행은 다음과 같다.
1. form 태그를 통해 입력받은 id, pw 정보를 request.getParameter를 통해 가져온다.
* if (session.isNew())
2. 만약 세션이 새로 생성 된 세션이고 id가 null이 아니라면, 세션에 id를 저장한다.
3. response.encodeURL을 통해 기존 URL을 세션의 ID인 jsessionId 을 포함시킨 URL로 변경한다.
4. 만약 새로 생성 된 세션이지만 id가 null이라면, 로그인 페이지로 다시 이동한다.
* else
5. 만약 세션이 새로 생성 된게 아니고 id가 null이 아니라면, id 정보를 출력한다.
6. 만약 세션이 새로 생성 된게 아니고 id가 null이라면, 로그인 페이지로 다시 이동한다.
private void doHandle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
PrintWriter out = response.getWriter();
HttpSession session = request.getSession();
String id = request.getParameter("id");
String pw = request.getParameter("pw");
if (session.isNew()) {
if (id != null) {
session.setAttribute("id", id);
String url = response.encodeURL("sess5");
out.print("<a href=" + url + ">로그인 상태 확인</a>");
}
else {
out.print("<a href='login2.html'>다시 로그인하세요!!!</a>");
session.invalidate();
}
}
else {
id = (String) session.getAttribute("id");
if (id != null && id.length() != 0) {
out.print("안녕하세요 " + id + "님!!!");
}
else {
out.print("<a href='login2.html'>다시 로그인하세요!!!</a>");
session.invalidate();
}
}
}
여기서 가장 헷갈리기 쉬운 부분인 response.encodeURL("url 정보") 에 대해 얘기하자면 다음과 같다.
1. 톰캣과 같은 컨테이너는 브라우저의 쿠키가 허용으로 되어있다면, 브라우저의 쿠키 내에 jsessionId 형태로 세션 ID를 저장한다.
2. 하지만 현재 실습은 쿠키가 허용되어있지 않기 때문에, 쿠키 내에 jsessionId를 저장할 수 없다.
3. 즉, 브라우저 개발자모드에서 확인해보면 쿠키 내에 jsessionId가 없다.
4. 때문에 우리는 이 상태에선 세션을 사용할 수 없는데, 현재 메서드 내에 비교문을 보면 세션의 여부에 따라 기능이 구현되어 있다.
5. 이 경우 세션정보를 요구하는 기능을 실행하기 전에, 브라우저에 jsessionId 정보를 보내 세션을 연결 해야한다.
6. response.encodeURL() 을 사용해 서블릿 주소를 치환하면, sess5;jsessionid=F59502FDB25F14DF2BFC8AF0A87FBEAD 이러한 형태가 된다.
7. 톰캣과 같은 컨테이너는 쿠키가 막혀있으면 컨테이너 내에서 URL Rewriting 기법을 통해 내가 사용하는 jsessionid를 기존 URL의 뒤쪽에 입력이 가능하다.
8. 즉, 치환 된 URL로 브라우저에 접속하면, 브라우저에 response 시 세션의 ID를 포함시켜 응답하는 것과 동일한 기능을 하기 때문에 세션을 사용할 수 있게 되고, doHandle() 내의 기능을 사용할 수 있게 된다.
5. Servlet의 속성과 Scope
1. 속성
servlet의 속성 (attribute) 란 다음 세 가지 servlet api 클래스에 저장되는 객체이다.
1) ServletContext
2) HttpSession
3) HttpServletRequest
* 사용 과정
1. servlet api의 setAttribute(String name, Object value) 로 바인딩
2. 필요할 때 getAttribute(String name)으로 바인딩 된 속성을 추출
3. removeAttribute(String name) 을 이용해 속성을 servlet api에서 제거
2. scope (범위)
servlet의 scope는 servlet api에 바인딩 된 속성(attribute) 에 대한 접근가능 범위를 의미한다.
1. servletContext에 바인딩된 속성 : 어플리케이션 전체에서 접근할 수 있으므로 어플리케이션 스코프
2. HttpSession에 바인딩된 속성 : 그 HttpSession에 해당하는 브라우저에만 접근할 수 있으므로 세션 스코프
3. HttpServletRequest : 해당 요청 & 응답에 대해서만 접근하므로 리퀘스트 스코프
ex) 로그인 상태 유지, 쇼핑몰 장바구니, MVC의 model과 view 간의 데이터 전달 (ModelAndView.class)
3. Scope 실습
setServlet에서 servlet의 속성들을 이용해 데이터를 담고, getServlet에서 가져와 출력해보는 실습을 진행하였다.
해당 실습의 결과는 request 속성으로 가져온 데이터만 null로 출력이 된다. 왜냐하면,
servletContext은 어플리케이션 전체, HttpSession은 서버 내의 Session에 데이터를 저장해 사용할 수 있기 때문에 다른 Servlet에서 가져다 사용할 수 있지만, HttpServletRequest는 클라이언트의 요청이 있어야 동작하는 속성이기 때문에 null이 출력되는 것이다.
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html; charset=utf-8");
PrintWriter out = response.getWriter();
ServletContext ctx = getServletContext();
HttpSession session = request.getSession();
String ctxMesg = (String)ctx.getAttribute("context");
String sesMesg = (String)session.getAttribute("session");
String reqMesg = (String)request.getAttribute("request");
out.print("context 값 : " + ctxMesg + "<br>");
out.print("session 값 : " + sesMesg + "<br>");
out.print("request 값 : " + reqMesg + "<br>");
}
6. Servlet의 URL 패턴
URL 패턴이란 실제 servlet의 매핑 이름이다. 즉, 클라이언트의 요청 시 톰캣 컨테이너는 해당 요청을 수행할 수 있는 servlet을 찾는데, 이 때 servlet의 @WebServlet 내 UrlPatterns에 적힌 주소를 보고 찾는다.
Servlet Mapping 이름으로 사용되는 URL 패턴의 종류
1. 정확히 이름까지 일치하는지 : "/first/login"
localhost:8080/first/login 처럼 정확한 경로로 들어가게 된다.
2. 디렉터리까지만 일치하는지 : "/first/*"
* 이 붙어있기 때문에 localhost:8080/first/디렉터리명 이렇게 first 안에있는 모든 디렉터리에 접근할 수 있게되고,
때문에 추후 controller를 통해 어떤 디렉터리에 접근할 것인지 정해줘야한다.
3. 확장자만 일치하는지 : "*.do"
이 경우 .do 로 끝나는 모든 경로에 접근할 수 있게 된다.
do는 예시이기 때문에, 자신이 사용해야되는 확장자에 맞춰 변경 또한 가능하다.
4. 무조건 실행 : "/*"
URL 패턴이란, 결국 해당 servlet에 접근해 실행하라는 것과 동일하다.
우리가 서버 컨테이너를 실행하고 브라우저를 띄우면, 기본적으로 localhost:8080/ 으로 시작하고,
이 후 요청에 따라 localhost:8080/first ~ 이런식으로 url이 변경된다.
즉, urlpatterns가 "/*" 로 정의되어 있는 servlet은 클라이언트의 요청 시 가장 먼저 동작한다고 볼 수 있다.
이 말은 요청에 따라 "/fisrt" 같은 servlet에 접근하기 전 반드시 선행되는 servlet이라는 뜻이다.
때문에 어떤 servlet에 접근하기 전 filter를 씌워야 할때(인코딩 등) 사용할 수 있다.
7. Filter
1. 필터 ?
브라우저에서 servlet에 요청하거나 응답에 관련해 여러 작업을 처리하는 기능이다. 우리가 개발 중 인코딩과 같이 각 servlet에서 공통적으로 처리해야하는 작업이 있을 수 있는데, 이 경우 filter를 적용할 수 있다.
2. 구현 ?
필터는 자바 내에서 interface 형태로 구현되어 있다. 때문에 사용자가 정의한 filter를 만들기 위해선 반드시 Filter interface를 구현해 사용해야한다.
해당 interface를 implement 하면, init(FilterConfig fConfig), doFilter(HttpServletRequest, HttpServletResponse, FilterChain), destroy() 메서드를 사용할 수 있다.
3. Filter 인터페이스 내부 메서드
1. init() : 필터 생성 시 컨테이너에 의해 호출, 초기화 작업을 수행
2. doFilter() : 요청 / 응답 시 컨테이너에 의해 호출, 기능을 수행
3. destroy() : 소멸 시 컨테이너에 의해 호출, 종료 작업을 수행
필터는 용도에 따라 크게 요청필터와 응답 필터로 나뉘며, Filter interface에서 implement 한 doFilter() 내에서 동작한다.
이 때, doFilter() 내 chain.doFilter(request, response) 코드를 기준으로 위쪽 부분은 요청필터, 아래쪽은 응답 필터로 동작한다.
1. 요청필터
1) 사용자 인증 및 권한 검사
2) 요청 시 요청 관련 로그 작업
3) 인코딩 기능
FilterChain.doFilter()
2. 응답 필터
1) 응답 결과에 대한 암호화 작업
2) 서비스 시간 측정
4. Filter 관련 API
1) javax.servlet.filter
2) javax.servlet.filterChain
3) javax.servlet.filterConfig
* FilterConfig의 메서드
1. getFilterName() : 필터 이름을 반환
2. getInitParameter(String name) : name에 대한 값을 반환
3. getServletContext() : servlet Context 객체를 반환
5. 생성한 Filter를 mapping 하는 방법
사용자가 정의한 필터를 생성하면, 생성한 필터를 각 요청에 맞게 적용해야 하기 때문에 mapping 작업을 해야한다.
1) 어노테이션
- 일반적으로 사용, xml에 비해 사용이 편리하다.
2) web.xml 내 설정
- xml은 text기반이라 컴파일이 없어서 빠르기 때문에, 필터를 수시로 바꿔야하는 경우에 사용한다.
6. Filter 구현 실습
현재까지 우리는 encoding 작업이 필요하면 request.setCharacterEncoding("UTF-8") 기능만을 사용했다.
해당 기능은 국내에서 사용할 땐 대부분 문제가 없지만, 해외 사이트에 접속 할 때 문제가 발생할 수도 있다.
때문에 직접 encoding 기능을 가진 filter를 만들어보는 실습을 진행하였다.
해당 servlet의 동작 과정은 다음과 같다.
1. @WebServlet 의 url을 "/*" 로 잡아, 어떤 servlet이 실행되더라도 선행되게 한다.
2. 요청 필터 내에서 long begin = System.currentTimeMillis(); 를 통해 요청 된 시간을 저장한다.
3. chain.doFilter(request, response); 를 통해 요청 / 응답 필터를 구분한다.
4. 응답 필터 내에서 long end = System.currentTimeMillis(); 를 통해 응답한 시간을 저장한다.
5. 응답시간 - 요청시간을 계산하여 클라이언트가 해당 페이지에서 작업했던 총 시간을 출력한다.
@WebFilter("/*")
public class EncoderFilter implements Filter {
ServletContext context;
public EncoderFilter() {
}
public void init(FilterConfig fConfig) throws ServletException {
System.out.println("utf-8 인코딩.........................");
context = fConfig.getServletContext();
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("doFilter 호출");
request.setCharacterEncoding("UTF-8");
String context = ((HttpServletRequest) request).getContextPath();
String pathinfo = ((HttpServletRequest) request).getRequestURI();
String realPath = request.getRealPath(pathinfo);
String mesg = "Context 정보 : " + context + "\nURI 정보 : " + pathinfo + "\n물리적 경로 : " + realPath;
System.out.println(mesg);
long begin = System.currentTimeMillis();
chain.doFilter(request, response);
long end = System.currentTimeMillis();
System.out.println("작업 시간 : " + (end - begin) + "ms");
}
public void destroy() {
System.out.println("destroy 호출");
}
}
8. Listener
1. Listener ?
화면에서 발생하는 이벤트를 처리할 수 있는 객체이다. 이는 대부분 자바스크립트로 해결할 수 있지만,
자바 또한 이벤트 발생 여부를 확인하는 Listener를 통해 이벤트를 처리할 수 있다.
대표적으로 HttpSessionBindingListener가 존재하는데, 해당 interface는 세션의 바인딩 이벤트를 처리하는 이벤트 핸들러가 구현되어있다.
우리는 session 객체의 setAttribute() 를 통해 세션에 저장하는 것을 바인딩이라하고,
removeAttribute() 를 통해 세션 내 데이터를 제거하는 것을 언바인딩이라 한다.
즉, 클라이언트에서 넘어온 정보를 세션에 저장하거나 제거하는 것을 하나의 이벤트라고 보고,
이벤트 발생여부를 확인할 수 있는 Listener를 통해 이벤트를 처리하는 객체가 HttpSessionBindingListener 이다.
* HttpSessionBindingListener
interface로 구현되어 있기 때문에 해당 interface를 implements 한 후 추상 메서드(이벤트 핸들러)를 override 해,
이벤트 발생 감지 시 원하는 동작을 메서드 내에 재정의 할 수 있다.
1. valueBound() : 세션에 바인딩 객체를 알려주는 이벤트 발생 시, 동작을 처리하는 이벤트 핸들러
2. valueUnbound() : 세션에 언바인딩 된 객체를 알려주는 이벤트 발생 시, 동작을 처리하는 이벤트 핸들러
2. HttpSessionBindingListener 실습
session.setAttribute() 메서드가 실행되는 것을 감지 시, 재정의 한 valueBound() 를 통해 총 유저수를 1 증가시키고,
session.removeAttribute() 가 실행되는 것을 감지 시, 재정의 한 valueUnbound() 를 통해 총 유저수를 1 감소시키는 실습을 진행하였다.
해당 클래스의 구조는 다음과 같다.
public class LoginImpl2 implements HttpSessionBindingListener {
String id;
String pw;
static int total = 0;
public LoginImpl2() {
}
public LoginImpl2(String id, String pw) {
this.id = id;
this.pw = pw;
}
@Override
public void valueBound(HttpSessionBindingEvent event) {
System.out.println("사용자 접속");
++total;
}
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
System.out.println("사용자 접속 해제");
total--;
}
}
HttpSessionBindingListener를 제외한 모든 Listener들은, Listener를 구현한 모든 이벤트 핸들러들을 반드시 어노테이션을 이용해서 리스너로 등록해야한다.
즉, HttpSessionBindingListener는 어노테이션을 사용하지 않아도 톰캣 컨테이너에서 해당 interface를 상속받은 클래스가 Listener 역할을 하는 클래스인 것을 알고 있지만,
일반적인 Listener 들은 @WebListener 를 붙여 톰캣 컨테이너에게 해당 클래스가 Listener 역할을 하는 클래스인 것을 알려줘야 한다.
3. HttpSessionListener 실습
해당 interface는 HttpSessionBindingListener가 아니다. 때문에 해당 interface를 implements 한 후, 이벤트 핸들러를 상속받아 구현해도 톰캣 컨테이너는 이 클래스가 Listener 역할을 하는 클래스 인것을 모른다.
이를 해결하기 위해 @WebLisner 어노테이션을 붙여, 톰캣 컨테이너에 해당 클래스가 Listener 인 것을 알렸다. 이 후 session.setAttribute() 메서드가 실행되는 것을 감지 시, 재정의 한 sessionCreated() 를 통해 총 유저수를 1 증가시키고
session.removeAttribute() 가 실행되는 것을 감지 시, 재정의 한 sessionDestroyed() 를 통해 총 유저수를 1 감소시켰다.
@WebListener
public class LoginImpl implements HttpSessionListener {
String id;
String pw;
static int total = 0;
public LoginImpl() {
}
public LoginImpl(String id, String pw) {
this.id = id;
this.pw = pw;
}
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("세션 생성");
++total;
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("세션 소멸");
total--;
}
}
9. 22일차 후기
웹 브라우저 내의 저장소인 쿠키와, 서버 내 저장소인 세션은 웹 개발을 하려면 반드시 숙지하고 다룰 줄 알아야하는 저장소이다.
일반 사용자 입장에선 웹 브라우저에 한번 로그인하면 브라우저를 종료하기 전까지 알아서 로그인 상태가 유지 되는줄 알지만, 실상은 개발자가 쿠키 및 세션을 사용해 브라우저 종료 전까지 해당 데이터를 계속 쥐고 있을 수 있도록 구현해야 하기 때문이다.
이번 실습에선 평소에 거의 사용하지 않았던 Filter와 Java 내의 Listener 를 사용했다. 기존엔 Filter 기능을 어노테이션을 활용해 처리했기 때문에 내부 기능을 모르고 사용했지만, 해당 실습을 통해 Filter 기능이 URL Mapping을 통해 동작한다는 것을 알게 되었다.
Java의 Listener 또한 이전엔 이벤트 처리를 대부분 자바스크립트 내에서만 처리했기 때문에 해당 클래스가 존재한다는 것조차 몰랐지만, 이번 실습을 통해 해당 기능을 사용하려면 톰캣 컨테이너에 Listener 를 알려야 동작한다는 것을 알게 되었다.
'Bootcamp > Java web' 카테고리의 다른 글
[Java web] 12. MVC, 외래키, Oracle 함수, 계층형 쿼리 (1) | 2023.08.02 |
---|---|
[Java web] 11. File Up/Download, JQuery, Ajax, Json (0) | 2023.08.01 |
[Java web] 9. Servlet Context, Servlet Config (0) | 2023.07.27 |
[Java web] 8. Oracle JDBC, DispatcherServlet (0) | 2023.07.25 |
[Java web] 7. Web 복습, Oracle (0) | 2023.07.24 |