[스프링 부트] 2. 서블릿

이 내용은 "윤석진"님의 "스프링 부트로 배우는 자바 웹 개발" 책을 기반으로 참고하여 제 생각과 이해한 내용을 요약한 것임을 알려드립니다.

[2.1] 서블릿 시작하기

서블릿은 Java EE(Enterprise Edition)에 포함 된 스펙 중 하나다.
자바에서 HTTP 요청과 응답을 처리하기 위한 내용들을 담고 있다.

[2.1.1] 서블릿 설정

그래들(Gradle)은 메이븐(Maven), 앤트(Ant)와 같은 빌드 도구다.
간단히 말해 자바에서 라이브러리를 편리하게 추가할 수 있는 도구라고 할 수 있다.
그래들은 build.gradle 파일을 생성해서 사용한다.
jar파일은 build.gradle 파일에 추가하면 사용할 수 있다.

[2.1.1.1] 그래들을 이용한 서블릿 설정

라이브러리 의존성을 추가할 때는 이클립스(Eclipse)나 인텔리제이(IntelliJ)와 같은 도구에서 자동완성을 이요해서 추가할 수 있는데, 사이트에서 검색을 통해 추가하는 방법도 있다.
메이븐 중앙 저장소 https://mvnrepository.com/

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
buildscript{
repositories {
jcenter()
}
dependencies {
classpath 'org.akhikhl.gretty:gretty:+'
}
}
apply plugin: 'java'
apply plugin: 'war'

apply plugin: 'org.akhikhl.gretty'

apply plugin: 'eclipse'
apply plugin: 'idea'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
jcenter()
}

compileJava.options.encoding = 'UTF-8'

dependencies {
compile 'org.slf4j:slf4j-api:1.7.7'
testCompile 'junit:junit:4.12'
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}

//gretty는 jetty와 같은 내장 서블릿 컨테이너(내장 WAS)
gretty{
httpPort = 8080
contextPath = '/'
servletContainer = 'jetty9'
}

//webappDir은 JSP,HTML,CSS 파일등이 놓일 root폴더 경로
def webappDir = "$rootDir/src/main/webapp"

eclipse{
classpath{
downloadSources = true
defaultOutputDir = file("${buildDir}/classes/main")
}
}

//idea는 설정 안해줘도 된다. 그래서 intelliJ가 짱짱.

[2.2] 서블릿 내부 동작

[2.2.1] 서블릿의 생명주기

웹 애플리케이션 컨테이너에서 콘텍스트가 초기화되면 생명주기가 시작된다.
초기화(initialize), 서비스(Service), 소멸(destroy)의 3단계로 구성되어 있다.

  1. 초기화(initialize) - 로드한 서블릿의 인스턴스를 생성하고, 리소스를 로드하는 등 클래스 생성자의 초기화 작업과 동일한 역할을 수행한다.
  2. 서비스(Service) - 클라이언트의 요청에 따라서 호출할 메서드를 결정한다.
  3. 소멸(destroy) - 서블릿이 언로드된다. 서블릿의 메서드 호출결과가 정상적으로 표출되지 않는다.

[2.2.1.1] 서블릿 초기화와 init 메서드

init 메서드는 초기화를 담당하는 메서드다.
HttpServlet은 추상 클래스인데 서블릿을 만들 때는 이 클래스를 상속받아서 만든다.
URL매핑은 WebServlet 어노테이션(annotation)으로 작성한다.

gradlew는 gradle wrapper를 실행하는 명령어로, gradle을 설치하지 않은 사용자는 gradle대신 gradlew로 실행 할 수 있다.

