스터디 모음/Spring 스터디

서블릿 프로그래밍 - 2 / 서블릿 인터페이스를 구현해보자 ! / Servlet / GenericServlet

always-dev 2022. 7. 2.
반응형

서블릿 프로그래밍 - 2 / 서블릿 인터페이스를 구현해보자 ! / Servlet / GenericServlet


저는 저번 포스팅 이전에 서블릿에 대해 물어본다면 클라이언트의 요청을 처리해주는 정도로만 얘기 했겠지만

 

지금은 서블릿은 클라이언트의 요청을 받아 웹 서버와 데이터 전송 간의 규칙을 지켜주는 CGI 프로그램이라고 할 수 있고

 

실제로 서블릿 컨테이너가 서블릿의 생명주기를 관리해주어 서블릿이 직접 웹서버와 통신하지 않고 서블릿 컨테이너를 거쳐서 통신함으로 CGI 규칙을 직접 개발하지 않고 서블릿 규칙을 통해 훨씬 간편하게 개발이 가능하게 해주는 역할을 하고 있습니다.

 

여기서 말하는 규칙은 HTTP 프로토콜을 의미합니다.

 

라고 얘기할 수 있게 됐습니다 !

 

 

이전 포스팅 서블릿 프로그래밍 - 1 :: 너 서블릿(Servlet) 알고 사용하니 :: CGI 프로그램과 서블릿

이전에 서블릿(Servlet)에 대해서 좀 더 자세히 알아보았습니다.

  • CGI 프로그램이 무엇인지 ?
  • 서블릿이 왜 필요한지 ?
  • 서블릿 컨테이너는 무슨 역할을 하는지 ?

 

 

 

 

내 마음 : 그정도면 그만해도 되잖아

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(저는 벽지를 열심히 바르고 다시 이 밤에 포스팅 ..)

아 무 튼

이번에는 이론이 아닌 실습으로 서블릿에 대해 더 알아보도록 하겠습니다.

 

 

 

1. 서블릿 직접 구현해보기 !

서블릿이 어떻게 구동 되는지 알아보면 후에 Web Server, WAS의 동작원리를 이해하는데 도움이 될 겁니다 !

이번에 해볼 일들은 다음과 같습니다.

  1. Servlet 인터페이스 구현해보기
  2. Servlet 인터페이스와 GenericServlet 추상클래스
  3. web.xml 로 서블릿 직접 매핑해보기
  4. @WebServlet 어노테이션 사용

 

 

 

1-1. 프로젝트 생성

아래 깃허브는 프로젝트 완성 자료입니다.

MyStudy/스프링/servelt-programming at master · hyena0608/MyStudy

 

GitHub - hyena0608/MyStudy: Java, CleanCode, SpringBoot, JPA, 개발도서 등을 정리하는 레포 입니다.

Java, CleanCode, SpringBoot, JPA, 개발도서 등을 정리하는 레포 입니다. - GitHub - hyena0608/MyStudy: Java, CleanCode, SpringBoot, JPA, 개발도서 등을 정리하는 레포 입니다.

github.com

저는 IntelliJ 환경에서 진행하겠습니다 !

Generators : Java Enterprise

Template : Web Application

Application Server : Tomcat 9.0.64

Language : Java

Build System : Gradle

JDK : 11.0.14

프로젝트를 생성하면 다음과 같은 4가지 폴더를 볼 수 있습니다.

  • build
  • WebApp
  • WebApp/WEB-INF
  • WebApp/WEB-INF/web.xml

WebApp

  • HTML, CSS, JS 등이 존재하게 됩니다.

WebApp/WEB-INF/web.xml

  • 웹 애플리케이션 배치 설명서(Deployment Descriptor) 파일
  • 서블릿, 필터, 리스너, 매개변수, 기본 웹 페이지 등 웹 애플리케이션 컴포넌트들의 배치 정보를 넣습니다.
  • 서블릿 컨테이너는 클라이언트의 요청을 처리할 때 이 파일의 정보를 참고하여 서블릿 클래스를 찾거나 필터를 실행합니다.

 

1-2. 서블릿 인터페이스 구현해보기

먼저 javax.servlet.Servlet 인터페이스를 구현한 HelloWorld 클래스를 작성하겠습니다.

 

Servlet과 Servlet Container가 있음으로써 CGI 규칙을 직접 구현하지 않아도 되어 간편해졌습니다.

또한 위에서 말했듯이 Servlet은 생명주기가 있고 Servlet Container가 관리합니다.

먼저 Servlet 인터페이스를 왜 구현해야 할까요?

