본문 바로가기
개발일지/Spring

서블릿

by 꾸주니=^= 2025. 3. 21.
이 포스트는 인프런 사이트에 김영한 - 스프링 MVC 1편 강의를 보고 정리한 포스터입니다.

 

 

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

김영한 | , 원리를 알아야 핵심이 보인다!김영한의 스프링 MVC 기본편 👨‍💻 📌 수강 전 확인해주세요! 본 강의는 자바 스프링 완전 정복 시리즈의 네 번째 강의입니다. 우아한형제들 최연소

www.inflearn.com


서블릿은 톰캣 같은 웹 애플리케이션 서버를 직접 설치하고, 그 위에 서블릿 코드를 클래스 파일로 빌드해서 올린 다음, 톰캣 서버를 실행하면된다.

스프링 부트는 톰캣 서버를 내장하고 있으므로, 톰캣 서버 설치 없이 편리하게 서블릿 코드를 실행할 수 있다.

 


🔆 스프링 부트 서블릿 환경 구성

@ServletComponentScan

스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 @ServletComponentScan을 지원한다.

@ServletComponentScan //서블릿 자동 등록
@SpringBootApplication
public class ServletApplication {
	public static void main(String[] args) {
    	SpringApplication.run(ServletApplication.class, args);
    }
}

 

서블릿 등록하기

처음으로 실제 동작하는 서블릿 코드를 등록해보자

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
	@Override
    
    //HTTP요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 실행한다.
    protected void service(HttpServletRequest request, HttpServletResponse response)
    	throw 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().write("hello " + username);
    }
}

- @WebServlet : 서블릿 애노테이션
name : 서블릿 이름 / urlPatterns : URL 매핑

 

콘솔 실행결과

HelloServlet.service
request = org.apache.catalina.connector.RequestFacade@5e4e72
response = org.apache.catalina.connector.ResponseFacade@37d112b6
username = world

 


🔆 HTTP 요청 메시지 로그로 확인하기

설정 추가하기

application.properties

logging.level.org.apache.coyote.http11=debug

 


🔆 서블릿 컨테이너 동작 방식 설명

🌀 내장 톰캣 서버 생성

 

🌀 웹 애플리케이션 서버의 요청 응답 구조

 

참고
HTTP응답에서 Content-Length는 웹 애플리케이션 서버가 자동으로 생성해준다.

 


🔆 HttpServletRequest - 개요

🌀 HttpServletRequest 역할

HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 매우 불편할 것이다.
서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱한다.
그리고 그 결과를 HttpServletRequest 객체에 담아서 제공한다.

 

HttpServletRequest를 사용하면 다음과 같은 HTTP 요청 메시지를 편리하게 조회할 수 있다.

🎈 HTTP 요청 메시지

POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

username=kim&age=20

(1) START LINE
- HTTP 메소드
- URL
- 쿼리 스트링
- 스키마, 프로토콜

(2) 헤더
- 헤더 조회

(3) 바디
- form 파라미터 형식 조회
- message body 데이터 직접 조회

 

HttpServletRequest 객체는 추가로 여러가지 부가기능도 함께 제공한다.

(1) 임시 저장소 기능
해당 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능
- 저장 : request.setAttribute(name, value)
- 조회 : request.getAttribute(name)

(2) 세션 관리 기능
request.getSession(create: true)

 

중요
HttpServletRequest, HttpServletResponse를 사용할 때 가장 중요한 점은
이 객체들이 HTTP 요청 메시지, HTTP 응답 메시지를 편리하게 사용하도록 도와주는 객체라는 점이다.
따라서, 이 기능에 대해서 깊이있는 이해를 하려면 HTTP 스펙이 제공하는 요청, 응답 메시지 자체를 이해해야 한다.

 


🔆 HttpServletRequest - 기본 사용법

HttpServletRequest가 제공하는 기본 기능들을 알아보자.

🎈 hello.servlet.basic.request.RequestHeaderServlet