[처음코드] [/ch02/src/main/java/info/thecodinglive/basic/initServlet.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package info.thecodinglive.basic;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;

//URL 매핑은 @WebServlet 어노테이션을 이용해서 작성
@WebServlet("/init")
public class InitServlet extends HttpServlet{

//init() 메서드는 한 번만 호출 됨
@Override
public void init() throws ServletException {
System.out.println("init call");
}
}

gradlew appStartWar로 실행한 다음, 브라우저에서 http://localhost:8080/init 를 입력한 뒤 콘솔창을 확인하면
init call

서블릿 3.0 이후 버전부터 XML 없이 URL을 매핑할 수 있다.
이전 버전과 비교해보자면

[이전버전]

1
2
3
4
5
6
7
8
<servlet>
<servlet-name> Init </servlet-name>
<servlet-class> InitServlet </servlet-class>
</servlet>
<servlet-mapping>
<servlet-name> Init </servlet-name>
<url-pattern>/init</url-pattern>
</servlet-mapping>

[이후버전]

1
2
3
4
@WebServlet(name="Init", urlPatterns={"/init"})
public class InitServlet extends HttpServlet{
--- 중략 ---
}

@WebServlet(name="Init", urlPatterns={"/init"})

init 메서드의 초기화 시에만 작동하는 성격을 이용해 초기화 시 파라미터를 전달 하고 싶은 경우 servletConfig를 사용한다.

[수정코드] [/ch02/src/main/java/info/thecodinglive/basic/InitServlet.java]

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
28
29
30
31
32
package info.thecodinglive.basic;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(
name = "initServlet", urlPatterns = {"/init"},
//@WebInitParam 이노테이션으로 파라미터 설정
initParams = {@WebInitParam(name = "siteName", value = "jpub")}
)
public class InitServlet extends HttpServlet{
private String myParam = "";

public void init(ServletConfig servletConfig) throws ServletException{
System.out.println("init call");
//이와 같이 servletConfig.getInitParameter를 이용해서 web.xml 또는 WebInitParam 어노테이션의 정보를 서블릿 초기화 시 전달한다.
this.myParam = servletConfig.getInitParameter("siteName");
System.out.println("입력받은 사이트 명은" + myParam + "입니다.");
}

//doGet은 아래에서 얘기하는걸로
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("hello");
}
}

[2.3] 서블릿 활용

[2.3.1] HTTP 요청과 응답

HTTP 요청에 대한 응답을 브라우저를 통해 확인하기
Get방식은 Select 할떄 사용한다.
Post방식은 입력/수정 할때 사용한다. Http Request Body에 파라미터 정보가 추가된다.

[2.3.1.1] GET 요청 처리

서블릿에서는 doGet 메서드를 이용해서 GET 메서드 방식의 요청을 응답받을 수 있다.
doGet은 HttpServletRequest, HttpServletResponse를 파라미터로 전달받도록 되어 있는데
HttpServletRequest는 요청에 대한 정보를 가지고 있고,
HttpServletResponse는 브라우저에서 정보를 표현하기 위해 사용한다.

[코드] [/ch02/src/main/java/info/thecodinglive/basic/HelloServlet.java]

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
package info.thecodinglive.basic;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "HelloServlet", urlPatterns = {"/helloget"})
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doGet 메소드 호출");
resp.setCharacterEncoding("UTF-8");
PrintWriter writer = resp.getWriter();

//contentType 정의 이런식으로 response시 보여줄 내용을 코드로 작성할 수 있다.
resp.setContentType("text/html");
writer.println("<html>");
writer.println("<head><title>jpub java webservice</title></head>");
writer.println("<body> get 요청 예제입니다. </body>");
writer.println("</html>");
}
}

웹브라우저에서 http://localhost:8080/helloget 을 입력하면 doGet 메서드가 호출된다.
메서드 안에서는 response 객체에 printWriter의 인스턴스를 얻어서 HTML 내용을 출력한다.
HTTP 메서드 요청 방식이 예제와 같이 GET인 경우에는 브라우저를 통해서 직접 URL을 입력해서 확인하면 된다.

[2.3.1.2] POST 요청 처리

doPost는 post요청에 대해서만 처리할 수 있는 메서드다.
그래서 URL이 일치해도 에러 405가 발생한다.

[코드] [/ch02/src/main/java/info/thecodinglive/basic/HelloServlet2.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package info.thecodinglive.basic;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//기본 doPost 골격
@WebServlet(name = "HelloServlet2", urlPatterns = {"/hellopost"})
public class HelloServlet2 extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doPost 호출");
}
}

URL에 /hellopost 입력 시 오류는 해당 URL이 get 요청을 지원하지 않는다는 뜻이다. 서블릿뿐만 아니라 모든 HTTP 요청에 대해 서버가 허용하지 않는 경우 HTTP 에러코드 405를 응답받게 된다.

[2.3.1.3] HTML 폼 데이터 전송

POST 방식은 주로 폼(form)에서 데이터를 입력 후 전송할 때 사용하는데 회원가입, 로그인등의 기능을 구현할 때 많이 쓰인다.

폼의 두 가지 기억 해야하는 속성은 action="요청을 보낼 경로(urlPatterns)"과 method="post"

[전송용폼] [/ch02/src/main/webapp/login.html]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="style.css" type="text/css"/>
</head>
<body>
<div class="login-card">
<h1>Log-in</h1><br>
//method="post"속성을 입력해줘야하며 action이 post로 보내는 url과 같다.
<form method="post" action="postsend">
<input type="text" name="user" placeholder="Username">
<input type="password" name="pwd" placeholder="Password">
<input type="submit" class="login login-submit" value="login">
</form>
</div>
</body>
</html>