바로 서블릿 컨테이너가 서블릿에 대해 호출할 메서드를 정의한 것이 Servlet 인터페이스 이기 때문입니다.

 

또한 Servlet.java에 들어가면 다음과 같이 문서가 작성되어 있습니다.

A servlet is a small Java program that runs within a Web server. 

Servlets receive and respond to requests from Web clients,
usually across HTTP, the HyperText Transfer Protocol.

 

 

 

 

1-2-1. javax.servlet.Servlet 인터페이스

먼저 Servlet 인터페이스를 구현하게 되면서 메서드들을 오버라이딩 해줘야 합니다.

아래 그림과 같은 상황이 나옵니다.

이 5가지 메서드 중 서블릿의 생명주기 메서드는 다음과 같이 3가지 입니다.

  • init()
    • 서블릿 컨테이너가 서블릿을 생성한 후 초기화 작업을 수행하기 위해 호출하는 메서드
    • 서블릿이 클라이언트의 요청을 처리하기 전에 준비할 작업을 수행하기 위해서는 이 메서드를 작성합니다.
  • service()
    • 클라이언트가 요청할 때마다 호출되는 메서드 입니다.
  • destroy()
    • 서블릿 컨테이너가 종료되거나 웹 애플리케이션이 멈출 때, 해당 서블릿을 비활성화 시킬 때 호출됩니다.

나머지 2가지 메서드는 서블릿 인터페이스의 기타 정보를 추출할 때 필요한 보조 메서드입니다.

  • getServletInfo()
    • 서블릿을 작성한 사람에 대한 정보
    • 서블릿 버전, 권리 등을 담은 문자열을 반환합니다.
  • getServletConfig()
    • 서블릿 설정 정보를 ServletConfig 객체를 반환합니다.
    • 서블릿 이름과 서블릿 초기 매개변수 값, 서블릿 환경정보를 얻을 수 있습니다.

저는 다음과 같이 Servlet 인터페이스를 구현하였습니다

// HelloWorld.java
public class HelloWorld implements Servlet {
    ServletConfig config;

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("init() 호출됨");
        this.config = config;
    }

    @Override
    public ServletConfig getServletConfig() {
        System.out.println("getServletConfig() 호출됨");
        return this.config;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        System.out.println("service() 호출됨");
    }

    @Override
    public String getServletInfo() {
        System.out.println("getServletInfo() 호출됨");
        return "version=1.0;author=parkhyunseo;copyright=parkhyunseo 2022";
    }

    @Override
    public void destroy() {
        System.out.println("destroy() 호출됨");
    }
}

 

 

 

 

 

1-3. 서블릿 배치 정보 작성하기

이제 서블릿을 작성했으면 서블릿을 등록해줘야 합니다.

저희는 전에 말한 웹 애플리케이션 배치 정보가 들어가는 web.xml에 서블릿 배치 정보를 추가하겠습니다.

 

 

 

🤷‍♂️ 근데 web.xml에 적는다고 해서 누가 서블릿을 찾아주는 건데 ? 귀찮은데

 

 

일단 질문에 대답은 바로 ‘서블릿 컨테이너’가 서블릿을 찾아줍니다.

하지만 서블릿을 효율적으로 찾기 위해서, 접근하기 위해서는 다음과 같은 정보가 필요하고 납득이 갈 겁니다.

  1. 실제 서블릿 파일의 위치
  2. 서블릿 접근 url

그렇다면 결국 web.xml (DD 파일)에 등록되지 않은 서블릿은 서블릿 컨테이너가 찾을 수 없겠죠 ?

그래서 저희는 DD 파일인 web.xml에 서블릿 배치 정보를 다음과 같이 추가하게 됩니다.

  • 서블릿 선언 (서블릿 별명, 클래스명)
  • 서블릿 url 등록

아래에서 더 자세한 얘기를 하겠습니다.

먼저 배치 기술서 (Deployment Descriptor :: DD파일)이라 불리는 web.xml파일에 서블릿 배치 정보를 등록하겠습니다.

저희는 HelloWorld에 서블릿을 구현 했었고 배치 정보는 다음과 같습니다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- 서블릿 선언 -->
    <servlet>
        <servlet-name>Hello</servlet-name> <!-- 서블릿 별명 -->
        <servlet-class>com.hyena.serveltprogramming.servlets.HelloWorld</servlet-class> <!-- 서블릿 클래스명 -->
    </servlet>

    <!-- 서블릿을 URL과 연결(매핑) -->
    <servlet-mapping>
        <servlet-name>Hello</servlet-name> <!-- <servlet> 태그에서 정의한 서블릿 별명 -->
        <url-pattern>/Hello</url-pattern> <!-- 서블릿을 요청할 때 클라이언트가 사용할 URLx -->
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
        <welcome-file>default.html</welcome-file>
        <welcome-file>default.htm</welcome-file>
        <welcome-file>default.jsp</welcome-file>
    </welcome-file-list>
