새소식

반응형
Back-end/Spring

[Spring] Part 1-4-3. 나만의 MVC 프레임워크 만들기 | 계산기 서블릿 만들기

2023.01.15
  • -
반응형

이번시간에는 지난 시간에 다루었던 서블릿을 만들어볼 것입니다. Part1에서 계속 다루었던 계산기 서블릿을 만들어보겠습니다.

 

이론 - 서블릿

만들어보기에 앞서 서블릿에 대한 간단한 이론 설명을 하도록 하겠습니다.

사진을 보시면 맨 위에 Servlet 인터페이스가 있고, 이를 구현한 GenericServlet, 그리고 이를 상속할 HttpServlet 이 있습니다.

 

이번 포스팅에서 계산기를 만들면서는 HttpServlet을 상속하여 만들 것입니다. 하지만, 서블릿 인터페이스와 제네릭 서블릿, 그리고 최종적으로 HttpServlet이 왜 등장하게 되었는지는 계산기 프로그램을 서블릿으로 만들어보면서 설명하도록 하겠습니다.

 

Servlet 인터페이스는 호출 규약이라고 볼 수 있습니다. 즉, 서블릿 컨테이너(톰캣)과 같이 호출하기 위해서 서블릿 인터페이스를 구현한 서블릿을 만들 것입니다.

 

따라서 서블릿 인터페이스에 있는 메소드들은 서블릿 컨테이너가 호출한다고 보시면 됩니다.

 

 

서블릿 인터페이스와 관련된 메소드는 크게 두 가지가 있습니다.

  • 서블릿 생명주기(Life Cycle)와 관련된 메소드
    • init(): 서블릿 컨테이너가 서블릿 생성 후 초기화 작업을 수행하기 위해 호출하는 메소드
    • service(): 클라이언트 요청이 들어올 때마다 서블릿 컨테이너가 호출하는 메소드
    • destroy(): 서블릿 컨테이너가 종료될 때 호출되는 메소드
  • 서블릿 기타 메소드
    • getServletConfig(): 서블릿 초기 설정 정보(서블릿 이름, 초깃값)를 담고있는 객체를 반환하는 메소드
    • getServletInfo(): 서블릿을 작성한 사람, 서블릿 버전, 저작권과 같이 서블릿에 대한 정보를 반환하는 메소드

 

여기서 의문이 들 수 있는 부분은 스프링으로 개발을 진행하면서는 서블릿을 모르고도 개발을 할 수도 있습니다. 그럼에도 불구하고 서블릿을 학습해야 하는 이유는 무엇일까요?

 

그에 대한 대답은 다음 그림을 보면서 할 수 있습니다.

이는 Spring MVC에 대한 간단한 flow를 나타낸 그림입니다. 여기서 가장 핵심이 되는 부분이 Dispatcher Servlet입니다. 이 부분을 이해해야 Spring MVC에 대한 전체적인 flow를 이해할 수 있을 것입니다.

 

Dispatcher Servlet은 이름에서도 알 수 있듯이 Servlet입니다. 따라서 이를 잘 이해하기 위해서는 Servlet이 무엇인지, 서블릿이 어떻게 호출되고 어떠한 flow를 가지는지를 알아야 하는 것입니다.

 

 

2. 계산기 서블릿 구현

순서는 서블릿 인터페이스를 가지고 계산기 서블릿을 만들고, 그 다음 GenericServlet으로 만든 후, 마지막으로 HttpServlet으로 만들어 볼 것입니다.

 

먼저 CalculatorServlet 클래스 파일을 생성합니다. 여기서는 Servlet 인터페이스를 구현할 것이기 때문에 implements Servlet을 해 줍니다.

 

그 후 메소드 Override를 하게 되면 앞서 배웠던 5가지의 메소드가 나오는데 이 중에서 Life Cycle과 관련된 메소드는 init(), service(), destroy() 입니다.

 

public class CaculatorServlet implements Servlet {
    private static final Logger log = LoggerFactory.getLogger(CaculatorServlet.class);
    @Override
    public void init(ServletConfig config) throws ServletException {
        log.info("init");
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        log.info("service");
    }

    @Override
    public void destroy() {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }



    @Override
    public String getServletInfo() {
        return null;
    }


}

 

우리는 앞쪽에서 서블릿 객체를 싱글톤으로 관리한다고 했습니다. 즉, 인스턴스를 하나만 생성하여 공유하는 방식이라는 의미입니다. 따라서 이 CalculatorServlet라는 클래스는 이 메소드를 호출하려면 인스턴스를 만들어야 하는데 인스턴스 하나만을 만들어서 공유하는 식인 것입니다. 이를 보여주기 위해 init()과 service()에 로그를 찍어보도록 하겠습니다.

 