[폼으로부터 Post로 받는 코드] [/ch02/src/main/java/info/thecodinglive/basic/LoginServlet.java]

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
28
29
30
31
package info.thecodinglive.basic;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "LoginServlet", urlPatterns = {"/postsend"})
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doPost 메소드 호출");
resp.setCharacterEncoding("UTF-8");
req.setCharacterEncoding("UTF-8");
PrintWriter writer = resp.getWriter();

resp.setContentType("text/html");

String user = req.getParameter("user");
String pwd = req.getParameter("pwd");
writer.println("<html>");
writer.println("<head><title>Login Servlet</title></head>");
writer.println("<body>");
writer.println("전달받은 이름은" + user + "이고" + "<br/>" + "비밀번호는" + pwd + "입니다.");
writer.println("</body>");
writer.println("</html>");
}
}

doPost 메서드 안에서 getParameter 메서드로 아이디와 패스워드를 각각 전달받은 후에 printWriter 객체를 생성하여 println 메서드로 출력한다.
폼에서 만든 input 필드의 name 속성값과 getParameter의 메서드의 파라미터는 같아야 한다.
폼태그의 action 속성값과 서블릿의 urlPatterns 값은 같아야한다.

폼에서 입력한 아이디와 비밀번호 값이 LoginServlet의 doPost 메서드로 출력되는 결과를 볼 수 있다.

[2.3.2] 멀티파트

멀티파트(multipart)는 바이너리 데이터 전송을 위해 사용한다. (= 사진, 동영상, 프로그램, 알집 같은 거)
서블릿 3.0 이후부터는 서블릿 스펙에 multipart가 추가되어서 별도의 라이브러리 없이 구현이 가능해졌다.

[업로드 하는 폼] [/ch02/src/main/webapp/upload.html]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>업로드</h1><br>
<form method="post" action="upload" enctype="multipart/form-data">
File:
<input type="file" name="file" id="file">
업로드할 서버 경로:
<input type="text" value="c:/upload" name="destination"/>
<br/>
<input type="submit" value="upload">
</form>
</body>
</html>

파일을 전송할 때는 폼 속성에 enctype="multipart/form-data"를 입력해야한다.
그리고 폼 태그 하위에 input 타입의 속성을 'file'로 설정한다.

이 예제에서는 c드라이브에 upload 디렉터리를 만든다. 디렉터리를 반드시 먼저 만들 필요는 없고, upload 서블릿이 동작할 때 upload 디렉터리도 함께 생성된다.

멀티파트 데이터를 처리를 위해 MultiPartConfig 어노테이션을 사용한다.

MultiPartConfig 어노테이션 속성

Annotation 명 설명
@fileSizeThreshold fileUploa 시에 메모리에 저장되는 임시 파일 크기를 정의한다.
[자료형: int]
@location 파일 업로드 시에 임시 저장 디렉터리를 지정한다.
[자료형: String]
@maxFileSize 업로드할 파일의 최대 크기를 지정한다.
[자료형: long]
@maxRequestSize request시에 최대 크기를 지정한다.
[자료형: long]

[폼으로부터 Post로 업로드하는 코드] [/ch02/src/main/java/info/thecodinglive/upload/UploadServlet.java]

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package info.thecodinglive.upload;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.*;

@WebServlet(urlPatterns = "/upload", name = "uploadServlet")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024 * 2, // 2mb
maxFileSize = 1024 * 1024 * 10, // 10mb
maxRequestSize = 1024 * 1024 * 50, //50mb
location = "c:/upload" //파일저장위치
)
public class UploadServlet extends HttpServlet {

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
//경로
final String path = request.getParameter("destination");
//파일
final Part filePart = request.getPart("file");
//파일이름
final String fileName = getFileName(filePart);
final PrintWriter writer = response.getWriter();

try (OutputStream out = new FileOutputStream(new File(path + File.separator + fileName)); InputStream filecontent = filePart.getInputStream()) {
int read = 0;
final byte[] bytes = new byte[1024];

while ((read = filecontent.read(bytes)) != -1) {
out.write(bytes, 0, read);
}

writer.print("new File: " + fileName + path + "에 생성되었습니다.");

} catch (FileNotFoundException fne) {
System.out.println(fne.getMessage());
}
}