</web-app>

처음에 말했듯이 서블릿 배치 정보는 크게 2가지로 나뉘게 됩니다.

  1. 서블릿 선언
  2. 서블릿 url 등록

먼저 ‘서블릿 선언'에 대해 알아보겠습니다.

 

 

 

1-3-1. 서블릿 선언

먼저 서블릿 배치 정보를 작성할 때 <servlet> 태그를 이용하여 ‘서블릿 별명’,과 ‘서블릿 클래스명'을 등록 해줍니다.

<!-- 서블릿 선언 -->
<servlet>
    <servlet-name>Hello</servlet-name> <!-- 서블릿 별명 -->
    <servlet-class>com.hyena.serveltprogramming.servlets.HelloWorld</servlet-class> <!-- 서블릿 클래스명 -->
</servlet>

 

 

 

1-3-2. 서블릿 URL 등록, 서블릿 매핑

클라이언트에서 서블릿 실행 요청하기 위해서는 URL이 필요합니다.

따라서 서블릿에 URL에 등록함으로써 해결이 가능해집니다.

URL을 등록할 때는 다음과 같이 <servlet-mapping> 태그를 사용합니다.

<!-- 서블릿을 URL과 연결(매핑) -->
<servlet-mapping>
    <servlet-name>Hello</servlet-name> <!-- <servlet> 태그에서 정의한 서블릿 별명 -->
    <url-pattern>/Hello</url-pattern> <!-- 서블릿을 요청할 때 클라이언트가 사용할 URLx -->
</servlet-mapping>

이제 서블릿 배치 파일 작성이 완료 되었습니다.

이제 클라이언트가 해당 서블릿 url에 접속하면 다음과 같이 서블릿이 실행 됩니다

 

 

🙋‍♂️ 그러면 매핑된 url에 들어가면
서블릿 컨테이너가 서블릿을 관리해주니까
서블릿 컨테이너가 서블릿을 찾아와서 생성하겠네요.
그리고 처음에 생성되면 init()이
서비스가 실행될 때 마다 service() 가
그리고 끝날 때는 destroy()
맞죠 ??

 

오 맞습니다.

조금 더 추가하자면

서블릿(.servlet)이 있을 때와 없을 때

그리고

service()로 만들어진 결과는

HTTP 프로토콜을 준수하는 형태로 웹 서버에 넘어가고 웹 서버에서 클라이언트에게 넘어가게 됩니다.

 

아래에 좀 더 정리를 해보았습니다.

 

 

 

 

 

1-4. 서블릿 구동 절차

다음은 클라이언트가 등록한 서블릿 url을 요청하면 다음과 같은 절차가 발생됩니다.

  1. 클라이언트의 요청이 들어오면 서블릿 컨테이너는 서블릿을 찾습니다.
  2. 서블릿(.servlet)이 없다면 ‘서블릿 클래스'를 로딩합니다.
  3. 인스턴스를 준비하여 생성자를 호출합니다.
  4. 서블릿 초기화 메서드인 init()를 호출하고
  5. 클라이언트 요청을 처리하는 service() 메서드를 호출합니다.
  6. service() 메서드에서 만든 결과를 ‘HTTP 프로토콜’에 맞추어 클라이언트에 응답합니다.
  7. 서블릿 컨테이너 종료 또는 웹 애플리케이션 종료 시에 서블릿이 마무리 작업을 수행할 수 있도록 생성된 모든 서블릿이 destory() 메서드를 호출합니다.

 

 

 

 

2-1. Servlet 인터페이스와 GenericServlet 추상클래스의 관계

 

🙅‍♂️ 아니 근데 매번 이렇게 메서드 5개나 구현… 저는 못해요


 

🤷‍♂️ 그리고 데이터를 응답하는데 … 뭐 어디서 어떻게 넣어서 응답합니까 ? 요청 받을 때도 모르겠고..


 

알겠습니다 !!

이제 아래와 같은 방법들을 알아보겠습니다.

  1. 서블릿을 더 간단하게 구현하는 방법과
  2. 서블릿으로 데이터를 보내는 방법
  3. 서블릿에서 데이터를 꺼내는 방법

