본문 바로가기
국비학원

[국비지원] KH 정보교육원 81일차

by 도전하는 개발자 2022. 7. 22.

세션 (Session) : 추상적인 연결, 계속 유지되는 연결

*HTTP protocol (통신규약)의 특징
1) 연결이 유지되지 않는다
2) 연결생성 > 요청전송 > 응답수신> 연결종료
3) Connectionless, Stateless Protocol 
 => 물리적인 세션 (지속적 연결)의 생성은 불가능!
 => 추상적인 연결로 세션을 생성/관리 (2가지 방법)
  1. HttpSession 객체를 이용하는 방법
  2. Cookie 객체를 이용하는 방법

---

*** 3. 세션관리***

 일반적으로 사용되는 세션의 정의는 ‘서버와 클라이언트간의 지속적인 연결’을 의미한다. 연결을 통하여 클라이언트는 지속적으로 서버에 특정 동작을 요청할 수 있으며 서버는 실행 결과를 클라이언트에 응답할 수 있다. 데이터베이스를 사용하는 경우에도 클라이언트와 DB서버간에 지속적인 연결을 의미하는 세션이 필요하다. 하지만 HTTP 프로토콜을 기반으로 하는 웹서비스는 동작 메커니즘이 다르다. 불특정 다수인 클라이언트와 지속적인 연결방식으로는 웹서버의 부하가 매우 크기 때문이다. 동시 접속자가 100만건이라고 가정했을 때, 100만 클라이언트와 지속적으로 연결된 서버가 동작하는 것은 불가능하다. 따라서 클라이언트가 웹서버에 요청하고 응답 받으면 즉시 연결을 끊는 connectionless 방식으로 동작된다. 이것은 웹브라우저의 각 페이지는 서로 간에 연결고리가 없다는 것을 의미한다. 즉, 첫 화면에서 선택한 물건을 장바구니에 담고 다음 페이지에서 결재할 때 이전 화면의 장바구니에 담긴 정보를 확인 할 수 없다는 것이다. 하지만 서비스되고 있는 많은 웹 사이트를 보면 장바구니 및 로그인 기능 같은 처리가 구현되어 있다. 이처럼 HTTP 프로토콜의 문제점을 극복하기 위해서 사용자의 상태 정보를 관리하는 메커니즘이 필요한데 이것을 ‘세션 관리’라고 한다.


세션 관리는 일반적으로 다음과 같이 2가지 방법이 제공된다. 
1) HttpSession 클래스를 이용한 세션처리 방법
2) Cookie 클래스를 이용한 쿠키처리 방법

---

1) HttpSession 클래스를 이용한 세션처리 방법

세션(session)이란 사용자의 상태정보를 서버에서 관리하는 메커니즘을 의미한다. 세션의 정보는 클라이언트가 서버에 접속해서 종료될 때까지 (브라우저를 종료할 때까지) 유지된다. 상태정보가 서버에 저장되기 때문에 서버의 부하가 클 수 있다. 따라서 time-out 제한을 두어 일정시간(기본 30분)요청이 없으면 서버는 세션 정보를 유지하지 않고 제거한다.

다음은 세션의 전체적인 아키텍쳐이다



 A클라이언트(B클라이언트)가 서블릿에 요청을 하면, 서블릿에서는 getSession() 메소드를 사용하여 session영역을 생성하고 A클라이언트의 고유 식별값인 세션ID를 생성하여 session영역에 저장시킨다. 일반적으로 session영역에는 장바구니 정보 및 로그인 정보등을 저장한다. 서블릿의 실행결과가 응답 처리될 때 자동으로 세션ID값이 포함되며, 동일한 브라우저에서 재요청이 발생되면 세션ID값을 포함하여 요청처리 된다. 따라서 서버는 재요청에 포함된 세션ID값을 이용하여 클라이언트와 연결기능을 유지할 수 있다. 이때 A클라이언트가 일정시간동안(기본 30분) 요청을 하지 않으면 서버는 클라이언트 정보를 제거할 목적으로 session영역을 삭제한다. 로그아웃 같은 기능을 구현하기 위해서 session영역을 즉시 삭제할 수도 있다. 


다음은 세션를 제거하는 방법이다. 



---

