땃쥐네

[Spring MVC] 서블릿(Servlet)이란? 본문

Spring/Spring Web

[Spring MVC] 서블릿(Servlet)이란?

ttasjwi 2022. 7. 24. 17:13

서블릿 학습의 필요성

Spring MVC를 이용하면 웹 애플리케이션 구현을 매우 손쉽게 할 수 있습니다. 하지만 이 Spring MVC는 서블릿이라는 오래된 자바 표준 기술을 기반으로 동작합니다. 따라서 Spring MVC에 대해 깊은 이해를 하기 위해서는 우선 Servlet을 이해할 필요가 있습니다. 


요청과 응답은 Http 메시지를 통해 이루어진다.

요즘 사용되는 웹 애플리케이션 대 부분은 Http 프로토콜을 기반으로 동작합니다.

 

Http 프로토콜에서 정해진 사양에 따라 Http 메시지를 작성하고 HTML 문서, 단순 텍스트, 이미지, 음성, 영상, 파일, JSON, ... 등등 다양한 형태의 데이터를 끊임 없이 빠르고 간편하고 정확하게 주고받을 수 있습니다.

 

이 프로토콜에서 정의된 방식대로 Http 메시지를 두 컴퓨터(클라이언트, 서버)가 주고 받음으로서 데이터를 쉽고 빠르고 안전하게 주고받을 수 있는데요. 이 Http 메시지 사양에 대한 내용은 이 글의 범위를 벗어나고 생각보다 공부할게 많은 영역이라서 이 글에서 다루지는 않겠습니다.

 

(개인적인 경험 상 시작부터 너무 깊게 배우지 말고 아주 기본적인 지식만 가볍게 한 번 훑은 뒤 Spring MVC를 학습하면서 그 때 그 때 Http 지식을 함께 병행, 보강하는 것이 좋은 학습 방법이라 생각하는데 이 학습 과정에서 인프런의 김영한님 Http 강의를 한번 쓱 듣고, Spring Mvc 강의(1편, 2편)를 학습하면서 병행, 보강하는 것을 추천합니다.)


서블릿의 도입 배경 : 너무 많은 Http 메시지 처리 작업

우리 서비스는 구글이 아니긴 하지만... 일단 회원 가입을 한다 생각을 해보자!

회원을 등록, 관리하는 서비스를 상상해봅시다.

 

고객은 웹 브라우저를 통해 넘어온 회원 가입 페이지에서 신상 정보를 기입할 것이고, 가입 버튼을 누를 시 이 데이터가 담긴 Http 메시지가 우리 서비스의 WAS(Web Application Server)에 넘어오게 될 것입니다. WAS에서는 이 메시지를 분석해서 회원을 등록하는 비즈니스 로직을 수행한 뒤 사용자에게 적절한 응답을 해줘야합니다.

 

근데 말은 쉽지 이걸 생 Java 코드로 작성하면 과정이 생각보다 깁니다.

우리 WAS에서는 외부의 고객 요청 메시지를 받을 수 있게 TCP/IP 서버소켓을 만들고 고객의 요청 메시지를 언제든 받아들일 수 있어야합니다. (이 소스 코드는 간단한 상황을 가정했지, 실제로는 복수의 클라이언트로부터 오는 요청을 유연하게 처리할 수 있도록 복잡한 멀티스레드 응답 환경도 제어해야합니다.)

입력 스트림을 통해 전달된 Http 메시지를 파싱해서 분석하고, 그 안에 전달된 파라미터들을 해석해야합니다.

 

- 메서드 : GET 방식인지 POST 방식인지, PUT인지, DELETE인지, ...

- url

- 요청 헤더(content-type, accept, ...)

- 쿼리 파라미터, Message Body

 

요청 메시지의 종류도 다양하고 이를 매번 유연하게 해석할 수 있어야하는데 이것들도 전부 고려한 요청 해석 로직을 작성해야합니다.

적절한 비즈니스 로직을 수행한 뒤 결과에 해당하는 Http응답 메시지를 작성하는 로직도 수행해야합니다. 단순히 200 메시지만 보내는게 아니라, 리다이렉트에 해당하는 300대 요청, 클라이언트 측 오류임을 알리는 400대 응답, 서버 측 오류임을 알리는 500대 응답 등등도 처리해야하기 때문에 이걸 고려해서 다양한 상황에 대응하는 응답 로직도 고려해야합니다.

 

단순히 정적 리소스만 반환하는게 아니라 동적 컨텐츠 리소스를 반환해야하는 경우에는 Body에 담길 데이터도 그 때 그 때 달라질테고... 때로는 쿠키를 통해 특정 데이터를 전달해야할 수도 있고 생각보다 여러가지 케이스를 고려하여 응답 로직을 작성해야합니다.