private String getFileName(final Part part) {
final String partHeader = part.getHeader("content-disposition");
System.out.println("Part Header = {0}" + partHeader);
for (String content: part.getHeader("content-disposition").split(";")) {
if (content.trim().startsWith("filename")) {
return content.substring(
content.indexOf('=') + 1).trim().replace("\"", "");
}
}
return null;
}
}

doPost 메서드 블록 안에 request.getPart 메서드로 참조한다.
request.getPart 메서드로 Part 객체 생성 후 getInputStream 메서드로 파일의 내용을 저장한다.
헤더에 있는 파일 정보 Part 객체에서 getHeader 메서드로 얻을 수 있다.

upload 버튼을 클릭하면 multipart 요청이 uploadServlet의 doPost 메서드로 전달되고 uploadServlet의 PrintWriter로 결과를 브라우저에 출력한다.

자바에서는 파일 쓰기 시에 먼저 임시 디렉터리에 파일을 저장한다.
기본 임시 디렉터리는 자바의 시스템 프로퍼티로 java.io.tmpdir로 저장되어 있고,
실제 저장 위치는 System.getProperty("java.io.tmpdir");로 확인할 수 있다.

Get -> 데이터를 쿼리스트링 형식으로 전송
Post Application/x-www-form-urlencoded: -> 데이터를 스트림 형태로 인코딩하여 전달할 때 사용되는 전송 방식
Multipart/form-data: -> 파일 업로드 시 사용되는 전송 방식
파일 전송용 폼을 만들 때는 enctype="multipart/form-data" 추가

[2.4] 서블릿 관련 객체

[2.4.1] 필터

C언어의 전처리기와 비슷한 기능, 웹 클라이언트의 요청에 대해서 필요한 사전 작업이 있을 경우에 필터(filter)를 사용한다.

[2.4.1.1] 웹 필터

필터는 필터 인터페이스를 상속받아 만들 수 있다. 필터는 서블릿의 생명주기처럼 init와 destroy 메서드를 가지고 있고, 필터 기능 사용을 위한 doFilter 메서드가 있다.
인코딩 필터를 만들어 사용할 때 ex(UTF-8 필터) 쓰기도 한다.

[필터(전처리) 기능을 사용하는 코드] [/ch02/src/main/java/info/thecodinglive/filter/FilterEx.java]

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
28
29
package info.thecodinglive.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;

@WebFilter("*.jsp")
public class FilterEx implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
res.setContentType("text/html");
res.setCharacterEncoding("UTF-8");
PrintWriter out = res.getWriter();
out.println("필터 동작 전");
chain.doFilter(req, res);
out.println("필터 동작 후");
}

@Override
public void destroy() {

}
}

필터의 URL 매핑을 위해서 WebFilter 이노테이션을 사용할 수 있다.
필터의 실제 작업은 doFilter 메서드 안에서 이루어지는데, 이 예제에서는 필터가 jsp보다 먼저 동작하는지 확인하기 위해서 doFilter 이전과 이후를 표시하도록 했다.

jsp파일을 실행하면 앞 뒤로 전처리로 생성된 글을 볼 수 있다.
ex)
필터 동작 전 filter test 필터 동작 후

필터는 여러개 등록해서 사용할 수 있다. 하나의 요청에 대해서 다양한 변경이 필요하다면 여러 개의 필터를 매핑해서 처리할 수 있다. 이렇게 여러 개의 필터를 등록해서 처리하는 것을 필터 체인(filter chain)이라고 한다.

[2.4.2] 쿠키

[2.4.2.1] 쿠키의 구성

쿠키(Cookie)는 사용자가 사이트를 방문했을 때, 사용자의 컴퓨터에 저장되는 정보를 말한다. 쿠키의 구성 요소는 다음과 같다.

  • 이름: 각각의 쿠키의 값을 식별하기 위한 키
  • 값: 특정 이름으로 쿠키에 지정된 값
  • 유효 시간: 쿠키의 유지 시간
  • 도메인: 쿠키를 전송할 도메인
  • 경로: 쿠키를 전송할 요청 경로

쿠키는 HTTP 헤더 정보에 포함되어 전달된다. HTTP 프로토콜은 비연결지향으로 상태 정보를 저장하지 않는다. 자연스럽게 상태 정보를 저장할 공간이 필요하게 되며 이때 사용할 수 있는 메커니즘 중 하나가 쿠키다. 쿠키는 사용자의 PC에 저장되므로 로그인하지 않은 사용자에 대해서 다르게 적용할 필요가 있을 경우에 유용하다.

[2.4.2.2] 쿠키 생성

쿠키를 생성할 때는 간단하게 생성자를 이용해서 cookie 객체를 다음과 같이 생성할 수 있다.

