본문 바로가기

웹 개발/Spring Boot

Spring Boot) 2. 서블릿 Servlet

1. 서블릿 시작하기


1.1 서블릿이란

  • JVM 기반에서 웹 개발을 위한 명세, API
  • 서블릿 실행을 위해 웹 애플리케이션 컨테이너가 필요
  • Java EE에 포함된 스펙 중 하나
  • 자바에서 HTTP 요청 및 응답을 처리하기 위한 내용 담고 있음

1.2 서블릿 설정

  • build.gradle 파일에 JAR 파일 추가해서 사용
  • example
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{
    httpPort = 8080
    contextPath = '/'
    servletContainer = 'jetty9'
}


   def webappDir = "$rootDir/src/main/webapp"

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

2. 서블릿 내부 동작


2.1 서브릿의 생명 주기

  • start → initialize → service → destory

2.1.1 초기화

로드한 서브릿의 인스턴스를 생성하고 리소스 로드 등 클래스 생성자의 초기화 작업과 동일한 역할 수행

  • 한 번만 호출됨
  • annotation : WebServlet(name="Init", urlPatterns={"/init"}) 으로 맵핑
    • 다수의 url로 맵핑 가능 { .., .. ...}
  • HttpServlet을 상속 받아서 서블릿 생성 (일반적 방법)
  • 서블릿을 상속 받아서 init 메소드 작성
  • 초기화 할 때 파라미터 전달하고 싶은 경우 → servletConfig.getInitParameter 사용 → web.xml, WebInitParam 어노테이션 정보 전달
  • example
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;


// url mapping + 초기화 시 전달할 파라미터 설정 
@WebServlet(
        name = "initServlet", urlPatterns = {"/init"},
        initParams = {@WebInitParam(name = "siteName", value = "jpub")}
)

// 서블릿 사용을 위해 상속해야 할 클래스 HttpServlet 
public class InitServlet extends HttpServlet{
    private String myParam = "";

    public void init(ServletConfig servletConfig) throws ServletException{
        System.out.println("init call");
        this.myParam = servletConfig.getInitParameter("siteName");
        System.out.println("입력받은 사이트 명은" + myParam + "입니다.");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("hello");
    }
}

2.1.2 서비스

클라이언트 요청에 따라 호출할 메소드 결정

2.1.3 소멸

서브릿 언로드

3. 서블릿 활용


3.1 GET 요청 처리

  • get 메소드 : 데이터를 url에 쿼리 스트링 형식으로 전송하면 브라우저로 respone 표현
  • doGet 메소드 사용
  • HttpServletRequest(요청 정보), HttpServletResponse(브라우저에 표현할 정보)를 파라미터로 전달 받음
@WebServlet(name="HelloWervlet", urlPatterns = {"/helloget"} )
public class HelloServlet extends HttpServelet {
	protected void doGet (HttpServletRequest, request, HttpServletResponse response) throws ServeletException, IOException{
		System.out.println("doGet 메소드 호출");
		response.setCharaterEncoding("uft-8");
		PrintWriter writer = response.getWriter();
		response.setContentType("text/html");
		writer.println("<html>");
		writer.println("<head>hello doGet</head>");
		writer.println("<body>do get request</body>");
		writer.println("</html>");
	}
}

3.2 POST 요청 처리

  • post 요청에 대해서만 처리할 수 있어서 url이 /hellopost로 일치해도 405 에러 발생
  • POST: 폼에서 데이터 입력 후 전송하는데 사용하는 방식
