2025. 12. 18. 22:44ㆍSpring RoadMap/Spring MVC
서블릿
저번 시간에 웹 서버와 WAS의 차이, 그리고 서블릿의 개념에 대해 가볍게 훑어봤다. 이번 시간부터는 실제 코드로 서블릿이 어떻게 동작하는지, 그리고 스프링 부트 환경에서 서블릿을 어떻게 다루는지 자세히 알아본다.
1. 프로젝트 생성과 환경 설정
먼저 실습을 위한 프로젝트를 생성한다. 요즘은 대부분 Gradle을 사용하고, 자바 버전은 11을 기준으로 한다.
- Packaging: War를 선택한다. 보통 스프링 부트는 내장 톰캣을 써서 Jar를 많이 쓰지만, 서블릿과 JSP 등 레거시 기술부터 차근차근 배우기 위해 War를 선택한다.
- Jar vs War?
- Jar: JRE만 있으면 실행 가능 (스프링 부트 내장 톰캣 사용 시 주로 사용).
- War: 별도의 웹 서버(WAS)가 필요하거나, JSP 같은 기술을 쓸 때 사용.
- Jar vs War?
- Dependencies:
- Spring Web: 톰캣 내장 서버와 Spring MVC 기능을 포함한다.
- Lombok: 지루한 Getter, Setter 코드를 줄여주는 필수 라이브러리다.
2. 헬로 서블릿 (Hello Servlet)
스프링 부트에서 서블릿을 직접 등록해서 써보자.
서블릿 등록하기
가장 먼저 메인 애플리케이션 클래스에 @ServletComponentScan을 붙여야 한다. 그래야 스프링이 "아, 내 하위 패키지에 있는 서블릿들을 찾아서 등록해야겠구나"라고 인식한다.
ServletApplication.java
package org.example.springmvcpractice.servlet;
import org.example.springmvcpractice.SpringMvcPracticeApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.server.servlet.context.ServletComponentScan;
@ServletComponentScan // 서블릿 자동 등록 -> 스프링은 기본적으로 서블릿 클래스를 자동 인식 X, 이때 이 어노테이션을 사용하면 현재 패키지 및 하위 패키지를 훑고 @WebServlet이 붙은 클래스를 찾아 서블릿 컨테이너(내장 톰캣)에 자동 등록
@SpringBootApplication
public class ServletApplication {
public static void main(String[] args) {
SpringApplication.run(SpringMvcPracticeApplication.class, args);
}
}
이제 실제 서블릿 코드를 작성해보자.
package org.example.springmvcpractice.servlet.basic;
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 {
// HTTP 요청이 들어오면 서블릿 컨테이너에 의해 실행 되는 부분,
// request 객체에서 데이터를 꺼내고(파라미터 조회), response 객체에 응답 데이터를 담음
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("HelloServlet.service");
System.out.println("request = " + request);
System.out.println("response = " + response);
String username = request.getParameter("username");
System.out.println("username = " + username);
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
response.getWriter().println("Hello " + username);
}
}
- @WebServlet: 서블릿의 이름과 매핑할 URL을 지정한다. /hello로 요청이 오면 이 클래스가 실행된다.
- service(): 서블릿이 호출되면 실행되는 메서드다. 여기서 요청(Request)을 처리하고 응답(Response)을 만든다.
서블릿 컨테이너 동작 방식
브라우저에서 localhost:8080/hello?username=world라고 요청을 보내면 무슨 일이 일어날까?

1. 스프링 부트는 내장 톰캣 서버를 실행한다
2. WAS(톰캣)은 서블릿 컨테이너에서 helloServlet을 생성한다

이렇게 되면?

- WAS(톰캣)가 HTTP 요청 메세지를 받는다.
- 이 요청 내용을 바탕으로 HttpServletRequest와 HttpServletResponse 객체를 새로 만든다.
- 우리가 만든 HelloServlet.service() 메서드를 실행하면서 위 두 객체를 파라미터로 넘겨준다.
- 우리는 Response 객체에 응답 내용을 꾹꾹 담는다.
- 메서드가 끝나면 WAS가 Response 객체의 내용을 바탕으로 HTTP 응답 메세지를 만들어 브라우저에게 보낸다.