Cookie jcookie = new Cookie(name, value);

쿠키는 javax.servlet.http 패키지에 포함되어 있는데, map을 사용할 때처럼 key, value형태로 사용하고 도메인과 최대 유효 기간 등을 설정할 수 있다. 서블릿에서 생성하는 방법은 다음과 같다.

[쿠키 생성하는 서블릿 코드] [/ch02/src/main/java/info/thecodinglive/cookie/CookieCreateServlet.java]

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
28
29
30
31
package info.thecodinglive.cookie;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/newcookie")
public class CookieCreateServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req,
HttpServletResponse resp) throws ServletException,
IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><head><title> 쿠키 예제</title></head><body>");
out.println("<br/>");

Cookie jcookie = new Cookie("jpub", "books");
//만료시간을 설정하는 setMaxAge(second) 메서드 초단위다.
jcookie.setMaxAge(3600);
//여기서는 response에 저장했다, application이나 다른방식으로 저장가능.
resp.addCookie(jcookie);
out.println("<a href='/readcookie'>readcookie</a></body></html>");
}
}

이 코드는 쿠키를 생성한 후에 만료 시간(1시간)을 설정하고, response객체에 쿠키를 저장한다.

setDomain 메서드를 이용해서 사용 가능한 도메인을 지정할 수도 있다.
ex)
cookie.setDomain("*.jpub.com")

[쿠키 읽어오는 서블릿 코드] [/ch02/src/main/java/info/thecodinglive/cookie/CookieReadServlet.java]

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
28
29
30
31
32
package info.thecodinglive.cookie;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = "/readcookie")
public class CookieReadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><head><title>쿠키 읽기</title></head><body>");
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("jpub")) {
out.println("cookie::" + cookie.getValue());
}
}
}
//이 다음 코드 쿠키 수정 서블릿을 위한 링크
out.println("<a href='/modicookie'>쿠키수정</a></body></html>");
}
}

이 코드는 request객체에서 getCookies 메서드로 저장된 쿠키들을 꺼낸 뒤에 쿠키 생성 시에 'jpub'을 키로 저장된 쿠키값을 출력한다.

브라우저에서 http://localhost:8080/newcookie 를 입력하면 쿠키가 create되면서 read서블릿으로 넘어갈수 있는 링크가 생긴다.
readcookie 링크를 클릭 시 -> cookie::books 텍스트를 출력한다.

[2.4.2.3] 쿠키값 수정 및 삭제

쿠키값을 변경하려면 같은 이름으로 쿠키를 생성해서 새로운 값을 지정하면 된다.

Cookie modifiedCookie = new Cookie("name", "새로운 값")

CookieModifyServlet을 만들어서 기존에 jpub이란 이름으로 만든 쿠키값을 수정할 수 있다.

[쿠키 수정하는 서블릿 코드] [/ch02/src/main/java/info/thecodinglive/cookie/CookieModifyServlet.java]

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
28
29
30
31
32
package info.thecodinglive.cookie;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/modicookie")
public class CookieModifyServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><head><title> cookie 수정 </title></head>");
out.println("<body>");
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("jpub")) {
Cookie modifiedCookie = new Cookie("jpub", "read");
resp.addCookie(modifiedCookie);
}
}
}
out.println("<a href='/readcookie'>readcookie</a></body></html>");
}
}

request 객체에서 쿠키 이름이 기존에 만든 jpub과 같은지 비교한 뒤에 jpub과 같으면 동일한 이름으로 쿠키를 생성한다.
쿠키 수정을 쉽게 하기 위해서 기존에 만든 CookieReadServlet에서 다음과 같이 modicookie로 이동할 수 있는 링크 태그를 추가했다.

out.println("<a href='/modicookie'>쿠키수정</a></body></html>");

쿠키의 결과값이 books read로 변경된 것을 알 수 있다.
쿠키 자체를 삭제하는 API는 존재하지 않는다.
그렇지만 쿠키의 유효 시간을 '0'으로 설정함으로써 쿠키값을 무효화 할 수 있다.

[쿠키 무효화(삭제)하는 서블릿 코드] [/ch02/src/main/java/info/thecodinglive/cookie/CookieDeleteServlet.java]

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
28
29
30
31
32
33
package info.thecodinglive.cookie;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/delcookie")
public class CookieDeleteServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><head><title> cookie 삭제 </title></head>");
out.println("<body>");
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("jpub")) {
Cookie deletedCookie = new Cookie("jpub", "");
deletedCookie.setMaxAge(0);
resp.addCookie(deletedCookie);
}
}
}
out.println("<a href='/readcookie'>readcookie</a></body></html>");
}
}