저희는 Servlet 인터페이스를 구현하지 않고 GenericServlet 추상 클래스를 상속 받겠습니다.

GenericServlet 추상클래스는 Servlet 인터페이스에 선언된 5가지 메서드를 모두 구현되어 있습니다.

 

아래 그림과 관계가 같습니다.

 

 

 

🤷‍♂️ 아니 왜 service()는 구현되어 있지 않나요?

 

엥 아닙니다.

위에 글을 다시 보시면 GenericServlet 추상클래스는 Servlet 인터페이스에서 선언된 5가지 메서드가 모두 구현 되어 있습니다.

단, service() 같은 경우 데이터의 응답값이 계속 변해야 하는 경우가 훨씬 많기 때문에

저희가 직접 구현해줘야 하는 경우가 많아서 그림에서는 생략했습니다.

하지만 그 외 4가지 메서드인 init(), destory(), getServletConfig(), getServletInfo() 같은 경우는 특이한 상황이 아닌 이상 메서드를 재구현할 필요가 없기 때문입니다.

 

 

 

 

 

2-1-1. GenericServlet 추상 클래스로 서블릿 작성해보기

GenericServlet 추상클래스로 서블릿을 작성하는 이유는 더 간편하기 때문입니다.

데이터 요청, 응답값을 가져오는 부분은 service()에 매개변수 ServletRequest, ServletResponse로 받아주면 됩니다.

물론 이 경우는 Servlet 인터페이스를 구현하는 구현체에서도 가능합니다.

저는 다음과 같이 CalculatorServlet를 작성하였습니다.

public class CalculatorServlet extends GenericServlet {
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        int a = Integer.parseInt(req.getParameter("a"));
        int b = Integer.parseInt(req.getParameter("b"));

        res.setContentType("text/plain");
        res.setCharacterEncoding("UTF-8");
        PrintWriter writer = res.getWriter();
        writer.println("a = " + a + ", " + "b = " + b + "의 계산 결과 입니다.");
        writer.println("a + b = " + (a + b));
        writer.println("a - b = " + (a - b));
        writer.println("a * b = " + (a * b));
        writer.println("a / b = " + ((float) a / (float) b));
        writer.println("a % b = " + (a % b));

    }
}
  • 오직 service() 메서드만 구현 했습니다.
  • service() 메서드 내에 매개변수 타입이 ServletRequest와 ServletResponse가 있습니다.

 

 

 

 

2-1-2. ServletRequest, ServletResponse객체와 기능

service()의 매개변수 중 ServletRequest 객체는

클라이언트의 요청 정보를 다룰 때 사용합니다.

ServletRequest 객체는 다음과 같은 기능들이 있습니다.

  • getRemoteAddr()
  • getScheme()
  • getProtocol()
  • getParameterNames()
  • getParameterValues()
  • getParameterMap()
  • setCharacterEncoding()

ServletResponse 객체는 get이 아닌 set으로 되어 있다고 생각하면 편할 겁니다.

 

더 자세한 내용은 아래 링크를 참조 해주세요.

ServletRequest (Servlet API Documentation)

 

ServletRequest (Servlet API Documentation)

Stores an attribute in this request. Attributes are reset between requests. This method is most often used in conjunction with RequestDispatcher. Attribute names should follow the same conventions as package names. Names beginning with java.*, javax.*, and

tomcat.apache.org

ServletResponse (Servlet and JavaServer Pages API Documentation)

 

ServletResponse (Servlet and JavaServer Pages API Documentation)

Sets the preferred buffer size for the body of the response. The servlet container will use a buffer at least as large as the size requested. The actual buffer size used can be found using getBufferSize. A larger buffer allows more content to be written be

tomcat.apache.org

 

 

 

 

저희는 서블릿을 작성했습니다.

이제 뭐를 해야할까요? 바로 클라이언트가 해당 CalculatorServlet을 요청할 수 있을까요?

 

저희는 클라이언트의 요청을 서블릿 컨테이너가 받아서 해당 서블릿을 찾을 수 있도록 매핑 해줘야 합니다.

 

그렇다면 매핑은 어디에 했을까요?

 

바로 DD파일 입니다.

DD파일의 약자는 Deployment Descriptor 였고 배치 정보 파일이자 web.xml을 가리켰습니다.

 

다음과 같이 작성했습니다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- 서블릿 선언 -->
    <servlet>
       <servlet-name>Calculator</servlet-name>
       <servlet-class>com.hyena.serveltprogramming.servlets.CalculatorServlet</servlet-class>
    </servlet>

    <!-- 서블릿을 URL과 연결(매핑) -->
    <servlet-mapping>
        <servlet-name>Calculator</servlet-name>
        <url-pattern>/calc</url-pattern>4
    </servlet-mapping>