//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)
    	throw ServletException, IOException {
        	printStartLine(request);
            printHeaders(request);
            printHeaderUtils(request);
            printEtc(request);
            
            response.getWriter().wirte("ok");
     }
 }

 

🎈 start-line 정보

//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.getProtocol() = " + 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-header
	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();
}

 

결과

--- REQUEST-LINE - start ---
request.getMethod() = GET
request.getProtocol() = HTTP/1.1
request.getScheme() = http
request.getRequestURL() = http://localhost:8080/request-header
request.getRequestURI() = /request-header
request.getQueryString() = username=hello
request.isSecure() = false
--- REQUEST-LINE - end ---

 

🎈 헤더 정보

//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();
}

 

결과

--- Headers - start ---
host: localhost:8080
connection: keep-alive
cache-control: max-age=0
sec-ch-ua: "Chromium";v="88", "Google Chrome";v="88", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
upgrade-insecure-requests: 1user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_0) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/
webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site: none
sec-fetch-mode: navigate
sec-fetch-user: ?1
sec-fetch-dest: document
accept-encoding: gzip, deflate, br
accept-language: ko,en-US;q=0.9,en;q=0.8,ko-KR;q=0.7
--- Headers - end ---

 

🎈 헤더 편리한 조회

//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();
}

 

결과

--- Header 편의 조회 start ---
[Host 편의 조회]
request.getServerName() = localhost
request.getServerPort() = 8080

[Accept-Language 편의 조회]
locale = ko
locale = en_US
locale = en
locale = ko_KR
request.getLocale() = ko

[cookie 편의 조회]

[Content 편의 조회]
request.getContentType() = null
request.getContentLength() = -1
request.getCharacterEncoding() = UTF-8
--- Header 편의 조회 end ---

 

🎈 기타 정보

기타 정보는 HTTP 메시지의 정보는 아니다.

//기타 정보
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();
}

 

결과

--- 기타 조회 start ---
[Remote 정보]
request.getRemoteHost() = 0:0:0:0:0:0:0:1
request.getRemoteAddr() = 0:0:0:0:0:0:0:1
request.getRemotePort() = 54305

[Local 정보]
request.getLocalName() = localhost
request.getLocalAddr() = 0:0:0:0:0:0:0:1
request.getLocalPort() = 8080
--- 기타 조회 end ---

 


🔆 HTTP 요청 데이터 - 개요

HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법을 알아보자.

주로 3가지 방법을 사용한다.

1. GET - 쿼리 파라미터
- /url**?usrname=hello&age=20**
- 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
- 예) 검색, 필터, 페이징등에서 많이 사용하는 방식

2. POST - HTML Form
- content-type : application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
- 예) 회원가입, 상품주문, HTML Form 사용

3. HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용, JSON, XML, TEXT

4. 데이터 형식은 주로 JSON 사용
- POST, PUT, PATCH

 


🌀 HTTP 요청 데이터 - GET 쿼리 파라미터

다음 데이터를 클라이언트에서 서버로 전송해보자.

전달 데이터
- username=hello
- age=20

쿼리 파라미터는 URL에 다음과 같이 ?를 시작으로 보낼 수 있다. 추가 파라미터는 &로 구분하면 된다.

http://localhost:8080/request-param?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"); //복수 파라미터 조회

 


🌀 HTTP 요청 데이터 - POST HTML Form

이번에는 HTML의 Form을 사용해서 클라이언트에서 서버로 데이터를 전송해보자.
주로 회원가입, 상품 주문 등에서 사용하는 방식이다.

특징
- content-type : application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파라미터 형식으로 데이터를 전달한다. username=hello&age=20

POST의 HTML Form을 전송하면 웹 브라우저는 다음 형식으로 HTTP 메시지를 만든다.
(웹 브라우저 개발자 모드 확인)

- 요청 URL : http:/localhost:8080/request-param
- content-type : application/x-www-form-urlencoded
- message body : username=hello&age=20

