HTML 렌더러와 JavaScript 엔진
HTML와 CSS 형식에 따라 화면으로 렌더링해주는 HTML 렌더러와 자바스크립트를 실행하는 JavaScript 엔진이라는 두 개의 핵심 부품이 있다.
쿠키
쿠키는 웹 서버가 웹 브라우저에게 맡기는 데이터이다. 응답할 때 응답 헤더에 포함시켜 보낸다. 웹브라우저는 응답헤더로 받은 쿠키 데이터를 보관하고 있다가 지정된 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 - 쿠키 보냈습니다.");
쿠키 사용 범위 지정하기
쿠키의 사용 범위를 지정하지 않으면 쿠키를 발행한 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 방식으로 통신한다. 즉 연결한 후 요청하고 응답을 받으면 연결을 끊는다. 그래서 서버는 클라이언트가 요청할 때마다 누구인지 알 수 없다. 이를 해결하기 위해 클라이언트가 접속하면 웹 서버는 그 클라이언트를 위한 고유 번호를 발급(쿠키 이용)한다. 이 고유번호를 ‘세션 아이디’라 부른다. 웹 브라우저는 세션 아이디를 쿠키에 보관해두었다가 그 서버에 요청할 때마다 세션 아이디를 보낸다. 왜? 세션 아이디는 쿠키이다. 세션 아이디를 쿠키로 보낼 때 유효기간을 설정하지 않았기 때문에 웹 브라우저를 종료하면 세션 아이디 쿠키는 삭제된다. 세션 아이디 쿠키의 사용 범위는 웹 애플리케이션이다. 따라서 같은 웹 애플리케이션의 서블릿을 실행할 때는 무조건 세션 아이디를 보낸다.
서블릿 보관소
세션 아이디 생성 시점
새 세션을 생성할 때 세션 아이디를 발급한다. 이때 세션이 없는 상태에서 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 아키텍처
MVC(Model-View-Controller, MVC)는 소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다. 이 패턴을 사용하면 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있는 애플리케이션을 만들 수 있다. MVC에서 모델은 애플리케이션의 정보(데이터)를 나타내며, 뷰는 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스적 요소를 나타내고, 컨트롤러는 데이터와 비즈니스 로직 사이의 상호동작을 관리한다. 사용자가 Controller를 조작하면 Controller는 Model을 통해서 데이터를 가져오고, 그 정보를 바탕으로 시각적인 표현을 담당하는 View를 제어해서 사용자에게 전달하게 된다.
컨트롤러는 모델에 명령을 보냄으로써 모델의 상태를 변경할 수 있다. 또 컨트롤러가 관련된 뷰에 명령을 보냄으로써 모델의 표시 방법을 바꿀 수 있다. 모델은 모델의 상태에 변화가 있을 때 컨트롤러와 뷰에 이를 통보한다. 이와 같은 통보를 통해서 뷰는 최신의 결과를 보여출 수 있고, 컨트롤러는 모델의 변화에 따른 적용 가능한 명령을 추가, 제거, 수정할 수 있다. 어떤 MVC 구현에서는 통보 대신 뷰나 컨트롤러가 직접 모델의 상태를 읽어 오기도 한다. 뷰는 사용자가 볼 결과물을 생성하기 위해 모델로부터 정보를 얻어 온다.
이전 초기 MVC 모델에서는 JSP가 View와 Controller의 역할을 전부 다 했다. 따라서 JSP가 Service 객체를 사용하고, Client는 JSP로 요청을 보내고 JSP가 이에 응답했다.
그러나 현재 실무에서 사용되는 MVC 패턴에서는 JSP는 무조건 서블릿을 경유해야 한다. 우선 클라이언트로부터 요청이 들어오면 서블렛은 그 요청을 제어한다. 업무를 처리하는 모델인 서비스에게 명령하고, 서비스는 데이터를 처리하는 모델인 DAO에게 명령하여 DBMS에 있는 데이터에 대해 해당 명령을 수행하고 결과를 리턴한다, 결과를 리턴받은 서블릿은 View인 JSP에게 include
를 통해 UI 출력을 맡기고, 리턴되면 서블릿에서 클라이언트에게 응답한다. 이때 도메인도 모델인데 DAO와 Service, Servlet, JSP가 이 도메인 객체를 통해 데이터를 주고받는다.
배포 폴더의 \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>
위 코드는 서블릿을 인클루드하는 자바 코드로 바뀐다