그에 앞서 UrlPath와 해당 서블릿을 매핑(mapping)하기 위해서 어노테이션(annotation) 방법을 사용하겠습니다.

클래스 선언 바로 윗줄에 @WebServlet("/calculate") 를 적어주면 됩니다.

 

main 메소드를 다음과 같이 작성 합니다.

public class WebApplicationServerLauncher {
    private static final Logger logger = LoggerFactory.getLogger(WebApplicationServerLauncher.class);
    public static void main(String[] args) throws Exception {
        String webappDirLocation = "webapps/";
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);

        tomcat.addWebapp("/", new File(webappDirLocation).getAbsolutePath());

        logger.info("configuring app with basedir: {}", new File("./" + webappDirLocation). getAbsolutePath());

        tomcat.start();
        tomcat.getServer().await();

    }
}

 그 후 http.test 파일을 하나 만들어 GET http://localhost:8080/calculate 에게 요청을 날리면 아래와 같이 로그가 정상적으로 찍히는 것을 볼 수 있습니다.

  즉, 서블릿 컨테이너 톰캣이 해당하는 서블릿의 인스턴스를 만들 때만 init()을 먼저 호출하고 그 다음 service()를 호출한다는 사실을 알 수 있습니다.

 

이후에 요청이 계속 들어와도 init()은 만들어질 때 딱한번만 호출되기 때문에 그 이후에는 service()만 호출 됩니다.

 

 

그러면 이제 service 메소드를 구현해 보도록 하겠습니다.

@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
    log.info("service");
    int operand1 = Integer.parseInt(request.getParameter("operand1"));
    String operator = request.getParameter("operator");
    int operand2 = Integer.parseInt(request.getParameter("operand2"));

    int result = Calculator.calculate(new PositiveNumber(operand1), operator, new PositiveNumber(operand2));

    PrintWriter writer = response.getWriter();
    writer.println(result);

}

클라이언트로부터 operand1과 operator, operand2라는 parameter를 던져서 Calculator 인스턴스를 통해 값을 계산하고, 나온 결과를 response.getWriter() -> writer.println(result) 과정을 통해 응답을 하는 코드입니다.

 

그러고 나서 WAS를 재실행하여 request를 날려보면 아래 화면과 같이 정상적으로 응답이 온 것을 확인할 수 있습니다.

지난 번 톰캣을 직접 구현하여 계산 결과를 봤을 때는 5s 가 응답하는데 걸렸는데 실제 톰캣을 사용하여 구현해 보니 87ms로 응답속도가 기하급수적으로 늘어난 것 또한 볼 수 있습니다.

 

우리는 이걸 만들면서 destroy(), getServletConfig(), getServletInfo()은 사용할 필요가 없더라도 그 안의 내용을 비운채라도 구현을 해 주어야 했습니다.

 

그런데 사실 필요가 없는데도 위처럼 구현을 해야 한다는 것은 굉장히 비효율적입니다.

 

그래서 나온 것이 GenericServlet 입니다.

 

제네릭 서블릿은 간단히 말해서 destroy(), getServletConfig(), getServletInfo()처럼 잘 사용되지 않는 메소드는 필요할 때만 Override 하여 구현하고 서비스에 필요한 핵심적인 service() 메소드만 구현하면 됩니다.

 

또한 제네릭 서블릿은 추상 클래스이기 때문에 상속할 때 'extends GenericServlet' 과 같이 해 주어야 합니다.

 

실제로 제네릭 서블릿을 살펴보면 service 메소드만 추상 메소드이기 때문에 나머지 잘 사용되지 않는 init, destory, getServletConfig 등의 메소드들은 필요할 때만 override 하면 됩니다.

 

따라서 GenericServlet에서는 service 메소드를 구현해 주시면 됩니다.

@WebServlet("/calculate")
public class CaculatorServlet extends GenericServlet {
    private static final Logger log = LoggerFactory.getLogger(CaculatorServlet.class);
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        log.info("service");
        int operand1 = Integer.parseInt(request.getParameter("operand1"));
        String operator = request.getParameter("operator");
        int operand2 = Integer.parseInt(request.getParameter("operand2"));

        int result = Calculator.calculate(new PositiveNumber(operand1), operator, new PositiveNumber(operand2));