@WebServlet(name="HelloWervlet2", urlPatterns = {"/hellopost"} )
public class HelloServlet2 extends HttpServlet {
	protected void doGet (HttpServletRequest, request, HttpServletResponse response) throws ServeletException, IOException{
		System.out.println("doPost 메소드 호출");
	}
}
  • src/main/webapp/...html 만들기
  • <form method="post" action="postsend"> <input type="text" name="user" /> ... </form>
    • action : 요청을 보낼 경로 (submit 타입의 input 버튼을 클릭하면 해당 경로로 요청 전송)
    • form 값을 보낼 action 경로와 서블릿의 urlpatterns를 일치시켜야 함
  • doPost 메소드를 정의한 클래스의 어노테이션 설정 → @WebServlet(name="LoginServlet", urlPatterns={"/postsend"})
    • urlpattern을 form 의 action 값과 동일하게 설정
    • doPost 메소드 내부에 getparameter메소드로 파라미터 전달 받음
    • ex) String user = request.getParameter("user");
      • 여기서 user는 form 내부의 input 요소의 name으로 설정한 것
  • 즉, POST 처리 메소드의 urlpattern 값은 input의 action과 같아야 하고, 요청에 대한 getParameter메소드의 값은 input의 name과 같아야 한다.

3.3 멀티 파트

  • 바이너리 데이터 전송을 위해 사용 (파일을 바이너리로 전송)
  • 서블릿 3.0 이후부터 스펙으로 추가됨 (별도의 라이브러리 필요 없음)
  • <form method="post" action="upload" enctype="multipart/form-data"> ..
  • Multipart annotation @
    • @fileSizeThreshold : 파일 업로드 시 메모리에 저장되는 임시 파일 크기 정의 (int)
    • @location : 파일 업로드시 임시 저장 디렉터리 지정 (String)
    • @maxFileSize : 업로드할 파일 초대 크기 지정 (long)
    • @maxRequestSize : request 최대 크기 지정 (long)
  • 실제 파일 정보 request.getPart로 참조
    • final Part filePart = request.getPart("file");
    • part객체의 getHeader로 파일 정보 얻어옴
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");
        //경로 form 내부에 input의 name이 destination인 값을 path로 가져옴 
        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;
    }
}

4. 서블릿 관련 객체


4.1 웹 필터

  • 클라이언트 요청에 대해 필요한 사전 작업이 있는 경우 필터 사용
  • 필터 인터페이스 상송해서 생성
  • init, destroy 메소드 가짐
  • doFilter()
package info.thecodinglive.filter;

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

// url과 상관없이 모든 jsp파일에 대해 적용 
@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("필터 동작 전");
				// FilterChain chain : 여러 개의 필터를 맵핑해서 처리하기 위한 객체 
        chain.doFilter(req, res);
        out.println("필터 동작 후");
    }

    @Override
    public void destroy() {

    }
}
  • url 맵핑을 위해 WebFilter 어노테이션 사용
    • @WelFilter("*jsp") : jsp 파일에 적용

4.2 쿠키

사용자가 방무한 사이트에 사용자의 컴퓨터에 저장되는 정보

  • HTTP 헤더 정보에 포함되어 전달됨

4.2.1 쿠키 구성요소

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

4.2.2 쿠키 생성

  • Cookie jcookie = new Cookie(name, value);
  • import javax.servlet.http.Cookie;
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 객체 = response 객체의 getWriter로 선언 
        PrintWriter out = resp.getWriter();
        out.println("<html><head><title> 쿠키 예제</title></head><body>");
        out.println("<br/>");


				// 쿠키 생성 + setMaxAge로 생성 만료 시간 1시간으로 설정 
        Cookie jcookie = new Cookie("jpub", "books");
        jcookie.setMaxAge(3600);
        resp.addCookie(jcookie);
        out.println("<a href='/readcookie'>readcookie</a></body></html>");
    }
} 

4.2.3 쿠키 읽기

  • request 객체의 getCookies 메소드로 저장된 쿠키를 cookies로 가져옴
  • cookies가 있다면 모든 쿠키들을 getName하여 name을 확인하고 "jpub"과 일치하는 키를 갖는 쿠키를 가져와서 출력하는 코드
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>");
    }
}

4.2.4 쿠키 수정 및 삭제

  • 쿠키 값을 수정하려면 같은 이름의 쿠키를 생성하고 새로운 값을 지정해야 함
Cookie modifiedCookie = new Cookie("기존 수정하고자 하는 쿠키의 name", "새로운 값");

 

  • 쿠키를 삭제하는 API가 따로 없고 유효시간을 0으로 설정하여 무효화하는 방식 사용
