티스토리 뷰
Servlet(서블릿)?
우리가 일반적인 웹 서비스를 만든다고 했을 때, 현대에는 거의 대부분의 웹 통신이 HTTP 요청과 응답으로 이루어 진다. 때문에 서버는 HTTP 요청 메세지를 받아 해석하고, 그에 맞는 HTTP 응답 메세지를 만들어 클라이언트에게 응답해줘야 한다. HTTP가 워낙 잘 만들어진 프로토콜이라 표준 문서를 보면 규약이 명확하지만, 일반 텍스트로 작성된 HTTP 메세지에서 규약들을 지켜가며 수 많은 헤더들을 일일이 직접 파싱하고, 그에 맞는 비즈니스 로직을 처리한 뒤 응답 메세지를 만드는게 결코 쉬운 일은 아닐 것이다.
다행히 Java에는 이런 HTTP 통신을 전담하는 프로그램이 있는데, 그게 Java Servlet이다.
서블릿(Servlet) : 자바에서 HTTP 통신에 관련된 기능을 담당하는 자바 클래스
우리가 자바를 사용해 웹 서비스를 만들고 싶다면 필수적으로 서블릿을 사용하게 된다. 여기서 주의해야 할 점이 있는데, 보통 스프링(Spring)을 많이 쓰기 때문에 서블릿이 스프링에 종속된 기술이라고 생각하는 사람이 있는데, 서블릿은 엄연하게 스프링 없이 동작할 수 있는 자바 프로그램이다.
서블릿은 서블릿 컨테이너(Servlet Container)에서 동작하며 대표적인 서블릿 컨테이너가 스프링 부트에 내장되어 있는 톰캣(Tomcat)이다.
서블릿 컨테이너(Servlet Contaier)
서블릿의 실행을 관리하는 환경을 제공하는 Java WAS(Web Application Server)의 일부로, 클라이언트의 요청을 받아 서블릿을 생성하고 실행하며, 그 결과를 클라이언트에게 돌려주는 역할을 한다. 즉, 서블릿은 어떤 클라이언트의 요청을 어떻게 처리한 뒤 응답할 것인가를 적어 놓은 명세서 같은 역할이고, 실제 서블릿 컨테이너는 이 명세서 대로 외부에서 일을 가져와 효율적으로 수행하는 역할을 한다.
서블릿 컨테이너의 주요 역할을 간단하게만 알아보면 다음과 같다.
- 서블릿 생명 주기 관리(뒤에서 설명)
- 서블릿의 생명주기는 서블릿 컨테이너가 시작할 때부터 종료될 때까지 관리
- 서블릿이 최초로 호출되면 컨테이너는 서블릿을 메모리에 로드하고,
init()
메서드를 호출하여 초기화 - 클라이언트의 요청이 들어오면 컨테이너는 서블릿의
service()
메서드를 호출하여 요청을 처리 - 서블릿이 더 이상 필요하지 않거나, 컨테이너가 종료될 때
destroy()
메서드를 호출하여 서블릿을 제거하고 자원을 해제
- 요청 및 응답 관리
- 서블릿 컨테이너는 HTTP 요청을 받아 url 패턴에 맞는 서블릿에 전달하고, 서블릿이 처리한 결과를 HTTP 응답으로 클라이언트에게 반환
- 멀티스레드 지원
- 서블릿 컨테이너는 각 클라이언트 요청마다 별도의 스레드를 생성하여 서블릿이 동시에 여러 요청을 처리할 수 있게 함
- 보안 관리
- 서블릿 컨테이너는 인증, 인가, 세션 관리와 같은 보안 기능을 제공
- JSP 및 기타 Java EE 컴포넌트 지원
- 서블릿 컨테이너는 서블릿뿐만 아니라 JSP(JavaServer Pages) 및 기타 Java EE 컴포넌트(EJB, JNDI 등)와도 연동
서블릿 클래스 생성
서블릿을 사용하는 대표적인 예시 코드를 직접 보자. (기타 스프링 부트 설정이나 Servlet 의존성 추가같은 부가적인 것은 생략됨)
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
먼저 위 import
문을 보면 자바의 표준 패키지인 jakarta
(구 javax)에서 가져오는 것을 확인할 수 있다. 우리가 서블릿 클래스를 활용하기 위해서는 필수적인 2가지 선행 작업이 필요하다.
@WebServlet
서블릿에 대한 메타 정보들을 간편하게 넣어줄 수 있는 어노테이션으로 아래 2가지 파라미터를 자주 쓴다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
String name() default ""; // 서블릿 이름. 관례로 클래스 이름에서 앞 알파벳만 소문자로 치환
String[] urlPatterns() default {}; // 클라이언트의 어떤 요청 url에 동작할 지 매핑
// ...
}
위 예시에서는 helloServlet
이름의 서블릿이고, 도메인/hello
이라는 주소로 요청이 오면 이 서블릿 클래스가 매핑되어 실행된다.
HttpServlet
HTTP 프로토콜을 기반으로 클라이언트의 요청을 처리하기 위해 사용되는 추상 클래스다. 이 클래스를 상속받아야 서블릿을 만들 수 있다. 아래 HttpServlet
클래스 다이어그램을 보면 GenericServlet
이라는 클래스를 상속받고 있으며, 다시 Servlet
클래스를 상속받는다.
Servlet
이라는 인터페이스에는 서블릿을 생성하고 서비스 로직을 수행 및 제거하는 서블릿 생명 주기(Servlet Lifecycle)에 관련된 메서드들이 정의되어 있기 때문에 사실상 본체라고 봐도 무방하다. 사실상 이 인터페이스를 구현한 구현체가 서블릿이 되는 것인데, HttpServlet
은 Servlet
인터페이스를 구현하면서, HTTP 요청을 처리할 때 사용되는 메서드를 미리 정의해 놓았기 때문에 주로 사용되는 것이다.
여기서 말하는 서블릿 생명 주기(Servlet Lifecycle)이란 서블릿 컨테이너가 관리하는 서블릿이 생성되고 소멸되기까지의 과정으로 크게 3개의 메서드를 단계적으로 호출한다.
init()
: 서블릿을 생성해 메모리에 적재할 때 한 번만 호출되며, 오버라이딩을 통해 기본 세팅을 해줄 수 있다.service()
: 클라이언트의 요청을 처리하고 응답하는 로직이 수행되는 메서드.doGet()
이나doPost()
메서드같은 메서드들이 오버라이딩 되어 구현돼 있으면 이를 호출하기도 한다.destroy()
: 서블릿을 메모리에서 제거할 때만 호출된다.
예시 코드에서는 HttpServlet
의 service(HttpServletRequest request, HttpServletResponse response)
라는 메서드를 오버라이드했는데, 이 메서드가 바로 위 서블릿 생명 주기에서 service()
가 수행되는 단계에서 호출된다고 생각하면 된다. 때문에 보통 클라이언트의 요청을 처리하고 응답하는 비즈니스 로직을 오버라이딩한 service()
에 구현한다.
💡 참고로 접근제어자가 protected인 service와 public인 service 2개의 메서드가 있는데, 구현 코드를 보면 public service에서 간단한 타입 캐스팅 후 protected service 메서드를 호출하기 때문에, 우리는 protected인 service에 서비스 로직을 구현하면 된다.
HttpServletRequest & HttpServletResponse
HttpServletRequest
와 HttpServletResponse
객체는 HttpServlet
을 사용하는 이유라고 해도 무방할 정도로 HTTP 요청을 편리하게 처리하고 응답할 수 있는 핵심 기능들을 가지고 있다.
HTTP 요청 메시지를 파싱한 뒤, 그 결과를 HttpServletRequest
객체에 담아서 제공한다. 따라서 HTTP 요청 메세지의 start-line, header, body에 대한 정보들을 메서드 호출로 간편하게 불러올 수 있다.
또한, HttpServletResponse
는 클라이언트에게 일반 텍스트, HTML, JSON 등 다양한 방법으로 응답할 수 있도록 편의 기능들을 제공하며, 메서드로 간편하게 헤더를 추가하면 그에 맞는 HTTP 응답 메세지를 만들어 준다.
💡두 객체는 기본적으로 HTTP 표준 스펙에 맞게 설계됐으며, 관련된 많은 기능들을 제공하고 있다. 따라서 깊이있는 이해를 하려면 HTTP 스펙이 제공하는 요청, 응답 메시지 자체를 이해해야 한다.
서블릿을 사용해 클라이언트 요청에 응답하기
간단한 예시를 들어 클라이언트가 HTTP 요청 메세지를 보내면, 이를 서블릿에서 처리한 뒤, 응답 메세지를 보내보자. 구구단을 출력해 주는 간단한 서비스로 시나리오는 다음과 같다.
- 클라이언트가 /gugudan 이라는 url로 HTTP 요청을 보냄. 이때 쿼리 파라미터로 몇 단을 받아보고 싶은지 전달할 수 있음
- ex) /gugudan?number=5
- url에 매핑된 서블릿에서 쿼리 파라미터를 파싱
- 반복문을 돌아 number에 맞는 구구단을 계산한 뒤, HTML로 응답
- 브라우저에서 HTTP 응답 메세지를 해석한 뒤, HTML을 클라이언트의 화면에 그려줌
package hello.servlet.gugudan;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(name = "gugudanServlet", urlPatterns = "/gugudan")
public class GugudanServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int number = Integer.parseInt(request.getParameter("number"));
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("""
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
""");
for (int i = 1; i <= 9; i++) {
w.write(" <li>" + number + " x " + i + " = " + number * i + "</li>\n");
}
w.write("""
</body>
</html>
""");
}
}
getParameter(String s)
: url 쿼리 파라미터에서 매개변수s
와 매핑되는 key의 value 값을 반환setContentType()
: 응답 메세지의 Content-Type 헤더 정보setCharacterEncoding()
: 응답 메세지의 인코딩 정보getWriter()
: 응답 메세지의 HTTP Body에 텍스트를 입력할 수 있는PrintWriter
객체 반환
코드를 보면, 먼저 number
라는 이름(key)의 쿼리 파라미터 값을 가져온다. 그 다음 HTML로 응답하기 위해 Content-Type 헤더 정보를 입력해 줬다. 위 코드에서 입력된 결과 다음과 같은 헤더가 추가될 것이다.
- Content-Type: text/html;charset=utf-8
이제 HTML 문서를 만들어 응답하면 되는데, 위 방식처럼 텍스트 형태로 HTML을 작성해 주면 된다. 이때 중간에 for
문으로 구구단을 계산해 동적으로 HTML 코드를 추가해 주는걸 볼 수 있다. 이처럼 서블릿을 사용하면 동적인 HTML을 생성할 수 있다는 엄청난 장점이 있다. 때문에 위키백과에서 서블릿을 다음과 같이 정의하기도 한다.
자바 서블릿(Java Servlet)은 자바를 사용하여 웹페이지를 동적으로 생성하는 서버측 프로그램 혹은 그 사양
이제 서버를 실행하고 테스트를 해보자.
쿼리 파라미터에 number
값으로 5를 입력해 요청한 결과, 정확하게 5단이 출력된 것을 확인할 수 있다. 리스트 형태로 출력된 것을 보아 HTML도 잘 그려줬다. 서블릿에서 생성한 HTTP 응답 메세지도 브라우저에서 확인해 보면 다음과 같다.
우리가 작성한 Content-Type이 올바르게 들어가 있다. 이 외의 헤더들은 편의상 톰캣에서 추가해 준 것들이다. Body 부분도 의도한 대로 HTML이 작성된 것을 확인할 수 있다.
이렇게 간편하게 HTTP 요청을 파싱하고, HTML도 동적으로 만들어 응답을 할 수 있는 서블릿이지만, 불편한 점이 있다. 눈치 챘겠지만, 바로 텍스트 형식의 HTML을 작성하는 부분이다. 딱 봐도 지저분해 보이고, 오타 나기 십상인데, 심지어 오타가 나더라도 찾아내기도 어렵다. 가장 큰 문제는 비즈니스 로직이 들어가야 할 자리에 화면을 처리하는 뷰 로직이 있다는 것이다. 이렇게 되면 유지보수에 큰 장애가 생길 수 있다. 이런 문제점들을 해결하기 위해 등장한 것이 템플릿 엔진 중 하나인 JSP다.
JSP(Java Server Page)의 등장
동적인 웹 페이지를 생성하기 위해 사용되는 서버 측 스크립팅 기술로, HTML 코드 안에 Java 코드를 포함하여 웹 페이지를 작성할 수 있다. JSP는 Java 서블릿의 확장으로서 개발되었으며, 서블릿의 복잡한 HTML 생성 코드를 간편하게 작성할 수 있도록 도와준다. JSP 파일은 .jsp
확장자를 사용하긴 하지만, 컴파일 시 서블릿으로 변환되기 때문에 Java Servlet 기술을 기반으로 작동한다.
💡 사실 스프링 부트를 사용하는 현재 시점에서 JSP는 경쟁력에서 밀려 거의 사장된 기술이다. 대신 스프링 부트에서 권고하는 뷰 템플릿 엔진은 Thymeleaf다.
위 예시 코드를 JSP로 옮겨보면 다음과 같다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
int number = Integer.parseInt(request.getParameter("number"));
%>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
for (int i = 1; i <= 9; i++) {
out.write(" <li>" + number + " x " + i + " = " + number * i + "</li>\n");
}
%>
</body>
</html>
코드를 보면 서블릿과 거의 유사하며, 응답 메세지 결과도 동일하다. 문법도 간단한데, JSP에서는 <% %>
안에 자바 코드를 그대로 작성할 수 있으며, <%@ %>
로 다른 클래스들을 import
도 할 수 있다. 이 외에도 편의를 위한 다양한 예약어가 있으니 찾아보면 금방 배울 수 있을 것이다.
JSP 역시 한계가 있다
서블릿과의 차이점은 자바 코드를 HTML로 옮겼다는 것이다. 때문에 Intellij같은 IDE 도움을 받으면 코드를 쉽게쉽게 작성할 수 있으며, 오타같은 것도 빠르게 찾을 수 있다. 다만, 아직 해결되지 않은 가장 큰 문제점이 있다. 바로 비즈니스 로직과 뷰 로직이 섞여있다는 점이다. 실제로 그 옛날 실무에서는 만 줄 단위의 JSP 파일들이 즐비했으며, 이를 유지보수하는 일은 결코 간단할 수 없었다고 한다.
“김영한”님의 말을 빌리자면 진짜 문제는 둘 사이에 변경의 라이프 사이클이 다르다는 점이라고 한다. 예를 들어서 UI를 일부 수정 하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다. 이렇게 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않다.
또한 JSP같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과 적이다.
그래서 이후에 등장한 것이 비즈니스 로직과 뷰 로직을 분리하는 MVC(Model-View-Controller) 패턴이다. 현대의 웹 서비스들은 대부분의 이 MVC 패턴을 따라서 개발된다. 물론 약간의 변형들이 있기는 하지만, 핵심 개념과 틀은 변하지 않았다.
Servlet과 JSP를 같이 쓰자 → MVC 패턴
MVC 패턴에서는 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것이다.
- 컨트롤러(Controller): HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
- 모델(Model): 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
- 뷰(View): 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
MVC 패턴에서 클라이언트의 요청이 처리되는 간단한 과정이다.
- 클라언트가 서버에 HTTP 요청을 보내면, url 패턴에 맞는 Controller가 호출
- Controller에서 비즈니스 로직을 수행한 뒤, 결과 데이터를 Model에 담음
- View에서 Model 데이터를 참조해 HTML을 생성하는 등의 로직을 수행한 뒤, 클라이언트에게 응답
💡 3번 과정에서는 실제로 JSP가 응답하는 것이 아닌, JSP가 서블릿 컨테이너에서 변환된 서블릿이 동적으로 HTML을 생성하고, 그 결과를 HTTP Body에 담아 클라이언트에게 응답하는 것이다. 이 과정에서 웹 서버를 앞 단에 두었다면 WAS가 웹 서버에게 서블릿이 수행한 결과를 전달하고, 웹 서버가 HTTP 응답 메세지를 클라이언트에게 전송할 것이다.
최초의 MVC 패턴은 위와 같았지만 실제로 사용되는 방식은 우리에게 익숙한 Service
와 Repository
계층이 추가된 형태로 조금 다르다.
Controller에서는 클라이언트의 HTTP 요청처리에 집중하고, 더 복잡한 로직 처리나 데이터 접근같은 역할을 Service와 Repository에 위임하는 형태다.
References
- 망나니 개발자님 블로그 - https://mangkyu.tistory.com/14
- 인프런 김영한님 강의 - https://www.inflearn.com/course/스프링-mvc-1/dashboard
'Spring&Spring Boot' 카테고리의 다른 글
[Spring] 코드로 분석해보는 Spring MVC 구조 이해(Front Controller 패턴, DispatchServet) (2) | 2024.11.08 |
---|---|
스프링이란? 좋은 객체 지향 설계란? (SOLID, 스프링 컨테이너, IoC, DI) (3) | 2024.09.05 |
[Spring Boot] 입문 - AOP란? AOP 적용해보기 (2) | 2024.09.03 |
[Spring Boot] Spring MVC CRUD를 위한 방명록 프로젝트 - 2 (0) | 2023.04.29 |
[Spring Boot] Spring MVC CRUD를 위한 방명록 프로젝트 - 1 (0) | 2023.04.27 |
- Total
- Today
- Yesterday
- Python Cookbook
- jsp
- 쉘 코드
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문
- 운영체제 반효경
- 생활코딩 javascript
- git branch
- 패킷 스위칭
- 김영환
- 파이썬 for Beginner 연습문제
- Gradle
- Computer_Networking_A_Top-Down_Approach
- 프로그래머스
- 선형 회귀
- spring mvc
- git
- JPA
- Thymeleaf
- 스프링 컨테이너
- 쉽게 배우는 운영체제
- 방명록 프로젝트
- 스프링 테스트
- 지옥에서 온 git
- Spring
- Spring Boot
- Spring Data JPA
- 스프링 mvc
- git merge
- 스프링
- 파이썬 for Beginner 솔루션
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |