[SECTION 01. Java 웹의 출발점 — 서블릿으로 HTTP 직접 처리하기]
Name : servlet-basic
Location : 원하는 경로
Language : Java
Build System: Maven ← 꼭 Maven 선택
JDK : 17 이상 선택
프로젝트 루트의 pom.xml 파일을 아래처럼 작성합니다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>servlet-basic</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- war: 서블릿은 Tomcat 위에서 실행되므로 war로 패키징 -->
<packaging>war</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<!-- 의존성들 -->
<dependencies>
<!-- 서블릿 API: 코드 작성 시 필요, 실행은 Tomcat이 제공 -->
<!-- 하나의 의존성 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port>
<path>/</path>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
<scope>provided</scope>의 의미: 코드를 작성할 때만 필요하고, 실제 실행은 Tomcat이 이 라이브러리를 제공해 줍니다. 그래서 우리가 만드는 war 파일 안에는 포함되지 않습니다.
Maven 웹 프로젝트의 표준 구조를 직접 만들어야 합니다.
servlet-basic/
├── src/
│ └── main/
│ ├── java/ ← Java 코드
│ │ └── com.example/ ← 패키지
│ └── webapp/ ← 웹 관련 파일 디렉토리 (직접 생성!)
│ └── WEB-INF/
│ └── web.xml ← 서블릿 등록 설정 파일 (직접 생성!)
└── pom.xml
webapp 폴더 생성 방법:
src/main 우클릭 → New → Directorywebappwebapp 안에 WEB-INF 폴더 생성web.xml 파일 생성:
WEB-INF 폴더 안에 web.xml 파일을 만들고 아래 내용 작성:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="false">
</web-app>
IntelliJ 무료 버전은 Tomcat 플러그인을 직접 지원하지 않습니다. 대신 Maven 플러그인으로 실행합니다.
pom.xml의 </dependencies> 아래에 추가:
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port>
<path>/</path>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
</plugins>
</build>
실행 방법:
IntelliJ 우측 Maven 탭 → Plugins → tomcat7 → tomcat7:run 더블클릭
서블릿의 핵심은
HttpServlet클래스를 상속받는 것입니다.
내가 만든 서블릿
└── extends HttpServlet
└── extends GenericServlet
└── implements Servlet
src/main/java/com/example/ 아래에 HelloServlet.java 파일 생성:
package com.example;
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: 이 서블릿이 어떤 URL을 담당할지 지정
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
// GET 요청이 오면 이 메서드가 실행됨
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 쿼리 파라미터에서 name 값 꺼내기
String name = request.getParameter("name");
if (name == null) {
name = "이름 없음";
}
// 2. 응답 설정
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
// 3. 응답 본문 작성
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<body>");
out.println("<h1>안녕하세요, " + name + "님!</h1>");
out.println("</body>");
out.println("</html>");
}
}
| 코드 | 설명 |
|---|---|
@WebServlet("/hello") | /hello URL로 요청이 오면 이 서블릿이 처리 |
extends HttpServlet | 서블릿의 핵심 기능을 물려받음 |
doGet(...) | GET 요청 전용 처리 메서드 |
HttpServletRequest | 클라이언트가 보낸 요청 정보 전체 |
HttpServletResponse | 클라이언트에게 돌려줄 응답 |
request.getParameter("name") | URL의 ?name=값 부분을 꺼냄 |
response.getWriter() | 응답 본문을 쓸 수 있는 출력 스트림 |
Tomcat 실행 후 브라우저에서 접속:
http://localhost:8080/hello?name=김철수
예상 결과:
안녕하세요, 김철수님!
src/main/webapp/ 아래에 form.html 파일 생성:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
<h2>로그인</h2>
<form action="/login" method="post">
<label>아이디: <input type="text" name="username"></label><br><br>
<label>비밀번호: <input type="password" name="password"></label><br><br>
<button type="submit">로그인</button>
</form>
</body>
</html>
package com.example;
import javax.servlet.ServletException;
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.io.PrintWriter;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest rq, HttpServletResponse rs) throws ServletException, IOException {
rs.sendRedirect("/form.html");
}
@Override
protected void doPost(HttpServletRequest rq, HttpServletResponse rs) throws ServletException, IOException {
rq.setCharacterEncoding("utf-8");
rs.setCharacterEncoding("utf-8");
rs.setContentType("text/html");
String username = rq.getParameter("username");
String password = rq.getParameter("password");
PrintWriter pw = rs.getWriter();
if (username.equals("admin") && password.equals("1234")) {
pw.println("<body>");
pw.println("<h1>로그인이 완료되었습니다.</h1>");
pw.println("</body>");
}else {
pw.println("<body>");
pw.println("<h1>로그인이 실패하였습니다.</h1>");
pw.println("</body>");
}
}
}
| 항목 | GET | POST |
|---|---|---|
| 데이터 위치 | URL (쿼리스트링) | 요청 바디 |
| 보안 | URL에 노출됨 | URL에 노출 안 됨 |
| 용도 | 데이터 조회 | 데이터 전송/변경 |
| 예시 | 검색, 페이지 이동 | 로그인, 회원가입 |
| 즐겨찾기 | 가능 | 불가능 |
지금까지 @WebServlet 어노테이션으로 서블릿을 등록했습니다.
스프링 이전에는 이 모든 것을 web.xml에 직접 텍스트로 등록하기도 했습니다.
<web-app>
<!-- 서블릿 정의 -->
<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
</servlet>
<!-- URL 매핑 -->
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
<!-- 서블릿이 10개면 이 블록을 10번 반복해야 함... -->
</web-app>
포인트: 서블릿이 늘어날수록
web.xml도 함께 늘어납니다. URL 하나 추가할 때마다 최소 10줄의 XML을 작성해야 했습니다. 하지만 스프링을 사용하게 되면 @GetMapping 혹은 @PostMapping으로 간단하게 해결 가능합니다.
서블릿은 우리가 new로 직접 생성하지 않습니다. Tomcat(서블릿 컨테이너) 이 대신 생성하고 관리합니다.
[첫 번째 요청 도착]
↓
Tomcat: "이 URL 담당 서블릿 있나?" → HelloServlet 발견
↓
① init() — 서블릿 객체 생성 (딱 1번만)
↓
② service() — 요청 처리 (GET이면 doGet, POST면 doPost 호출)
↓
[응답 반환]
↓
[두 번째 요청 도착]
↓
② service() — 객체 재사용! init() 다시 호출 안 함
↓
[서버 종료]
↓
③ destroy() — 서블릿 객체 소멸 (딱 1번만)
@WebServlet("/lifecycle")
public class LifeCycleServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("init() 호출 — 서블릿 생성됨 (최초 1회)");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("doGet() 호출 — 요청 처리 중");
response.getWriter().println("요청 처리 완료!");
}
@Override
public void destroy() {
System.out.println("destroy() 호출 — 서블릿 소멸됨 (서버 종료 시)");
}
}
브라우저로 /lifecycle에 여러 번 접속하며 콘솔 출력을 확인해보세요.
중요: 서블릿 객체는 딱 1개만 생성됩니다 (싱글톤). 요청이 1000개 동시에 오면 서블릿 객체 1개가 1000개의 스레드를 처리합니다. 그래서 서블릿 안에 멤버 변수로 상태를 저장하면 절대 안 됩니다.
| 개념 | 핵심 내용 |
|---|---|
| HTTP | 요청(Request) / 응답(Response) 구조를 가진 통신 규약 |
| 서블릿 | Java에서 HTTP 요청을 처리하는 프로그램, HttpServlet을 상속 |
| doGet / doPost | GET/POST 요청을 처리하는 메서드 |
| web.xml | 서블릿을 URL에 매핑하는 XML 설정 파일 (구식 방법) |
| @WebServlet | web.xml 없이 어노테이션으로 URL 매핑 (개선된 방법) |
| 서블릿 생명주기 | (서버 실행x, 해당 서블릿에 처음 요청이 들어올 때)init → 비즈니스 로직(doGet/doPost) → (서버 종료 )destroy |
| 싱글톤 | 서블릿 객체는 1개만 생성, 멤버 변수에 상태 저장 금지 |