근데 정작 실질적으로 비즈니스적으로 의미 있는 것은 회원 가입 로직뿐입니다.

 

회원가입 하나 하자고 요청 메시지 해석하고, 파라미터 분석하고, 비즈니스 로직에 맞게 파라미터 변형도 하고, 응답 메시지 작성 로직도 복잡하게 구현해야합니다. 이게 너무 귀찮습니다!

이미지 출처 : 김영한님 Spring MVC 1편

결국 따져보면 요청메시지 분석, 파라미터 분석, 응답 메시지 작성 등의 로직은 거의 구조적으로 비슷한데... 매번 웹 애플리케이션을 만들 때마다 이 부분에 해당하는 코드를 작성하는 것은 너무 번거롭습니다. 매번 개발자가 이를 구현하려 들면 생각보다 경우의 수가 다양하고, 놓칠만한 포인트가 많아서 유지보수가 힘들것입니다.

 

수 많은 java 개발자들은 이런 삽질 끝에 위의 로직을 공통화가 가능할 것 같다는 결론에 이르게 되었습니다.

이렇게 하여 탄생한 것이 서블릿(Servlet)입니다.


서블릿의 도입(1997)

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response){
        //애플리케이션 로직
    }
}

서블릿은 웹 애플리케이션 개발 시, 요청-응답 흐름의 로직을 편리하게 다룰 수 있도록 제공되는 자바 표준 기술입니다.

 

자바가 처음 등장한 것이 1995년이고 서블릿이 처음 발표된 것이 1997년이죠. (이 사이 1-2년 사이에 웹 애플리케이션을 구현하셔야 했던 고대 개발자분들의 삽질에 경의를 표합니다. ㅜㅜ)

 

서블릿을 사용하는 법은 매우 간단한데요. 추상클래스 HttpServlet의 상속 클래스를 하나 만들어두고 service 인스턴스를 완성해서 적절한 비즈니스 로직을 구현해준 뒤 어노테이션을 통해 최소한의 메티데이터(매칭 url 패턴, 서블릿 등록 이름, ...)를 등록해두기만 하면 됩니다. 요청 메시지에 대한 책임은 HttpServleteRequest에 위임하면 되고, 응답 메시지 작성에 대한 책임은 HttpServletResponse에 위임합니다.

 

실질적으로 비즈니스 로직 전후의 대부분의 과정을 서블릿 컨테이너가 도맡아 처리해주니 개발자는 Http 스펙을 좀 더 편리하게 이용할 수 있고, 비즈니스 로직에 구현에 집중할 수 있게 되는 것입니다.


서블릿 방식을 통한 HTTP 요청, 응답의 흐름

이미지 출처 : 김영한 님의 Spring MVC 1편 강의

Http 요청/응답에 관한 복잡한 처리로직을 이제 서블릿 컨테이너에게 위임했습니다. (아까부터 서블릿 컨테이너라는 말이 새로 나와서 헷갈릴 수 있는데 이 부분은 아래에서 설명하겠습니다.)

 

요청이 온 순간 WAS는 요청 url 등을 분석한 뒤, HttpServletRequest, HttpServletResponse 인스턴스를 새로 만들어서 개발자가 코드로 작성해 등록해 둔 서블릿을 찾아다, 조건에 맞는 서블릿을 호출합니다.

 

그냥 개발자는 HttpServletRequest 객체의 http 요청 정보를 정해진 메서드를 통해 편리하게 꺼내서 사용하면 됩니다.

 

응답을 작성해서 보내는 복잡한 로직은 WAS에서 처리해주는데요. 개발자는 그냥 HttpServletResponse 객체에 응답에 필요한 몇 가지 정보들을 메서드를 적절히 호출해서 주입해주면 됩니다.


서블릿 컨테이너란?

서블릿 컨테이너는 개발자가 등록해둔 서블릿 코드를 기반으로 서블릿 인스턴스를 싱글톤으로 생성, 초기화, 호출, 종료하는 일련의 생명주기를 관리해주는 계층입니다.

 

즉 아까 서술한 서블릿 방식 흐름의 실질적인 핵심 주체에 해당하는 WAS죠. 주로 tomcat을 사용합니다. Spring Boot에서는 아예 tomcat을 표준 WAS로 하여 내장하고 있고... 그냥 Java/Spring 백엔드 엔지니어 입장에서는 tomcat을 서블릿 컨테이너, WAS라고 생각하시면 됩니다.

 

서블릿 컨테이너를 사용한 덕분에 개발자는 소스코드에서 수동으로 서블릿 컨테이너의 생명주기를 관리하는 코드(생성, 초기화 호출, 종료)를 작성할 필요가 없습니다.


서블릿은 싱글톤 인스턴스