jpub 쿠키의 유효 시간을 setMaxAge 메서드를 사용해서 '0'으로 설정했다.
http://localhost:8080/delcookie 를 들어가 readcookie 링크를 누르면 쿠키가 소멸되어서 값이 표시되지 않음을 확인할 수 있다.

쿠키값 한글 입력 시에는 URLEncoder를 이용해서 문자열을 감싸줘야한다.
ex) Cookie newCookie = new Cookie("kor", URLEncoder.encode("데이터", "UTF-8"));

[2.4.3] 세션

[2.4.3.1] 세션의 구성

세션(session)은 서버와 클라이언트의 유효한 커넥션을 식별하는 정보.
서버는 클라이언트가 요청을 보내면 요청을 식별할 수 있는 ID를 부여하는데, 이 ID가 세션 ID다.
세션ID는 JSESSIONID란 이름으로 쿠키로 저장되고, 클라이언트가 재접속할 때 해당 쿠키를 이용해 세션 ID값을 서버에 전달한다.
서블릿에서는 세션이 javax.servlet.http 패키지에 HttpSession 인터페이스로 정의되어 있다.

[2.4.3.2] 세션 생성

현재 생성된 세션 정보는 request 객체에서 꺼내서 사용할 수 있다.
request.getSession();

[기본 세션 서블릿 코드] [/ch02/src/main/java/info/thecodinglive/session/DefaultSessionServlet.java]

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
package info.thecodinglive.session;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/session")
public class DefaultSessionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html><head><title>세션</title></head><body>");
HttpSession session = req.getSession();
out.println("sessionId::" + session.getId() + "<br/>");
out.println("session created::" + session.getCreationTime() + "<br/>");
out.println("session lastAccessTime" + session.getLastAccessedTime() + "<br/>");
out.println("</body></html>");
}
}

getSession() 메서드를 이용해서 session 객체를 생성한 뒤 세션 정보를 출력하도록 한다.
.getId() : 세션의 고유 아이디를 얻을 수 있는 메서드
.getCreationTime() : 세션이 생성된 시간을 얻을 수 있는 메서드
.getLastAccessedTime() : 웹 브라우저가 가장 마지막에 세션에 접근한 시간을 얻을 수 있는 메서드다.

브라우저에서 http://localhost:8080/session 을 입력하면 세션의 기본 정보를 볼 수 있다.

[2.4.3.1] 세션에 값 저장 및 삭제

세션에 값을 저장하는 방식은 쿠키와 동일하게 이름, 값 형태로 저장할 수 있다.
setAttribute() 메서드를 사용한다.

Session.setAttribute('이름', 값)

[세션에 값을 저장하는 서블릿 코드] [/ch02/src/main/java/info/thecodinglive/session/CreateSessionValueServlet.java]

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
package info.thecodinglive.session;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/createse")
public class CreateSessionValueServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html><head><title>세션</title></head><body>");

HttpSession session = req.getSession();
session.setAttribute("jpub", "book");
out.println("세션이 생성되었습니다.");
out.println("<a href='/readse'>세션 읽기</a></body></html>");
}
}

현재 세션에 정보를 저장하기 위해서 request 객체에서 getSession 메서드를 이용해서 세션을 얻은 후 setAttribute 메서드를 이용해 jpub이란 이름으로 book을 값으로 입력했다.

[세션에 값을 읽어오는 서블릿 코드] [/ch02/src/main/java/info/thecodinglive/session/ReadSessionValueServlet.java]

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
package info.thecodinglive.session;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/readse")
public class ReadSessionValueServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html><head><title>세션</title></head><body>");

HttpSession session = req.getSession();
String sessionValue = (String) session.getAttribute("jpub");
out.println("생성된 세션 값:" + sessionValue);
out.println("</body></html>");
}
}

입력할 때 처럼 session을 얻은 후 getAttribute()로 세션에 입력한 값을 추출한다.
이 떄, getAttribute()의 반환값은 Object이므로 형변환이 필요하다.

[2.5] 디자인 패턴 활용

[2.5.1] Java EE 패턴

Java EE 패턴은 자바 기반의 엔터프라이즈 웹 애플리케이션 개발을 위한 패턴이다.
ValueObject, DataAccessObject 등은 여기서 나온 용어들이다.
Java EE 패턴은 자바 웹 개발 시에 겪는 문제를 해결하는 실마리를 제공한다.
Java EE 는 SUN사가 EJB를 출시하면서 N-Tier 각 레이어에 대한 클래스들의 역할에 관해서 로드맵을 만들고 백서(White Paper)로 배포해서 널리 알려지게 된 패턴이다. N-Tier 아키텍처에서 각 레이어에 적합한 패턴으로 구분되어 있다.
결국 디자인 패턴을 사용해서 스프링의 형태로 가게 된다.

[2.5.1.1] Java EE 패턴 목록

[Java EE 패턴]

패턴 이름 개요
Intercepting Filter 요청에 대한 전처리 및 후처리
요구 사항에 대해서 전처리와 후처리에 대한 솔루션을 제공하고 이를 통해 유동적인 아키텍처를 가능하게 한다.
Front Controller 요청에 대한 처리를 관리하는 중앙 컨트롤러
프리젠테이션 레이어에 일어나는 일들의 창구로 facade 패턴의 역할과 MVC 패턴에서 controller의 역할을 함으로써 보안, 뷰 관리, 탐색들을 관리한다.
View Helper 뷰의 표현을 위해 비즈니스 로직을 가지고 있는 개념상의 Helper
비즈니스 로직과 프레젠테이션 로직의 결합도를 낮추기 위해 사용한다.
Composite View 레고 블럭 같은 작은 뷰들을 조합해서 만드는 전체의 뷰
복잡한 뷰를 만들기 위해서 기본적인 뷰 레이어를 융통성 있게 하고, 개인화 영역과 커스터마이징을 보다 수월하게 한다.
Service to Worker Front Controller와 View Helper Pattern을 이용해 dispatcher 컴포넌트를 구성
대규모 애플리케이션에서 이용되는 기법으로 뷰에 대한 처리 이전에 동작한다.
Dispatcher View Service to Worker와 동일하며 차이점은 뷰에 대한 처리 중에 수행되어야 하고, 작은 시스템에서 더 안정적이다.

앞에서의 필터는 Intercepting Filter Pattern에 대한 구현체다.
Front Controller Pattern은 대부분의 웹 프레임워크에서 개념을 차용하고 있다.

[2.5.2] 프론트 컨트롤러 패턴

컨트롤러가 공동 요청을 먼저 수행하고 뷰를 호출하는 패턴이다.

Client의 요청에 의해서 컨트롤러가 응답하고, 결과에 따라 서블릿이나 JSP로 만든 뷰를 보여준다.
서버 측에서 메서드를 사용하여 화면을 전환하는 방법에는 두 가지가 있다.

  • Response 객체의 sendRedirect 메서드
  • RequestDispatcher 객체의 forward 메서드

[2.5.2.1] sendRedirect

sendRedirect 는 속성을 저장할 수 없고, 다른 로직을 추가할 수 없다.
HttpServletResponse에 속한 메서드다.

response.sendRedirect(경로);

[2.5.2.2] forward

forward 메서드는 서버 내부에서만 흐름이 이동하므로 속성을 저장할 수 있고, 브라우저에게 바로 전달하지 않고 원하는 작업을 처리한 후에 응답을 전환할 수 있으므로 컨트롤러를 만들 때 많이 사용하는 메서드다.
RequestDispatcher객체를 생성해야 forward 메서드를 사용할 수 있다.

1
2
RequestDispatcher rd = request.getRequestDispatcher(경로);
rd.forward(ServletRequest request, ServletResponse response);

RequestDispatcher 객체의 경로는 절대경로로 지정하고 상대경로를 사용할 수 없다.
forward 메서드 사용 시에는 ServletRequest와 ServletResponse 객체를 파라미터로 전달하므로 sendRedirect와는 다르게 ServletContext와 Session에 속성을 저장하고, 포워딩 한 곳에서 사용할 수 있다.

1
2
3
4
5
6
7
if(url == "list") {
RequestDispatcher rd = req.getRequestDispatcher(url);
rd.forward(request, response);
} else if(url == "write") {
RequestDispatcher rd = req.getRequestDispatcher(url);
rd.forward(request, response);
}

컨트롤러에서 화면을 보여 주는 구문은 위와 같이 if 문으로 분기처리하게 되는데, 이렇게 컨트롤러에서 직접적으로 forward 메서드를 사용하게 될 경우에는 URL이 변경되거나 뷰가 변경될 때마다 컨트롤러를 변경하게 되어서 추후에 유지보수가 어려워진다. 이럴 때는 커맨드 패턴을 이용해서 컨트롤러 클래스의 복잡도를 낮출 수 있다.

[2.5.2.3] 커맨드 패턴