3. HttpServletRequest - 요청 정보라니?
개발자가 HTTP 요청 메세지(Start Line, Header, Body 등)를 직접 파싱해서 쓰려면 머리가 아플 것이다. HttpServletRequest는 이 귀찮은 파싱 작업을 대신 해주고, 그 결과를 HttpServletRequest라는 객체에 예쁘게 담아서 제공해준다.
HTTP 요청 메시지 양식
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
username=MELT&age=30
- START LINE (POST)
- HTTP 메소드
- URL
- 쿼리 스트링
- 스키마, 프로토콜
- 헤더 (Host, Content-Type)
- 헤더 조회
- 바디 (username=MELT&age=30)
- form 파라미터 형식 조회
- message body 데이터 직접 조회
HttpServletRequest 객체는 여러 가지 부가기능도 함께 제공한다.
임시 저장소 기능
- 해당 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능
- 저장 : request.setAttribute(name, value)
- 조회 : request.getAttribute(name)
세션 관리 기능
- request.getSession(create: true)
기본적인 HttpServletRequest
package org.example.springmvcpractice.servlet.basic.request;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
//http://localhost:8080/request-header?username=hello
@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
printStartLine(request);
printHeaders(request);
printHeaderUtils(request);
printEtc(request);
response.getWriter().write("ok");
}
//start line 정보
private void printStartLine(HttpServletRequest request) {
System.out.println("--- REQUEST-LINE - start ---");
System.out.println("request.getMethod() = " + request.getMethod()); //GET
System.out.println("request.getProtocal() = " + request.getProtocol()); // HTTP/1.1
System.out.println("request.getScheme() = " + request.getScheme()); //http
// http://localhost:8080/request-header
System.out.println("request.getRequestURL() = " + request.getRequestURL());
// /request-test
System.out.println("request.getRequestURI() = " + request.getRequestURI());
//username=hi
System.out.println("request.getQueryString() = " + request.getQueryString());
System.out.println("request.isSecure() = " + request.isSecure()); //https 사용 유무
System.out.println("--- REQUEST-LINE - end ---");
System.out.println();
}
//Header 모든 정보
private void printHeaders(HttpServletRequest request) {
System.out.println("--- Headers - start ---");
/*
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println(headerName + ": " + request.getHeader(headerName));
}
*/
request.getHeaderNames().asIterator().forEachRemaining(headerName -> System.out.println(headerName + ":" + request.getHeader(headerName)));
System.out.println("--- Headers - end ---");
System.out.println();
}
//Header 편리한 조회
private void printHeaderUtils(HttpServletRequest request) {
System.out.println("--- Header 편의 조회 start ---");
System.out.println("[Host 편의 조회]");
System.out.println("request.getServerName() = " + request.getServerName()); //Host 헤더
System.out.println("request.getServerPort() = " + request.getServerPort()); //Host 헤더
System.out.println();
System.out.println("[Accept-Language 편의 조회]");
request.getLocales().asIterator()
.forEachRemaining(locale -> System.out.println("locale = " + locale));
System.out.println("request.getLocale() = " + request.getLocale());
System.out.println();
System.out.println("[cookie 편의 조회]");
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
System.out.println(cookie.getName() + ": " + cookie.getValue());
}
}
System.out.println();
System.out.println("[Content 편의 조회]");
System.out.println("request.getContentType() = " + request.getContentType());
System.out.println("request.getContentLength() = " + request.getContentLength());
System.out.println("request.getCharacterEncoding() = " + request.getCharacterEncoding());
System.out.println("--- Header 편의 조회 end ---");
System.out.println();
}
//기타 정보
private void printEtc(HttpServletRequest request) {
System.out.println("--- 기타 조회 start ---");
System.out.println("[Remote 정보]");
System.out.println("request.getRemoteHost() = " + request.getRemoteHost()); //
System.out.println("request.getRemoteAddr() = " + request.getRemoteAddr()); //
System.out.println("request.getRemotePort() = " + request.getRemotePort()); //
System.out.println();
System.out.println("[Local 정보]");
System.out.println("request.getLocalName() = " + request.getLocalName()); //
System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); //
System.out.println("request.getLocalPort() = " + request.getLocalPort()); //\
System.out.println("--- 기타 조회 end ---");
System.out.println();
}
}
요청 데이터를 읽는 3가지 방법
클라이언트에서 서버로 데이터를 보내는 방법은 크게 3가지가 있다. 서블릿은 이걸 어떻게 읽을까?
1) GET - 쿼리 파라미터
URL 뒤에 ?username=hello&age=20 처럼 데이터를 붙여서 보내는 방식이다. 검색이나 필터링에 주로 쓴다.
String username = request.getParameter("username"); //단일 파라미터 조회
Enumeration<String> parameterNames = request.getParameterNames(); //파라미터 이름들 모두 조회
Map<String, String[]> parameterMap = request.getParameterMap(); //파라미터를 Map으로 조회
String[] usernames = request.getParameterValues("username"); //복수 파라미터 조회
RequestParamServlet
package org.example.springmvcpractice.servlet.basic.request;
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;
/**
* 1. 파라미터 전송 기능
* http://localhost:8080/request-param?username=hello&age=20
* <p>
* 2. 동일한 파라미터 전송 가능
* http://localhost:8080/request-param?username=hello&username=kim&age=20
*/
@WebServlet(name = "requestParamServlet", urlPatterns = "/request-param")
public class RequestParamServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse
resp) throws ServletException, IOException {
System.out.println("[전체 파라미터 조회] - start");
/*
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
System.out.println(paramName + "=" +
request.getParameter(paramName));
}
*/
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> System.out.println(paramName +
"=" + request.getParameter(paramName)));
System.out.println("[전체 파라미터 조회] - end");
System.out.println();
System.out.println("[단일 파라미터 조회]");
String username = request.getParameter("username");
System.out.println("request.getParameter(username) = " + username);
String age = request.getParameter("age");
System.out.println("request.getParameter(age) = " + age);
System.out.println();
System.out.println("[이름이 같은 복수 파라미터 조회]");
System.out.println("request.getParameterValues(username)");
String[] usernames = request.getParameterValues("username");
for (String name : usernames) {
System.out.println("username=" + name);
}
resp.getWriter().write("ok");
}
}
만약 여기서
http://localhost:8080/request-param?username=hello 요청을 보내면,
[전체 파라미터 조회] - start
username=hello
[전체 파라미터 조회] - end
[단일 파라미터 조회]
request.getParameter(username) = hello
request.getParameter(age) = null
[이름이 같은 복수 파라미터 조회]
request.getParameterValues(username)
username=hello
위와 같은 실행 결과가 나온다.
2) POST - HTML Form
회원 가입 폼 같은 곳에서 데이터를 입력하고 전송할 때 쓴다. Content-Type이 application/x-www-form-urlencoded인데, 메시지 바디에 쿼리 파라미터 형식(username=hello&age=20)으로 데이터가 들어온다.
- 중요: 형식이 GET 쿼리 파라미터와 똑같기 때문에, 서블릿에서는 request.getParameter()로 GET과 POST Form 데이터를 구분 없이 조회할 수 있다. (이게 진짜 편하다!)
RequestBodyStringServlet
package org.example.springmvcpractice.servlet.basic.request;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@WebServlet(name = "requestBodyStringServlet", urlPatterns = "/request-bodystring")
public class RequestBodyStringServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
response.getWriter().write("ok");
}
}
3) HTTP Message Body (JSON)
요즘 API 통신에서 가장 많이 쓰는 방식이다. 주로 JSON 데이터를 담아서 보낸다. 이건 getParameter로 못 읽고, InputStream으로 직접 읽어야 한다. 하지만 스프링은 더 편한 방법을 제공한다.
위 코드인 RequestBodyStringServlet의 결과로 문자 형식의 결과가 나오는데,
여기서 JSON 형식의 파싱을 추가해보면
HelloData
package hello.servlet.basic;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class HelloData {
private String username;
private int age;
}
RequestBodyJsonServlet
package hello.servlet.basic.request;
import com.fasterxml.jackson.databind.ObjectMapper;
import hello.servlet.basic.HelloData;
import org.springframework.util.StreamUtils;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
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.nio.charset.StandardCharsets;
/**
* http://localhost:8080/request-body-json
*
* JSON 형식 전송
* content-type: application/json
* message body: {"username": "hello", "age": 20}
*
*/
@WebServlet(name = "requestBodyJsonServlet", urlPatterns = "/request-bodyjson")
public class RequestBodyJsonServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("messageBody = " + messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
System.out.println("helloData.username = " + helloData.getUsername());
System.out.println("helloData.age = " + helloData.getAge());
response.getWriter().write("ok");
}
}
이렇게 하면 결과가
messageBody={"username": "hello", "age": 20}
data.username=hello
data.age=20
JSON으로 파싱된다.
참고로, JSON을 파싱해 사용할 수 있는 자바 객체로 변환할 땐 Jackson, Gson 과 같은 JSON 변환 라이브러리를 사용해야 한다. 스프링 부트로 Spring MVC를 사용하면 기본으로 Jackson 라이브러리를 제공한다.
// JSON 데이터를 객체로 변환하기
private ObjectMapper objectMapper = new ObjectMapper(); // Jackson 라이브러리
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) ... {
// 1. 바디의 데이터를 읽는다 (String)
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
// 2. JSON을 자바 객체로 변환한다
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
System.out.println("username = " + helloData.getUsername());
}
스프링 부트는 기본적으로 Jackson이라는 라이브러리(ObjectMapper)를 제공해서 JSON을 자바 객체로 아주 쉽게 바꿀 수 있다.
4. HttpServletResponse - 응답 만들기
요청을 처리했으면 응답을 보내야 한다. HttpServletResponse 객체에 상태 코드, 헤더, 바디 정보를 입력하면 WAS가 알아서 HTTP 응답 메세지를 만들어준다.
우선 기본적인 ResponseHeaderServlet 사용 코드를 보자
ResponseHeaderServlet
package org.example.springmvcpractice.servlet.basic.response;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* http://localhost:8080/response-header
*
*/
@WebServlet(name = "responseHeaderServlet", urlPatterns = "/response-header")
public class ResponseHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
//[status-line]
response.setStatus(HttpServletResponse.SC_OK); //200
//[response-headers]
response.setHeader("Content-Type", "text/plain;charset=utf-8");
response.setHeader("Cache-Control", "no-cache, no-store, mustrevalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("my-header","hello");
//[Header 편의 메서드]
content(response);
cookie(response);
redirect(response);
//[message body]
PrintWriter writer = response.getWriter();
writer.println("ok");
}
// Content 편의 매서드
private void content(HttpServletResponse response) {
//Content-Type: text/plain;charset=utf-8
//Content-Length: 2
//response.setHeader("Content-Type", "text/plain;charset=utf-8");
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
//response.setContentLength(2); //(생략시 자동 생성)
}
// 쿠키 편의 메서드
private void cookie(HttpServletResponse response) {
//Set-Cookie: myCookie=good; Max-Age=600;
//response.setHeader("Set-Cookie", "myCookie=good; Max-Age=600");
Cookie cookie = new Cookie("myCookie", "good");
cookie.setMaxAge(600); //600초
response.addCookie(cookie);
}
// redirect 편의 메서드
private void redirect(HttpServletResponse response) throws IOException {
//Status Code 302
//Location: /basic/hello-form.html
//response.setStatus(HttpServletResponse.SC_FOUND); //302
//response.setHeader("Location", "/basic/hello-form.html");
response.sendRedirect("/basic/hello-form.html");
}
}
다양한 응답 데이터 보내기
1) 단순 텍스트
response.getWriter().write("ok");
2) HTML 응답
서블릿으로 HTML을 직접 짜는 건 좀 고통스럽다. (나중에 JSP나 타임리프를 쓰면 편해진다) Content-Type을 text/html로 지정해야 브라우저가 HTML로 인식한다.
ResponseHtmlServlet
package org.example.springmvcpractice.servlet.basic.response;
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 = "responseHtmlServlet", urlPatterns = "/response-html")
public class ResponseHtmlServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
//Content-Type: text/html;charset=utf-8
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<body>");
writer.println(" <div>안녕?</div>");
writer.println("</body>");
writer.println("</html>");
}
}
3) JSON 응답 (API)
Content-Type을 application/json으로 지정해야 한다. 객체를 JSON 문자로 바꿔서 내보내면 된다.
package org.example.springmvcpractice.servlet.basic.response;
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 org.example.springmvcpractice.servlet.basic.HelloData;
import tools.jackson.databind.ObjectMapper;
import java.io.IOException;
/**
* http://localhost:8080/response-json
*
*/
@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
public class ResponseJsonServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
//Content-Type: application/json
response.setHeader("content-type", "application/json");
response.setCharacterEncoding("utf-8");
HelloData data = new HelloData();
data.setUsername("kim");
data.setAge(20);
//{"username":"kim","age":20}
String result = objectMapper.writeValueAsString(data);
response.getWriter().write(result);
}
}
마무리
서블릿 덕분에 우리는 HTTP 패킷을 직접 뜯어보지 않고도 편리하게 웹 개발을 할 수 있게 되었다.
- HttpServletRequest: 요청 정보를 편리하게 꺼내 쓴다. (getParameter()가 효자다)
- HttpServletResponse: 응답 정보를 편리하게 채워 넣는다.
하지만 서블릿만으로는 아직 HTML을 동적으로 만드는 게 너무 불편하고, JSON 변환 코드도 매번 작성해야 해서 번거롭다. 스프링 MVC는 이걸 어떻게 더 편하게 만들어줄까? 다음 시간에 계속 알아보자.
요약: 서블릿
- 서블릿 컨테이너(WAS): HTTP 요청이 오면 Request, Response 객체를 만들어서 서블릿의 service() 메서드를 실행해준다.
- HttpServletRequest: HTTP 요청 메세지를 개발자가 파싱할 필요 없이 편하게 조회할 수 있게 해주는 객체다.
- 데이터 조회: GET 쿼리 파라미터와 POST HTML Form 방식은 둘 다 request.getParameter()로 편리하게 읽을 수 있다.
- JSON 처리: API 통신에서 주로 쓰는 JSON 데이터는 ObjectMapper (Jackson 라이브러리)를 사용해 자바 객체로 변환해서 사용한다.
- HttpServletResponse: 응답 헤더, 상태 코드, 바디 데이터를 담는 객체다. JSON 응답을 보낼 때도 ObjectMapper를 사용해 객체를 문자열로 바꿔서 보낸다.
'Spring RoadMap > Spring MVC' 카테고리의 다른 글
| 스프링 MVC 1편 (6), 스프링 MVC 기본 기능 (1) | 2025.12.21 |
|---|---|
| 스프링 MVC 1편 (5), 스프링 MVC 구조 이해 (0) | 2025.12.20 |
| 스프링 MVC 1편 (4), MVC 프레임워크 만들기 (0) | 2025.12.20 |
| 스프링 MVC 1편 (3), 서블릿 · JSP · MVC 패턴 (0) | 2025.12.19 |
| 스프링 MVC 1편 (1 - 시작), 웹 애플리케이션 이해 (0) | 2025.12.15 |