앞서 Tomcat, Servlet 개념에 대해 공부하면서 여러 추가 질문이 생겼다.
너무 오랜만에 봐서 흐릿했던 개념들과 Spring Boot에서 실제 사용되는 내장 Tomcat, DispatcherServlet이 어떻게 동작하고 Servlet의 기본 동작흐름과는 어떻게 다른지 살펴보았다.
WAS (Web Application Server):
Tomcat ≠ Servlet
비유하자면:
Tomcat = 아파트 건물
Servlet = 아파트에 사는 세입자들
Tomcat이 서블릿들을 관리하고 실행시켜주는 환경을 제공하는 것
맞다. Servlet은 Java 전용 기술이다.
javax.servlet.Servlet 인터페이스를 구현한 자바 클래스
핵심 포인트:
Servlet = 우리 서버?
정확히는:
@WebServlet("/users")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// URL: /users
String userId = req.getParameter("id");
User user = userService.findById(userId);
// 직접 JSON 변환, 응답 작성
resp.setContentType("application/json");
resp.getWriter().write(objectMapper.writeValueAsString(user));
}
}
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// URL: /products
// 또 다른 서블릿...
}
}
문제점:
// 하나의 DispatcherServlet이 모든 요청을 받음
@Controller
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.save(user);
return ResponseEntity.ok(created);
}
}
@Controller
public class ProductController {
@GetMapping("/products")
public ResponseEntity<List<Product>> getProducts() {
// ...
}
}
장점:
핵심 차이:
| 구분 | 일반 Servlet | DispatcherServlet |
|---|---|---|
| 개수 | URL마다 여러 개 | 하나만 존재 |
| 역할 | 직접 요청 처리 | 요청을 Controller로 위임 |
| 사용 | 레거시, 간단한 웹앱 | Spring MVC (현대적) |
| 패턴 | - | Front Controller 패턴 |
HTTP는 텍스트 기반 프로토콜이다. 서버가 받는 실제 요청은 이런 형태다:
GET /users/1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Content-Type: application/json
Cookie: sessionId=abc123
{"name": "John"}
이 텍스트를 파싱해서 사용하기 편한 객체로 변환한 것이 HttpServletRequest다.
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// URL 파라미터 읽기
String id = req.getParameter("id"); // ?id=123
// Path Variable 읽기
String path = req.getPathInfo(); // /users/123
// 헤더 읽기
String userAgent = req.getHeader("User-Agent");
String contentType = req.getHeader("Content-Type");
// 쿠키 읽기
Cookie[] cookies = req.getCookies();
// 세션 가져오기
HttpSession session = req.getSession();
// Body 읽기
BufferedReader reader = req.getReader();
String body = reader.lines().collect(Collectors.joining());
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 상태 코드 설정
resp.setStatus(200);
// 헤더 설정
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.setHeader("Cache-Control", "no-cache");
// 쿠키 추가
Cookie cookie = new Cookie("userId", "123");
resp.addCookie(cookie);
// Body 작성
PrintWriter writer = resp.getWriter();
writer.write("{\"name\":\"John\"}");
}
서버가 실제로 클라이언트에게 보내는 것:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache
Set-Cookie: userId=123
{"name":"John"}
즉, Request는 "받은 것을 읽기", Response는 "보낼 것을 쓰기" 위한 객체다.
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// GET 요청 처리
System.out.println("GET 요청 처리 중...");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
// POST 요청 처리
System.out.println("POST 요청 처리 중...");
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) {
// PUT 요청 처리
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) {
// DELETE 요청 처리
}
}
service() 메서드 내부 동작 (간소화):
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod(); // "GET", "POST", "PUT", "DELETE" 등
if (method.equals("GET")) {
doGet(req, resp);
} else if (method.equals("POST")) {
doPost(req, resp);
} else if (method.equals("PUT")) {
doPut(req, resp);
} else if (method.equals("DELETE")) {
doDelete(req, resp);
}
}
Controller는 서블릿이 아니다! 단지 DispatcherServlet이 호출하는 일반 클래스다.
@RestController
public class UserController {
@GetMapping("/users/{id}") // ← 애노테이션일 뿐, doGet()과는 무관
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping("/users") // ← 애노테이션일 뿐, doPost()와는 무관
public User createUser(@RequestBody User user) {
return userService.save(user);
}
}
Client → Tomcat → DispatcherServlet.service()
↓
HTTP 메서드 확인 (GET)
↓
DispatcherServlet.doGet()
↓
HandlerMapping으로 Controller 찾기
↓
@GetMapping("/users/{id}") 메서드 실행
↓
UserController.getUser() 호출
핵심:
doGet(), doPost()는 서블릿의 메서드@GetMapping, @PostMapping은 Controller의 애노테이션doGet()이 호출되고, 내부에서 적절한 Controller 메서드를 찾아 실행함public class Example {
// 1. 전역 변수 (사실 Java에는 없음, 클래스 변수가 가장 비슷)
public static int globalCount = 0; // 클래스 변수 (static)
// 2. 멤버 변수 (인스턴스 변수)
private int memberCount = 0;
public void method() {
// 3. 지역 변수
int localCount = 0;
}
}
| 종류 | 선언 위치 | 생명주기 | 공유 범위 |
|---|---|---|---|
| 전역 변수 (static) | 클래스 내부 | 프로그램 실행~종료 | 모든 인스턴스가 공유 |
| 멤버 변수 | 클래스 내부 | 객체 생성~소멸 | 해당 인스턴스만 |
| 지역 변수 | 메서드 내부 | 메서드 실행~종료 | 해당 메서드만 |
웹 애플리케이션 (myapp.war)
└─ UserServlet 인스턴스: 1개
└─ ProductServlet 인스턴스: 1개
└─ OrderServlet 인스턴스: 1개
"웹 앱당 하나"의 의미:
public class UserServlet extends HttpServlet {
private int count = 0; // ← 멤버 변수 (모든 스레드가 공유)
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
count++; // ← 동시에 여러 스레드가 접근!
resp.getWriter().write("Count: " + count);
}
}
동시 요청 시나리오:
| 시간 | Thread 1 (사용자 A) | Thread 2 (사용자 B) | count 값 |
|---|---|---|---|
| 1 | count 읽음 (0) | 0 | |
| 2 | count + 1 = 1 | count 읽음 (0) | 0 |
| 3 | count에 1 저장 | count + 1 = 1 | 1 |
| 4 | count에 1 저장 | 1 ← 문제 발생! 실제값은 2여야 하는데 1 |
결과: 두 번 증가했는데 count는 1! (Race Condition)
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
int count = 0; // ← 지역 변수 (각 스레드가 독립적으로 가짐)
count++;
resp.getWriter().write("Count: " + count);
}
}
public class UserServlet extends HttpServlet {
private AtomicInteger count = new AtomicInteger(0); // ← 원자적 연산 보장
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
int current = count.incrementAndGet();
resp.getWriter().write("Count: " + current);
}
}
public class UserServlet extends HttpServlet {
private int count = 0;
@Override
protected synchronized void doGet(HttpServletRequest req, HttpServletResponse resp) {
count++; // ← 한 번에 한 스레드만 실행
resp.getWriter().write("Count: " + count);
}
}
비유:
Servlet = 배우 (역할)
Tomcat = 극장 (무대를 제공)
DispatcherServlet = 주연 배우 (모든 씬에 등장, 다른 배우에게 역할 배분)
일반 Servlet = 조연 배우들 (각자 한 씬씩 담당)
Spring Boot 애플리케이션
├─ 내장 Tomcat (서블릿 컨테이너)
│ └─ DispatcherServlet (하나의 서블릿, 모든 요청 처리)
│ └─ Controller들 (서블릿이 아님, 일반 클래스)
│ ├─ UserController
│ ├─ ProductController
│ └─ OrderController
└─ Service, Repository 등 (비즈니스 로직)
요약: