Posts 학원 #93일차: Http 쿠키(Cookie), 세션(Session), MVC 아키텍처
Post
Cancel

학원 #93일차: Http 쿠키(Cookie), 세션(Session), MVC 아키텍처

HTML 렌더러와 JavaScript 엔진

HTML와 CSS 형식에 따라 화면으로 렌더링해주는 HTML 렌더러와 자바스크립트를 실행하는 JavaScript 엔진이라는 두 개의 핵심 부품이 있다.

쿠키

image

쿠키는 웹 서버가 웹 브라우저에게 맡기는 데이터이다. 응답할 때 응답 헤더에 포함시켜 보낸다. 웹브라우저는 응답헤더로 받은 쿠키 데이터를 보관하고 있다가 지정된 URL요청할 때 요청 헤더에 포함시켜 웹 서버에 쿠키를 다시 보낸다.

쿠키 생성

  • 이름과 값으로 생성한다.
  • 쿠키의 유효기간을 설정하지 않으면브라우저가 종료될 때까지 유지된다. 웹 브라우저를 종료하면 유효기간이 지정되지 않은 쿠키는 모두 삭제된다. 유효기간을 설정하지 않으면 로컬이 아니라 브라우저 메모리에 저장되기 때문이다. 메모리에 저장된 정보는 브라우저가 종료될 때 메모리를 OS에 반납하면서 사라진다.
  • 쿠키의 사용범위를 지정하지 않으면 현재 경로에 한정한다. 쿠키를 보낼 때의 URL이 /ex10/s1이라면, 웹 브라우저는 /ex10/* 경로를 요청할 때에만 웹 서버에게 쿠키를 보낸다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Cookie c1 = new Cookie("name", "hong");
// 프로토콜 예 => Set-Cookie: name=hong

Cookie c2 = new Cookie("age", "20");
// 프로토콜 예 => Set-Cookie: age=20

Cookie c3 = new Cookie("working", "true");
// 프로토콜 예 => Set-Cookie: working=true

Cookie c4 = new Cookie("name2", "홍길동");
// 프로토콜 예 => Set-Cookie: name2=홍길동

// URL 인코딩
Cookie c5 = new Cookie("name3", URLEncoder.encode("홍길동", "UTF-8"));
// 프로토콜 예 => Set-Cookie: name3=%ED%99%8D%EA%B8%B8%EB%8F%99

// 쿠키를 응답 헤더에 포함시키기
response.addCookie(c1);
response.addCookie(c2);
response.addCookie(c3);
response.addCookie(c4);
response.addCookie(c5);

response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();

out.println("/ex10/s1 - 쿠키 보냈습니다.");
  • 쿠키 생성: Cookie cookie = new Cookie("name", "value");
  • 쿠키를 응답 헤더에 포함시키기: response.addCookie(cookie)
  • 값은 반드시 문자열이어야 한다. 만약 문자열이 아닌 값을 보내려면 Base64와 같은 인코딩 기법을 이용하여 바이너리 데이터를 문자화시켜서 보내야 한다.
  • 값은 반드시 ISO-8859-1이어야 한다. 만약 UTF-8을 보내고 싶다면 URL 인코딩 같은 기법을 사용하여 ASCII 코드화시켜 보내야 한다.
    • URLEncoder.encode("이름", "값")

요즘 브라우저는 URLEncoder.encode()를 하지 않아도 잘 읽는다. 그래도 인코딩을 해서 값을 보내는 것이 안전하다. 낮은 버전의 브라우저를 사용하는 경우도 고려해야 하기 때문이다.

서버로부터 쿠키를 받기 전에는 받아서 보관하고 있는 쿠키가 없기 때문에, 요청에 쿠키를 보내지 않는다.

1
2
3
4
5
6
7
8
9
HTTP/1.1 200
Set-Cookie: name=hong <---- Set-Cookie 헤더에 '이름=값' 형태로 쿠키를 보낸다.
Set-Cookie: age=20
Set-Cookie: working=true
Set-Cookie: name2=홍길동 <---- URL 인코딩 하지 않은 상태
Set-Cookie: name3=%ED%99%8D%EA%B8%B8%EB%8F%99 <---- URL 인코딩한 예
Content-Type: text/plain;charset=UTF-8
Content-Length: 35 Date: Wed, 03 Apr 2019 01:03:37 GMT
...

받은 쿠키(cookie) 읽기

클라이언트가 보낸 쿠키는 요청 헤더에 포함되어 전달된다.

1
2
3
4
5
GET /java-web/ex10/s2 
HTTP/1.1 
Host: localhost:8080 
Connection: keep-alive ... 
Cookie:name=hong; age=20; working=true; name2=홍길동; name3=%ED%99%8D%EA%B8%B8%EB%8F%99

쿠키를 이름으로 한 개씩 추출할 수 없다. 한 번에 배열로 받아야 한다. 요청 헤더에 쿠키가 없으면 리턴되는 것은 빈 배열이 아니라 null이다. 따라서 무조건 반복문을 돌리면 안 된다.

한편, 쿠키 값이 URL 인코딩한 값이라면 개발자가 직접 디코딩해서 사용해야 한다. 쿠키 값에 대해서는 서버가 자동으로 디코딩 해주지 않는다. 이때 URL 인코딩하지 않은 값도 디코딩했을 때 값이 그대로 나온다. 따라서 인코딩을 한 값인지 아닌지 따지지 않고 무조건 디코딩하면된다.

어떻게 URL 인코딩 값과 아닌 값을 구분할 수 있을까? URL 인코딩한 값은 %로 시작한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Cookie[] cookies = request.getCookies();

response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();

if (cookies != null) {
  for (Cookie c : cookies) {
    out.printf("%s=%s\n", c.getName(), c.getValue());
    if (c.getName().equals("name3")) {
      out.printf(" => %s\n", URLDecoder.decode(c.getValue(), "UTF-8"));
    }
  }
}

}

쿠키의 유효기간 설정하기

유효기간을 설정하지 않으면 웹브라우저가 실행되는 동안에만 웹서버에게 쿠키를 보낸다. 이때 웹 브랑줘는 메모리에 쿠키를 보관한다. 따라서 브라우저를 종료하면 브라우저가 사용하고 있던 메모리를 OS에 반납하기 때문에 메모리에 있는 쿠키도 다 날아가는 것이다.

유효기간을 설정하면 쿠키는 웹 브라우저를 종료해도 삭제되지 않는다. 단 유효기간이 지나면 웹서버에 보내지 않고 삭제한다. 이때 웹 브라우저는 로컬 디스크에 쿠키를 보관한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// => 웹 브라우저는 메모리에 쿠키를 보관한다.
Cookie c1 = new Cookie("v1", "aaa");

// => 웹 브라우저는 로컬 디스크에 쿠키를 보관한다.
Cookie c2 = new Cookie("v2", "bbb");
c2.setMaxAge(30); // 쿠키를 보낸 이후 30초 동안만 유효

Cookie c3 = new Cookie("v3", "ccc");
c3.setMaxAge(60); // 쿠키를 보낸 이후 60초 동안만 유효

// 쿠키를 응답 헤더에 포함시키기
response.addCookie(c1);
response.addCookie(c2);
response.addCookie(c3);

response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();

out.println("/ex10/s11 - 쿠키 보냈습니다.");

쿠키 사용 범위 지정하기

image

쿠키의 사용 범위를 지정하지 않으면 쿠키를 발행한 URL 범위에 한정된다. 즉 같은 URL로 요청할 때에만 쿠키를 보낸다.

1
2
3
4
5
6
7
8
9
HTTP/1.1 200
Set-Cookie: v1=aaa
Set-Cookie: v2=bbb; Path=/eomcs-java-web/ex10/a
Set-Cookie: v3=ccc; Path=/eomcs-java-web
Content-Type: text/plain;charset=UTF-8
Content-Length: 36
Date: Wed, 08 Apr 2020 02:51:12 GMT
Keep-Alive: timeout=20
Connection: keep-alive

예시

  • 서버에서 쿠키를 발행한 URL: /ex10/s21
  • 클라이언트가 쿠키를 보내는 URL: /ex10/*
  • 클라이언트가 쿠키를 보내지 않는 URL: /ex10/ 이외의 모든 URL

사용범위를 지정하지 않은 쿠키의 경우 쿠키를 발급한 서블릿과 같은 경로이거나 하위 경로의 서블릿을 요청할 때만 웹 브라우저가 서버에 쿠키를 보낸다.

쿠키의 경로를 적을 때 왜 웹 애플리케이션 루트(컨텍스트 루트)까지 적을까? 쿠키 경로는 서블릿 컨테이너가 사용하는 경로가 아니다. 웹 브라우저가 사용하는 경로이다. 웹 브라우저에서 /는 서버 루트를 의미한다. 따라서 웹 브라우저가 사용하는 경로를 지정할 때는 조심해야 한다. /가 서버 루트를 의미하기 때문이다. 그래서 쿠키의 경로를 지정할 때는 웹 애플리케이션 루트(컨텍스트 루트)를 정확히 줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 사용 범위를 지정하지 않은 쿠키
Cookie c1 = new Cookie("v1", "aaa");
Cookie c2 = new Cookie("v2", "bbb");
c2.setPath("/bitcamp-web-project/ex10/a");


// 이 쿠키는 모든 서블릿이 사용해야 한다.
Cookie c3 = new Cookie("v3", "ccc");
c3.setPath("/bitcamp-web-project");


// 쿠키를 응답 헤더에 포함시키기
response.addCookie(c1);
response.addCookie(c2);
response.addCookie(c3);

response.setContentType("text/plain;charset=UTF-8");
PrintWriter out = response.getWriter();

out.println("/ex10/s21 - 쿠키 보냈습니다.");

세션

클라이언트를 식별하는 기술이다. HTTP 프로토콜은 Stateless 방식으로 통신한다. 즉 연결한 후 요청하고 응답을 받으면 연결을 끊는다. 그래서 서버는 클라이언트가 요청할 때마다 누구인지 알 수 없다. 이를 해결하기 위해 클라이언트가 접속하면 웹 서버는 그 클라이언트를 위한 고유 번호를 발급(쿠키 이용)한다. 이 고유번호를 ‘세션 아이디’라 부른다. 웹 브라우저는 세션 아이디를 쿠키에 보관해두었다가 그 서버에 요청할 때마다 세션 아이디를 보낸다. 왜? 세션 아이디는 쿠키이다. 세션 아이디를 쿠키로 보낼 때 유효기간을 설정하지 않았기 때문에 웹 브라우저를 종료하면 세션 아이디 쿠키는 삭제된다. 세션 아이디 쿠키의 사용 범위는 웹 애플리케이션이다. 따라서 같은 웹 애플리케이션의 서블릿을 실행할 때는 무조건 세션 아이디를 보낸다.

서블릿 보관소

image

image

세션 아이디 생성 시점

새 세션을 생성할 때 세션 아이디를 발급한다. 이때 세션이 없는 상태에서 request.getSession()을 호출할 때 세션이 생성된다. 생성한 세션의 ID쿠키를 통해 응답할 때 웹 브라우저로 보낸다.

1
2
3
4
5
6
7
HTTP/1.1 200
Set-Cookie: JSESSIONID=5801C115615A2C9074AC0B78E31C5F21; Path=/eomcs-java-web; HttpOnly
Content-Type: text/plain;charset=UTF-8
Content-Length: 44
Date: Wed, 08 Apr 2020 03:10:58 GMT
Keep-Alive: timeout=20
Connection: keep-alive

세션 생성하기: getSession() 호출

1
2
HttpSession session = request.getSession();
session.setAttribute("v1", "aaa");

클라이언트가 세션 아이디를 쿠키로 전송한 경우, 서버에서는 해당 아이디의 세션을 찾는다. 있으면, 그 세션을 리턴한다. 있는데 세션의 유효 기간이 지났다면, 새로 세션을 만들어 리턴한다. 없다면, 새로 세션을 만들어 리턴한다.

클라이언트가 세션 아이디를 보내지 않은 경우 서버는 새 세션을 만들어 리턴한다. 항상 새 세션을 만들면, 응답할 때 새 세션의 아이디를 쿠키로 보낸다.

세션에서 값 꺼내기

서버로부터 키를 통해 받은 세션 아이디는 웹 브라우저가 해당 서버에 요청할 때마다 쿠키로 세션 아이디를 보낸다.

1
2
3
4
5
GET /eomcs-java-web/ex11/s2 HTTP/1.1
Host: localhost:9999
Connection: keep-alive
...
Cookie: JSESSIONID=AAEEF5A0E55596AB47FF69573B179CB5 <--- 세션아이디

세션 아이디가 있다면, 그리고 세션 아이디가 유효하다면 기존에 생성한 HttpSession 객체를 리턴한다. 세션 아이디가 유효하지 않다면, 그리고 세션 아이디가 없다면 새 HttpSession 객체를 생성하여 리턴한다. 응답 프로토콜에 새로 생성한 HttpSession 객체의 세션ID를 쿠키로 보낸다.

1
2
HttpSession session = request.getSession();
out.printf("v1=%s\n", session.getAttribute("v1"));

웹 브라우저를 종료하면 이전에 ex11/s1을 실행했을 때 서버로부터 받은 세션 아이디 쿠키가 삭제된다. 그런 후에 웹 브라우저에서 이 서블릿을 요청하면 getSession() 메서드를 새 세션 객체를 생성한 후 리턴한다. 따라서 새 세션에는 v1이라는 이름으로 저장된 값이 없기 때문에 null을 출력할 것이다.

Model-View-Controller 아키텍처

img

MVC(Model-View-Controller, MVC)는 소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다. 이 패턴을 사용하면 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있는 애플리케이션을 만들 수 있다. MVC에서 모델애플리케이션의 정보(데이터)를 나타내며, 는 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스적 요소를 나타내고, 컨트롤러데이터와 비즈니스 로직 사이의 상호동작을 관리한다. 사용자가 Controller를 조작하면 Controller는 Model을 통해서 데이터를 가져오고, 그 정보를 바탕으로 시각적인 표현을 담당하는 View를 제어해서 사용자에게 전달하게 된다.

컨트롤러는 모델에 명령을 보냄으로써 모델의 상태를 변경할 수 있다. 또 컨트롤러가 관련된 뷰에 명령을 보냄으로써 모델의 표시 방법을 바꿀 수 있다. 모델은 모델의 상태에 변화가 있을 때 컨트롤러와 뷰에 이를 통보한다. 이와 같은 통보를 통해서 뷰는 최신의 결과를 보여출 수 있고, 컨트롤러는 모델의 변화에 따른 적용 가능한 명령을 추가, 제거, 수정할 수 있다. 어떤 MVC 구현에서는 통보 대신 뷰나 컨트롤러가 직접 모델의 상태를 읽어 오기도 한다. 뷰는 사용자가 볼 결과물을 생성하기 위해 모델로부터 정보를 얻어 온다.

이전 초기 MVC 모델에서는 JSP가 View와 Controller의 역할을 전부 다 했다. 따라서 JSP가 Service 객체를 사용하고, Client는 JSP로 요청을 보내고 JSP가 이에 응답했다.

image

그러나 현재 실무에서 사용되는 MVC 패턴에서는 JSP는 무조건 서블릿을 경유해야 한다. 우선 클라이언트로부터 요청이 들어오면 서블렛은 그 요청을 제어한다. 업무를 처리하는 모델인 서비스에게 명령하고, 서비스는 데이터를 처리하는 모델인 DAO에게 명령하여 DBMS에 있는 데이터에 대해 해당 명령을 수행하고 결과를 리턴한다, 결과를 리턴받은 서블릿은 View인 JSP에게 include를 통해 UI 출력을 맡기고, 리턴되면 서블릿에서 클라이언트에게 응답한다. 이때 도메인도 모델인데 DAO와 Service, Servlet, JSP가 이 도메인 객체를 통해 데이터를 주고받는다.

image

배포 폴더의 \work\Catalina\localhost에서 JSP엔진이 JSP 파일을 가지고 서블릿 인터페이스를 구현해 만든 클래스(JSP 기술명세(규약))를 확인할 수 있다.

1
2
3
public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
  implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {

왜 JSP 라는 기술이 탄생했을까? 서블릿 자바 코드 안에서 출력문을 작성하는 것이 굉장히 번거롭기 때문이다. out.println()으로 html 코드를 작성하는 것은 개발의 효율성을 낮춘다. 대신 서블릿이 나 대신 출력문을 만들어 달라고 역할을 분리하는 것이다. 그럼 컨트롤러은 어떤 것을 출력할 지 중앙에서 통제하는 역할만 하게 된다. 이제 컨트롤러에는 어떠한 UI 출력도 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 스크립트릿-->
<%
String contextPath = request.getServletContext().getContextPath();
%>
<body>
  <div id='menubar'>
    <a href='<%=contextPath%>/board/list'>게시판</a> <a
      href='<%=contextPath%>/member/list'>회원</a> <a
      href='<%=contextPath%>/project/list'>프로젝트</a> <a
      href='<%=contextPath%>/task/list'>작업</a> <a
      href='<%=contextPath%>/auth/login'>로그인</a> <a
      href='<%=contextPath%>/auth/logout'>로그아웃</a>
  </div>
</body>
</html>

여기서 <% %>를 스크립트릿이라고 부른다. 스크립트릿에 자바 코드를 집어넣는다. JSP는 문법을 하나하나 외우는 방법으로 공부하는 것이 아니라, 실제 JSP 코드가 자바 코드로 어떻게 바뀌는지를 보면서 공부해야 한다. 주의해야 할 점은 JSP 코드가 들어가는 곳이 메서드라는 것이다. 따라서 <% %> 안에 메서드는 넣을 수 없지만 로컬 클래스는 넣을 수 있다.

<%= %>에는 out.println(); 안에 넣을 수 있는 요소만 올 수 있다. 즉 스크립트릿은 자바 파일에 그대로 복사되고 <%= %>out.println()으로 바뀐다.

1
<jsp:include page="/header.jsp"></jsp:include>

위 코드는 서블릿을 인클루드하는 자바 코드로 바뀐다

This post is licensed under CC BY 4.0 by the author.

HTML5 CSS3 웹 표준의 정석 #8장: CSS 박스모델

DEVIL: 태그 언팔로우 시 /tag/list로 redirect하는 문제

Loading comments from Disqus ...