Cookie deletedCookie = new Cookie("name", "value"); 
deletedCookie.setMaxAge(0); 
response.addCookie(deletedCookie);

4.3 세션

서버와 클라이언트의 유효간 연결을 식별하는 정보

  • 클라이언트 요청 → 서버가 요청에 대해 식별할 수 있는 ID (세션 ID) 부여
  • 주로 세션 ID는 쿠키로 저장
  • 클라이언트 재접속시 해당 쿠키를 사용해서 세션 ID 값을 서버에 전달
  • import javax.servlet.http;

4.3.1 세션 생성

  • request.getSession();
  • 생성한 session 객체의 getId를 통해 세션 id를 얻을 수 있음
  • getCreationTime으로 세션 생성 시간 알 수 있음
  • getLastAccessedTime으로 웹브라우저가 가장 마지막에 세션에 접근한 시간을 얻을 수 있음
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>");
    }
}

4.3.2 세션 값 저장 및 삭제

  • 세션 값 설정하기
HttpSession session = req.getSession();
session.setAttribute("name", "value");

 

  • 세션 값 가져오기
HttpSession session = req.getSession();
String sessionValue = (String) session.getAttribute("name");

5. 디자인 패턴 활용


5.1 Java EE 패턴

자바 기반의 엔터프라이즈 웹 애플리케이션 개발을 위한 패턴

  • Java EE 패턴 목록
    • intercepting filter : 요청에 대한 전처리 및 후처리 솔루션 제공 (HTTP 요청 처리 메소드 등)
    • front controller : 요청 처리 관리하는 중앙 컨트롤러, 보안, 뷰 관리, 탐색 관리
    • view helper: 뷰 표현을 위해 비즈니스 로직이 가지고 있는 개념상의 helper, 비즈니스 로직과 뷰 로직을 분리하기 위해 사용
    • composite view : 레고 블럭 같은 작은 뷰를 조합해서 만든 전체 뷰
    • service to worker : front controller와 view helper를 이용해 dispather 컴포넌트 구성, 뷰에 대한 처리 이전에 동작
    • dispatcher view : service to worker과 동일하지만 뷰에 대한 처리 중에 수행되며 작은 시스템에서 더 안정적

5.2 프론트 컨트롤러 패턴

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

  • 클라이언트 요청에 대해 컨트롤러가 응답하고 결과에 따라 서블릿이나 JSP로 만든 뷰를 보여줌
  • 방법 1) response.sendRedirect(경로)
    • 응답을 서버에서 클라이언트로 보내고 나면 속성을 저장할 수 없고 다른 로직을 추가할 수 없음
  • 방법 2) requestDispatcher.forward(ServletRequest request, ServletResponse response)
    • 서버 내부에서만 흐름이 이동하기 때문에 속성을 저장할 수 있고, 클라이언트에게 바로 전달하지 않고 원하는 작업을 처리한 후에 응답을 전환할 수 있음
    • 컨트롤러를 만들때 많이 사용하는 메소드
    • 절대 경로로 지정 (상대 경로 불가능)
    • url을 if 문으로 분기처리 하여 컨트롤

5.3 커맨드 패턴

로직을 객체 안에 캡슐화해서 저장하여 컨트롤러와 같은 클래스를 수정하지 않고 재사용할 수 있게 하는 패턴

  • 커맨드 클래스는 abstract
  • request와 response를 변수로 선언하고 setter 메소드로 컨트롤러에서 req, res 인스턴스를 가져올 수 있게 한다.
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;
    }
}

 

  • 커맨드 클래스 객체에 제공할 서블릿 클래스 
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();
    }
}

 

  • 모든 요청은 frontContoller 클래스가 받고
  • Command 클래스를 상속 받은 뷰 클래스가 jsp 파일을 호출
  • 프론트 컨트롤러로 뷰 페이지 요청을 한 곳에서 처리할 수 있게 함.

References

  • 스프링 부트로 배우는 자바 웹 개발 (윤석진) 기술서를 참고하여 정리한 내용입니다.