        PrintWriter writer = response.getWriter();
        writer.println(result);

    }
}

그래서 보시면 아시겠지만 코드가 굉장히 간결해 진 것을 볼 수 있고 WAS를 재실행 하여 요청을 날려보면 정상적으로 작동하는 것을 볼 수 있습니다.

 

 


마지막으로 HttpServlet 추상 클래스는 어떤 클래스인지 보도록 하겠습니다. 먼저 HttpServlet을 extends 해 줍니다.

 

여기서는 GET 메소드를 요청하기 때문에 doGet 메소드를 오버라이드 합니다. 추상클래스에서 원하는 메소드를 override 하고 싶을 때 오버라이드를 편하게 하려면 test를 만들 때와 같이 Alt + Insert 키를 눌러 override method 옵션을 선택하시면 됩니다.

@WebServlet("/calculate")
public class CalculatorServlet extends HttpServlet {
    private static final Logger log = LoggerFactory.getLogger(CalculatorServlet.class);
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("service");

        int operand1 = Integer.parseInt(req.getParameter("operand1"));
        String operator = req.getParameter("operator");
        int operand2 = Integer.parseInt(req.getParameter("operand2"));

        int result = Calculator.calculate(new PositiveNumber(operand1), operator, new PositiveNumber(operand2));

        PrintWriter writer = resp.getWriter();
        writer.println(result);
    }
}

HttpServlet과 GenericServlet과의 차이는 GET 메소드나 PUT, DELETE 와 같은 것들을 HttpServlet은 메소드로 구현을 하기만 하면 됩니다.(각각 doGet, doPut, doDelete...)

 

HttpServlet도 service라는 메소드가 존재합니다. 앞서 내부적으로 톰캣이 service()를 호출한다고 했는데 HttpServlet에서도 역시 service가 호출됩니다. 그리고 service() 메소드를 들여다 보시면 그 안에서 GET 메소드인 경우 doGet을 호출하시는 것을 볼 수 있습니다.

 

doGet은 우리가 불러서 작성하지 않으면 GET 요청을 해도 그대로 끝마치는데 우리가 Override하여 새로 작성을 해 줌으로써 서비스 로직을 구현한대로 GET 요청에 응답할 수 있게 되는 것입니다.

 

 


그런데 여기서 재밌는 점이 하나 있습니다.

GET 요청을 할 때 UrlPath 부분에 operator queryString을 "+"로 하여 요청을 날려보면 올바른 사칙연산이 아니라는 오류가 납니다. 이는 왜 그런 것일까요?

 

 

그 이유를 알아보기 앞서 먼저 URL 인코딩(=퍼센트 인코딩)이라는 것이 있습니다. 즉, URL로 사용할 수 없는 문자를 사용할 수 있게끔 인코딩 해 주는 것을 말하는데요.

URL로 사용할 수 없는 문자란 예약어, Non-ASCII 문자(한글) 등을 말합니다. 이것들은 URI에서 굉장히 중요한 문법적 의미를 가지고 있기 대문에, 그 의미로 사용할 것이 아니라면 반드시 인코딩을 해 주어야 하는 것입니다.

 

인코딩 된 문자는 triplet(세 개가 한 세트)로 인코딩 되며 각각을 % 다음에 두 개의 16진수로 표현합니다.

(예를 들어, '홍'이라는 문자를 인코딩 하면 '%ec%99%8d'와 같이 됩니다.)

 

 

예약어에는 어떠한 것들이 있는지는 아래 링크를 통해 한 번 확인해 보시면 도움이 될 것 같습니다.

 

퍼센트 인코딩 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 퍼센트 인코딩(percent-encoding)은 URL에 문자를 표현하는 문자 인코딩 방법이다. 이 방법에 따르면 알파벳이나 숫자 등 몇몇 문자를 제외한 값은 옥텟 단위로 묶어

ko.wikipedia.org

 

따라서 + 기호를 URL에 포함시키면 +는 예약 문자이기 때문에 인코딩을 해야했기 때문에 오류가 발생했던 것입니다. +는 인코딩을 하면 %2b로 인코딩이 되고 +라는 기호 대신 %2b를 넣어주어야 요청이 정상적으로 수행하는 것을 확인하실 수 있습니다. 

 

최종적으로 우리는 HttpServlet을 이용해서 프로그래밍을 하면 되지만 Servlet 인터페이스가 무엇이고 GenericServlet은 어떤 부분을 보완한 것인지 알아보면서 더 잘 사용할 수 있도록 연습을 해 보았습니다.

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.