</web-app>

이제 톰캣 서버를 가동하고 localhost:8080/프로젝트명/calc 로 들어가면 클라이언트는 원하는 응답을 받을 것입니다.

 

 🤷‍♂️ 확실히 service() 메서드 하나만 작성하니 편하긴 하네요. 근데 혹시 DD파일을 작성하지 않을 수는 없나요 ??

 

 

😁 당연히 대체 가능한 ‘어노테이션’이 있습니다.

DD 파일에 서블릿 배치 정보를 저희가 직접 하나한 고생할 필요가 없습니다.

아래에서 바로 보시죠.

 

 

 

 

 

 

2-1-3. @WebServlet 어노테이션을 이용한 서블릿 배치 정보 등록

저희는 이제 Servlet 3.0 부터 어노테이션으로 서블릿 배치 정보를 등록할 수 있습니다.

web.xml에 태그를 하나하나 적으면서 매핑하는 번거로운 일을 하지 않아도 되죠 !!

바로 코드를 보면 이해하기 수월 할 것입니다.

예제는 위에서 작성한 CalculatorServlet 파일이고 web.xml에 작성했던 서블릿 매핑 정보는 ‘주석' 처리 하겠습니다.

먼저 web.xml에 작성한 기존 CalculatorServlet 서블릿 배치 정보를 주석처리 하겠습니다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!-- 서블릿 선언 -->
<!--    <servlet>-->
<!--        <servlet-name>Calculator</servlet-name>-->
<!--        <servlet-class>com.hyena.serveltprogramming.servlets.CalculatorServlet</servlet-class>-->
<!--    </servlet>-->

    <!-- 서블릿을 URL과 연결(매핑) -->
<!--    <servlet-mapping>-->
<!--        <servlet-name>Calculator</servlet-name>-->
<!--        <url-pattern>/calc</url-pattern>-->
<!--    </servlet-mapping>-->

</web-app>

이제 CalculatorServlet에 돌아가 @WebServlet 어노테이션을 작성하겠습니다.

@WebServlet("/calc")
public class CalculatorServlet extends GenericServlet {
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        int a = Integer.parseInt(req.getParameter("a"));
        int b = Integer.parseInt(req.getParameter("b"));

        res.setContentType("text/plain");
        res.setCharacterEncoding("UTF-8");
        PrintWriter writer = res.getWriter();
        writer.println("a = " + a + ", " + "b = " + b + "의 계산 결과 입니다.");
        writer.println("a + b = " + (a + b));
        writer.println("a - b = " + (a - b));
        writer.println("a * b = " + (a * b));
        writer.println("a / b = " + ((float) a / (float) b));
        writer.println("a % b = " + (a % b));

    }
}

이제 끝입니다.

 

이전과 똑같이 클라이언트가 localhost:8080/프로젝트명/calc 에 들어가면 서블릿 컨테이너가 요청을 받아 @WebServlet에 의해 CalculatorServlet을 찾을 수 있고 정상적으로 작동합니다.

 

web.xml에 써야하던 그 수많은 태그들을 어노테이션 단 한줄로 표현하면 되어 너무 손쉬워졌습니다.

 

🙋‍♂️ 확실히 줄어들었긴 한데 기능이 고작 이게 끝인가요 ?

 

@WebSerlvet 어노테이션은 이 뿐만 아니라 더 많은 기능을 가지고 있습니다.

괄호 안에 들어가는 속성들이 있는데

설명을 하면 다음과 같습니다.

  • name
    • 서블릿 이름을 설정
  • urlPatterns
    • 일반적인 문자열로 표시 가능
    • 중괄호를 사용하여 여러 url 등록 가능
  • value
    • urlPatterns와 같은 용도

아래와 같이 사용 할 수 있습니다.

@WebServlet(value="/calc", name="Calculator")

@WebServlet(urlPatterns={"/calc", "calc.do"})

@WebServlet("/calc")

@WebServlet(value="/calc")

정리

이제 저희는 GenericServlet 추상클래스로 service() 메서드만 오버라이딩하여 간단하게 구현할 수 있고

DD파일을 매번 작성하던 불편함을 @WebServlet 어노테이션의 활용으로 할 일이 매우 줄어들었습니다.

 

이제 서블릿에 대해서 배웠으니 다음에는 WebServer와 WAS(Web Application Server)에 대해 배워야겠네요~~

 

 

 

반응형

댓글