http://localhost:8080/AbstractSession
http://localhost:8080 -> 우리의 웹사이트 주소

*쿠키(Cookie) 
1)아주 작은 데이터를 의미
2)백엔드 서버에서 "최초요청"을 받았을 때 "웹브라우저"에게 쿠키를 주는데
   그 쿠키의 이름은 "JSESSIONID"이고, 값은 "무작위 문자열"임
   이 값이 브라우저의 이름이 된다? 이게 Session ID임
3) 응답문서의 헤더(Set-Cookie)에 쿠키가 [이름=값] 형태로 저장되어 전송

* 웹브라우저는 특정 웹사이트 주소로 최초요청을 보내면 세션아이디를 쿠키로 받게 되고 이를 웹사이트 주소마다 다른 쿠키파일로 저장함!  이후, 두번째 이상 요청부터는 동일한 웹사이트로 요청을 보낼 때마다 해당 웹사이트 주소로 보관된 쿠키파일이 존재한다면 이를 읽어서 다시 서버로 전송  (요청문서의 헤더에 저장되어 전송됨)
  - 언제까지 요청 헤더에 쿠키값을 저장해서 전송하는가? -> 웹 브라우저가 죽을 때까지

@Log4j2
@NoArgsConstructor

@WebServlet("/AbstractSession")
public class AbstractSessionServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		log.trace("service(req, res) invoked.");
		
		HttpSession session = req.getSession();
		String sessionId = req.getRequestedSessionId();
		
		log.info("\t+ 1. session : {}", session);
		log.info("\t+ 2. sessionId : {}", sessionId);
	
	} // service

} // end class

AbstractSessionServlet.java

최초 요청시 SessionID가 Null 이지만 그 이후의 요청시는 SessionID 값이 나온다.
최초 요청시 SessionID가 생성되고 그 이후의 요청시부터 이를 서버로 전송하기 때문.
이 값은 웹 브라우저가 죽을 때까지 유지된다.



---

web.xml에 session-config 추가해주자


---

세션을 이용한 장바구니 실습

@Log4j2
@NoArgsConstructor

@WebServlet("/CartSave")
public class CartSaveServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
    
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		log.trace("service(req, res) invoked.");
		
		// Step1. 장바구니에 저장할 상품항목을 전송파라미터로 획득
		req.setCharacterEncoding("utf8");
		
		String product = req.getParameter("product");
		log.info("\t+ product : {} ", product);
		
		// Step2. 장바구니 생성 및 수신된 상품을 장바구니에 추가
		HttpSession sess = req.getSession();
		
		@SuppressWarnings("unchecked")
		List<String> list = (List<String>) sess.getAttribute("basket");
		
		if(list==null) { // Session Scope에서 장바구니가 없으면
			list = new ArrayList<>();          // 장바구니 새로 만들고
			sess.setAttribute("basket", list); // Session Scope에 올려놓고
		} //if-else
		
		list.add(product);
		log.info("\t+ list : {}", list);
		
		// 응답화면 생성 및 전송
		res.setContentType("text/html; charset=utf8");
		
		@Cleanup
		PrintWriter out = res.getWriter();
		
		out.println("<h1>장바구니에 담기 성공</h1>");
		out.println("<a href = '/CartBasket'> 장바구니 보기 </a>");
		
		out.flush();	
	}// service

} // end class

CartSaveServlet.java

 

@Log4j2
@NoArgsConstructor

@WebServlet("/CartBasket")
public class CartBasketServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
    
	@SuppressWarnings("unchecked")
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		log.trace("service(req, res) invoked.");
		
		List<String> list = null;
		
		try {
			HttpSession sess = req.getSession(false);
			log.info("\t+ sess: {} ", sess);
			
			Objects.requireNonNull(sess);
			
			list = (List<String>) sess.getAttribute("basket");
			
			Objects.requireNonNull(list);
			
			list.forEach(log::info);
		} catch (Exception e) {
			throw new ServletException(e);
		} // try-catch
		
		// 응답문서생성
		res.setContentType("text/html; charset=utf8");
		
		@Cleanup
		PrintWriter out = res.getWriter();
		
		out.println("<h1>장바구니 내용</h1>");
		out.println("<ol>");
		
		list.forEach(s -> {
			out.println("\t<li>" + s + "</l1>");
		}); // forEach
		
		out.println("<ol>");
		
		out.println("<a href='/CartDelete'> 장바구니 비우기 </a>");
		
		out.flush();
	}// service

} // end class