커맨드 패턴은 명령(로직)을 객체 안에 캡슐화해서 저장함으로써 컨트롤러와 같은 클래스를 수정하지 않고 재사용할 수 있게 하는 패턴이다.

Invoker의 역할은 컨트롤러가 담당한다. 이전에 있던 forward 메서드 관련 코드를 커맨드로 옮길 수 있다.

[forward 메서드 관련 코드를 Command Pattern화 한 코드] [/ch02/src/main/java/info/thecodinglive/pattern/Command.java]

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package info.thecodinglive.pattern;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public abstract class Command {
private HttpServletRequest req;
private HttpServletResponse res;
private ServletContext servletContext;

abstract public void execute();

public void forward(String url){
try{
RequestDispatcher rd = req.getRequestDispatcher(url);
rd.forward(getReq(), getRes());
}catch (IOException ioe){
servletContext.log("forward Error",ioe);
}catch (ServletException servletEx){
servletContext.log("servlet Error", servletEx);
}
}

public HttpServletRequest getReq() {
return req;
}

public void setReq(HttpServletRequest req) {
this.req = req;
}

public HttpServletResponse getRes() {
return res;
}

public void setRes(HttpServletResponse res) {
this.res = res;
}

public ServletContext getServletContext() {
return servletContext;
}

public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
}

커맨드 클래스는 서블릿 클래스가 아니므로 HttpServletRequest와 HttpServletResponse를 변수로 선언하고 setter 메서드를 통해서 컨트롤러에서 인스턴스를 얻을 수 있도록 한다.
요청을 응답받고 전달받은 데이터를 Command 객체에 제공할 서블릿 클래스를 만들어야 한다.

[command 객체에 제공할 서블릿 클래스 코드] [/ch02/src/main/java/info/thecodinglive/pattern/FrontController.java]

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package info.thecodinglive.pattern;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

@WebServlet(urlPatterns = "/controller", initParams = {@WebInitParam(name = "mapping", value = "/WEB-INF/command.properties")})
public class FrontController extends HttpServlet {
private Properties cmdMapping;

@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
InputStream is = null;
try {
String location = config.getInitParameter("mapping");
is = getServletContext().getResourceAsStream(location);
cmdMapping = new Properties();
cmdMapping.load(is);
} catch (IOException e) {
getServletContext().log("I/O Error", e);
} finally {
try {
is.close();
} catch (IOException iog) {

}
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
String cmdClass = (String) cmdMapping.get(cmd);
Command command = null;

try {
command = (Command) Class.forName(cmdClass).newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException ex) {
getServletContext().log("class not found", ex);
}
command.setReq(req);
command.setRes(resp);
command.setServletContext(getServletContext());
command.execute();
}
}

doGet 메서드에서 command 객체로 사용될 클래스의 이름을 입력받은 후 리플랙션을 이용해서 인스턴스를 생성하고 execute 메서드를 호출한다. execute 메서드는 입력받은 파라미터와 뷰 클래스들을 매칭해서 응답을 전달하는 역할을 한다. 뷰 클래스들의 패키지 위치 정보는 command.properties 파일로 관리한다.

[command.properties 파일] [/ch02/src/main/webapp/WEB-INF/command.properties]

1
2
3
home=info.thecodinglive.pattern.HomeView
list=info.thecodinglive.pattern.ListView
write=info.thecodinglive.pattern.WriteView

command.properties 파일은 입력된 파라미터가 view 클래스들과 연결되도록 하는 역할을 한다.
아래는 home.jsp 페이지를 연결하는 HomeView 클래스를 만든 것이다.

[HomeView 클래스] [/ch02/src/main/java/info/thecodinglive/pattern/HomeView.java]

1
2
3
4
5
6
7
8
package info.thecodinglive.pattern;

public class HomeView extends Command {
@Override
public void execute() {
forward("/home.jsp");
}
}

브라우저에 localhost:8080/controller?cmd=home를 입력하고 결과를 확인해보면 HomeView 클래스에서 Command를 상속받아서 home.jsp로 리퀘스트를 포워딩하는 것을 볼 수 있다.

클래스의 흐름을 정리해보면
FrontController -> command
^
|
home.jsp <- HomeView
모든 요청은 FrontController 클래스가 받고, HomeView는 Command클래스를 상속받았고 요청 파라미터가 Home인 경우 home.jsp를 호출한다.

이와 같이 프론트 컨트롤러 패턴은 뷰 페이지 요청을 한 곳에서 관리할 수 있게 해준다. HomeView외에 다른 뷰를 만들어 jsp와 연결 시킬 수 있다.

0%