고객의 요청이 올 때마다 서블릿 인스턴스를 매번 생성/ 파괴하는 것은 매우 비효율적입니다. 그래서 서블릿 컨테이너는 서블릿을 싱글톤 인스턴스로 생성해서 관리합니다.

생명주기

애플리케이션 로딩 시점에 서블릿 인스턴스를 싱글톤으로 딱 하나 생성하두고, 매번 고객의 요청이 올 때마다 같은 서블릿 인스턴스에 접근할 수 있도록 합니다. 또 다시 요청이 들어오면 다시 기존에 생성해둔 서블릿 인스턴스를 재사용할 수 있게 합니다.

 

서블릿이 파괴되는 시점은 서블릿 컨테이너가 소멸하는 시점... 즉 서버 내리는 시점이 됩니다. 

공유변수에 주의

서블릿은 싱글톤 인스턴스이므로, 공유 변수(전역변수, 서블릿의 static 변수 또는 인스턴스 변수)를 사용할 때 주의해야합니다. 이는 싱글톤 패턴의 공통적인 주의사항에 해당하죠. 사용자가 싱글톤 인스턴스(여기서는 서블릿)를 통해 비즈니스 로직을 수행할 때마다 공유변수의 상태가 변경되고 이 변수를 다른 요청에서도 참조하게 된다면 의도치 않은 상황이 발생할 수 있으니 주의해야합니다.


JSP(Java Server Pages, 1999)

        PrintWriter w = response.getWriter();
        w.write("<html>\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" +
                "</head>\n" +
                "<body>\n" +
                "성공\n" +
                "<ul>\n" +
                " <li>id="+member.getId()+"</li>\n" +
                " <li>username="+member.getUsername()+"</li>\n" +
                " <li>age="+member.getAge()+"</li>\n" +
                "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" +
                "</body>\n" +
                "</html>");

서블릿을 통해 요청, 응답 과정을 추상화할 수 있어서 편리하긴 한데...

 

서블릿만으로 HTML 페이지를 동적으로 작성하는 것은 여러모로 불편한게 많습니다. HttpServletResponse에서 출력스트림(OutputStream)을 얻어온 뒤 이를 통해 자바 코드로 HTML 코드를 동적으로 작성해줘야하거든요.

 

위의 코드를 보시면 아시겠지만 HTML 구조를 java로 직접 짜니 매우 구조 파악도 힘들고 유지보수가 힘들어집니다.

위에서는 단순하게 사용자 식별자, 이름과 같은 몇 가지 정보만 동적으로 뿌려주는데 여기서 뿌려줘야할 데이터가 더 많아지면 View 코드를 java로 짜는 것도 한세월일겁니다.

 

<%@ page import="com.ttasjwi.servlet.domain.Member" %>
<%@ page import="com.ttasjwi.servlet.domain.MemberRepository" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    //request, response 사용 가능.

    MemberRepository memberRepository = MemberRepository.getInstance();

    System.out.println("MemberSaveServlet.service");
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));

    Member member = new Member(username, age);
    memberRepository.save(member);
%>
<html>
<head>
    <title>Title</title>
</head>
<body>
성공
<ul>
    <li>id=<%=member.getId()%></li>
    <li>username=<%=member.getUsername()%></li>
    <li>age=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

그래서 등장한게 JSP입니다. JSP를 사용하면 jsp 문법에 따라 jsp를 작성하면 이 jsp파일이 서블릿으로 변환되어서 HTML을 응답할 수 있습니다. 결국 jsp라는 기술도 서블릿 기반으로 돌아가기 때문에 서블릿에 대한 기본적인 지식을 학습 후 사용해야합니다. (물론, 요즘 jsp는 오래된 기술이고... JSON을 통한 API 통신이 최신 트렌드기 때문에 jsp는 간단하게 손만 살짝 대는 정도면 됩니다.)


동시요청 - 멀티 스레드

서블릿 없이 순수 자바 코드로 WAS를 구현하려 해보면, 앞선 요청/응답에 관한 로직 처리는 어떻게 할 수 있더라도 복잡한 동시요청 상황에 대한 유연한 처리가 매우 복잡해집니다. 멀티 스레드 프로그래밍은 개발자가 한땀 한땀 코드로 작성하려 들면 복잡한게 많아지죠...

 

이를 편리하게 처리할 수 있도록 서블릿 컨테이너가 도와주는데요. 이 부분은 다음 포스팅([Spring MVC] 서블릿 컨테이너와 멀티 스레드)에서 이어서 설명하겠습니다.


참고자료

- 인프런 : 김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술


'Spring > Spring Web' 카테고리의 다른 글

[Spring MVC] 서블릿 컨테이너와 멀티 스레드  (0) 2022.07.25
Comments