CartBasketServlet.java

 

 

@Log4j2
@NoArgsConstructor

@WebServlet("/CartDelete")
public class CartDeleteServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
    
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		log.trace("service(req, res) invoked.");
		
		// 장바구니 비우기 : 현재 브라우저의 세션아이디로 식별되는
		// Session Scope 공유영역 파괴 + 세션아이디 무효화
		HttpSession sess = req.getSession(false);
		
		try {
			Objects.requireNonNull(sess);
			
			sess.invalidate(); // Session ID 무효화 + Session Scope 영역 파괴!
			
			// 마지막 응답 페이지는 요청 포워딩을 통해 JSP에서 생성하게함			
			RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/cartdelete.jsp");
			dispatcher.forward(req, res);
			
		} catch (Exception e) {
			throw new ServletException(e);
		} // try-catch
	
	}// service

} // end class

CartDeleteServlet.java

 


/WEB-INF/views/caetdelete.jsp를 이클립스에서 만들어준다

 

---

2) Cookie 클래스를 이용한 처리 방법

쿠키는 사용자의 상태정보를 클라이언트에서 관리하는 메커니즘을 의미한다. 클라이언트에 정보가 저장되기 때문에 서버의 부하가 크지 않지만 보안에 매우 취약하다. 쿠키는 웹사이트의 도메인당 300개까지 저장이 가능하며, 또한 클라이언트에서 쿠키사용을 못하도록 설정할 수 있기 때문에 제약이 있다. 쿠키는 클라이언트의 브라우저 메모리 및 OS파일에 저장 가능하다. 기본 저장은 브라우저 메모리이기 때문에 브라우저를 종료하면 자동으로 쿠키정보도 제거된다. 쿠키 만료시간은 setMaxAge(sec) 메소드를 사용하며 지정된 시간까지 OS파일에 저장된다. 

다음은 쿠키의 전체적인 아키텍쳐이다

 

@WebServlet("/CartSaveCookie")
public class CartSaveCookieServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
    
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		log.trace("service(req, res) invoked.");
		
		// step1. 화면에서 전송한 장바구니에 저장할 품목 (전송파라미터)를 획득
		String product = req.getParameter("product");
	
		// step2. 요청메시지의 헤더에 포함되어 있는 모든 쿠키 획득
		Cookie[] cookies = req.getCookies();
		
		// step3. 우리가 직접 새로운 쿠키를 생성하자
		Cookie cookie = null;
		
		if( cookies == null || cookies.length == 0 ) { // 위 스텝2에서 얻는 쿠키 배열이 무효하면
			cookie = new Cookie("product", product);   // 새로운 쿠키객체 생성
		} else {
			cookie = new Cookie("product" + (cookies.length + 1 ), product); // 새로운 쿠키객체 생성
		} //if-else
		
		// step4. 우리가 생성한 쿠키의 만료기간 설정
		cookie.setMaxAge(60 *60); // 1시간동안 유지 (브라우저에서)
		
		// step5. 응답메시지의 헤더에, 우리가 위 스텝3에서 생성한 쿠키객체를 저장
		res.addCookie(cookie); // 응답문서 헤더에 새로운 쿠키를 추가하여 응답으로 보냄
		
		// step6. 응답문서 생성 및 전송
		res.setContentType("text/html; charset=utf8");
		
		@Cleanup
		PrintWriter out = res.getWriter();
		
		out.print("Ptoduct 추가 <br>");
		out.print("<a href='/CarBasketCookie'> 장바구니 보기 </a>");

		out.flush();
	}// service

} // end class

CartSaveCookieServlet.java

 

@Log4j2
@NoArgsConstructor