`application/x-www-form-urlencoded` 형식은 앞서 GET에서 살펴본 쿼리 파라미터 형식과 같다.
따라서 파라미터 조회 메서드를 그대로 사용하면 된다.

클라이언트( 브라우저) 입장에서는 방식에 차이가 있지만, 서버 입장에서는 둘의 형식이 동일하므로,
`request.getParameter()` 편리하게 구분없이 조회할 있다.

정리하면 `request.getParameter()` GET URL 쿼리 파라미터 형식도 지원하고, POST HTML Form 형식도 지원한다.

 

참고
content-type은 HTTP 메시지 바디의 데이터 형식을 지정한다.
- GET URP 쿼리 파라미터 형식으로 클라이언트에서 서버로 데이터를 전달할 때는 HTTP 메시지 바디를 사용하지 않기 때문에 content-type이 없다.
- POST HTML Form 형식으로 데이터를 전달하면 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 바디에 포함된 데이터가 어떤 형식인지 content-type을 꼭 지정해야한다.

이렇게 폼으로 데이터를 전송하는 형식을 application/x-www-form-urlencoded라고 한다.

 


🌀 HTTP 요청 데이터 - API 메시지 바디 - 단순 텍스트

HTTP message body에 데이터를 직접 담아서 요청
 - HTTP API에서 주로 사용, JSON, XML, TEXT
 - 데이터 형식은 주로 JSON 사용
 - POST, PUT, PATCH


🌀 HTTP 요청 데이터 - API 메시지 바디 - JSON

JSON 형식 전송
 - POST http://localhost:8080/request-body-json
 - content-type : application/json
 - message body : {"username": "hello", "age":  20}
 - 결과 : messageBody = {"username": "hello", "age": 20}

 


🔆 HttpServletResponse - 기본 사용법

🌀 HttpServletResponse 역할

HTTP 응답 메시지 생성
 - HTTP 응답코드 지정
 - 헤더 생성
 - 바디 생성

편의 기능 제공
 - content-Type, 쿠키, Redirect

 

🌀 HttpServletResponse - 기본 사용법

hello.servlet.basic.response.ResponseHeaderServlet

@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, must-revalidate");
        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");
}

 


🔆 HTTP 응답 데이터 - 단순 텍스트,  HTML

HTTP 응답 메시지는 주로 다음 내용을 담아서 전달한다.
 - 단순 텍스트 응답
    : write.println("ok");
 - HTML 응답
 - HTTP API - MessageBody JSON 응답

 

🌀 HttpServletResponse - HTML 응답

hello.servlet.basic.response.ResponseHtmlServlet

@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>");
    }
}

HTTP 응답으로 HTML을 반환할 때는 content-type을 text/html로 지정해야 한다.

 


🔆 HTTP 응답 데이터 - API JSON

hello.servlet.basic.response.ResponseJsonServlet

@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 응답으로 JSON 반환할 때는 content-type `application/json` 지정해야 한다.
Jackson 라이브러리가 제공하는 `objectMapper.writeValueAsString()` 사용하면 객체를 JSON 문자로 변경할 있다.

 

참고

`application/json` 은 스펙상 utf-8 형식을 사용하도록 정의되어 있다.
그래서 스펙에서 charset=utf-8 과 같은 추가 파라미터를 지원하지 않는다.
따라서 `application/json` 이라고만 사용해야지 `application/json;charset=utf-8` 이라고 전달하는 것은 의미 없는 파라미터를 추가한 것이 된다.
response.getWriter()를 사용하면 추가 파라미터를 자동으로 추가해버린다.
이때는 response.getOutputStream()으로 출력하면 그런 문제가 없다.

 

'개발일지 > Spring' 카테고리의 다른 글

빈 스코프  (0) 2025.02.24
빈 생명주기 콜백  (0) 2025.02.20
의존관계 자동 주입 2편  (0) 2025.02.19
의존관계 자동 주입 1편  (0) 2025.02.19
컴포넌트 스캔  (0) 2025.02.18