@WebServlet("/CartBastketCookie")
public class CartBastketCookieServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
    
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		log.trace("service(req, res) invoked.");
		
		// step1. 나의 모든 장바구니 항목을 담고있는 쿠키목록을 웹브라우저가
		// request message의 헤더에 담아서 보내게 되어있다. (웹브라우저의 기본동작)
		Cookie[] basket = req.getCookies();
		log.info("* basket : {}", Arrays.toString(basket));
		
		// step2. 응답문서 생성 (step1에서 얻은 장바구니 목록을 이용)
		res.setContentType("text/html charset=utf8");
		
		@Cleanup
		PrintWriter out = res.getWriter();
		
		if(basket == null) {
			out.print("장바구니 비었음 <br>");
		} else { // 장바구니에 여러 항목이 있다면 (쿠키로 저장되어 있다면)
			for(Cookie cookie : basket) {
				String name = cookie.getName();   // 서버에 전달된 각 쿠키의 이름 획득
				String value = cookie.getValue(); // 서버에 전달된 각 쿠키의 값 획득
				
				out.print("<h2>" + name + ":" + value + "</h2>");
			} // enhanced for
		} // if-else
		
		out.print("<a href = 'product.html'> 상품선택 페이지 </a> <br>");
		out.print("<a href = '/CartDeleteCookie'> 장바구니 비우기 </a>");
		
		out.flush();
	}// service

} // end class

CartBasketCookieServlet.java

 

@Log4j2
@NoArgsConstructor

@WebServlet("/CartDeleteCookie")
public class CartDeleteCookieServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
    
	// 클라이언트 브라우저에 쿠키파일로 저장되어 있는 모든 장바구니 데이터를 삭제
	// 어떻게? => 각 쿠키의 만료 기간을 조작!!!
	
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res) 
			throws ServletException, IOException {
		log.trace("service(req, res) invoked.");
		
		// step1. 웹 브라우저가 보낸 request 메시지의 헤더에 있는 모든 쿠키를 배열로 획득
		Cookie[] basket = req.getCookies();
		
		// step2. 각 쿠키의 만료기간을 1로 바꾸어서 바로 만료되게 만듦
		if(basket != null) { // 장바구니가 있다면
			for(Cookie cookie : basket) {
				cookie.setMaxAge(1); // 만료기간 1초로 변경 (바로 삭제)
				
				// 1초의 만료기간으로 변경된 쿠키들을 다시 웹 브라우저로 보내면
				// 웹 브라우저는 이 사이트 주소에 대해 파일로 보관중이던 
				// 모든 쿠키를 1초후에 파괴시켜버림
				res.addCookie(cookie); // 시간만료가 걸정된 쿠키를 브라우저로 전송
			} // enhanced for
		} // if
		
		// 응답문서 생성 및 전송
		res.setContentType("text/html charset=utf8");
		@Cleanup
		PrintWriter out = res.getWriter();
		
		out.print("<a href='product.html'>상품 선택 페이지</a>");
		
		out.flush();
	
	}// service

} // end class

CartDeleteCookieServlet.java

------------------------------------------

*** 4. 파일 업로드 및 다운로드 기능


1. @MultipartConfig 어노테이션을 이용한 파일 업로드
파일 업로드 기능을 구현하기 위한 여러 가지 방법중에서 현재까지 가장 많이 사용되고 알려진 것은 아파치 그룹에서 제공하는 Commons FileUpload 라이브러리이다. Spring 프레임워크 및 Struts2 프레임워크 같은 유명한 프레임워크에서도 사용되는 매우 안정적인 라이브러리이다. 하지만 서블릿 3.0 버전부터는 @MultipartConfig 어노테이션 javax.servlet.http.Part 인터페이스를 사용하여 보다 쉽게 파일 업로드 기능을 구현할 수 있다.

먼저, webapp/resources/uploadForm.html을 만들어주자!

 

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>FileUpload 실습</title>
</head>

<body>
    <h1>/resources/uploadForm.html</h1>
    <hr>
    
    <!-- 파일업로드 태그가 포함된 Form은, 반드시 enctype 속성의 값으로 아래와 같이 지정해야함 (표준) -->
    <form action="http://localhost:7000" method="post" enctype="multipart/form-data">

        1. 작성자 : <input type="text" name="writer"><br><br>
        2. 업로드 파일 : <input type="file" name="uploadFile" multiple>
        <br><br>

        <input type="submit" value="업로드">
    </form>
</body>

</html>


uploadForm.html

 

서블릿은 다음 시간에 만들어보아요~