Tomcat, Servlet (2)

Xylitol311·2026년 1월 9일

Back-end

목록 보기
4/14

앞서 Tomcat, Servlet 개념에 대해 공부하면서 여러 추가 질문이 생겼다.
너무 오랜만에 봐서 흐릿했던 개념들과 Spring Boot에서 실제 사용되는 내장 Tomcat, DispatcherServlet이 어떻게 동작하고 Servlet의 기본 동작흐름과는 어떻게 다른지 살펴보았다.

1. WAS란? 그리고 Tomcat과 Servlet의 관계

WAS (Web Application Server):

  • 동적 콘텐츠를 처리하는 서버
  • 비즈니스 로직 실행, DB 연결, 트랜잭션 처리 등을 담당
  • 예: Tomcat, JBoss, WebLogic, WebSphere

Tomcat ≠ Servlet

  • Tomcat: 서블릿을 실행하는 컨테이너(환경)
  • Servlet: 요청을 처리하는 자바 클래스(객체)

비유하자면:

Tomcat = 아파트 건물
Servlet = 아파트에 사는 세입자들

Tomcat이 서블릿들을 관리하고 실행시켜주는 환경을 제공하는 것


2. Servlet은 Java 전용인가?

맞다. Servlet은 Java 전용 기술이다.

  • Java EE(Jakarta EE) 표준 스펙의 일부
  • javax.servlet.Servlet 인터페이스를 구현한 자바 클래스
  • 다른 언어에서는 유사한 개념을 다른 이름으로 사용
    • Python: WSGI (Web Server Gateway Interface)
    • Ruby: Rack
    • Node.js: Express 미들웨어
    • PHP: 내장 request/response 처리

3. 전체 동작 흐름: Spring Boot 서버에서의 요청 처리

핵심 포인트:

  • Tomcat: 서블릿 컨테이너. HTTP 요청을 받고, 서블릿(DispatcherServlet)에게 전달
  • DispatcherServlet: Spring의 특별한 서블릿. 모든 요청의 진입점
  • Controller: 실제 비즈니스 로직 처리를 시작하는 지점
  • Service/Repository: 비즈니스 로직과 데이터 접근 계층

Servlet = 우리 서버?

정확히는:

  • Tomcat = 서버 (물리적 실행 환경)
  • DispatcherServlet = 요청 라우팅 담당
  • Controller + Service + Repository = 우리가 작성한 비즈니스 로직

4. Servlet vs DispatcherServlet

일반 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
        // 또 다른 서블릿...
    }
}

문제점:

  • URL마다 서블릿을 만들어야 함
  • 공통 로직(인증, 로깅 등) 중복
  • 코드 관리가 어려움

DispatcherServlet (Spring MVC 방식)

// 하나의 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() {
        // ...
    }
}

장점:

  • 하나의 DispatcherServlet이 모든 요청 처리
  • Controller는 비즈니스 로직에만 집중
  • 공통 기능은 Interceptor로 처리
  • 애노테이션 기반으로 간결함

관계도

핵심 차이:

구분일반 ServletDispatcherServlet
개수URL마다 여러 개하나만 존재
역할직접 요청 처리요청을 Controller로 위임
사용레거시, 간단한 웹앱Spring MVC (현대적)
패턴-Front Controller 패턴

5. HttpServletRequest와 HttpServletResponse

왜 필요한가?

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다.

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

HttpServletResponse의 역할

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는 "보낼 것을 쓰기" 위한 객체다.


6. doGet(), doPost() vs Controller의 @GetMapping, @PostMapping

doGet(), doPost()는 서블릿의 메서드

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의 @GetMapping, @PostMapping

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, @PostMappingController의 애노테이션
  • DispatcherServlet의 doGet()이 호출되고, 내부에서 적절한 Controller 메서드를 찾아 실행함

7. 변수의 종류와 차이점

public class Example {

    // 1. 전역 변수 (사실 Java에는 없음, 클래스 변수가 가장 비슷)
    public static int globalCount = 0;  // 클래스 변수 (static)

    // 2. 멤버 변수 (인스턴스 변수)
    private int memberCount = 0;

    public void method() {
        // 3. 지역 변수
        int localCount = 0;
    }
}

차이점

종류선언 위치생명주기공유 범위
전역 변수 (static)클래스 내부프로그램 실행~종료모든 인스턴스가 공유
멤버 변수클래스 내부객체 생성~소멸해당 인스턴스만
지역 변수메서드 내부메서드 실행~종료해당 메서드만

8. 서블릿 싱글톤과 동시성 문제

서블릿 인스턴스는 하나만 생성된다

웹 애플리케이션 (myapp.war)
  └─ UserServlet 인스턴스: 1개
  └─ ProductServlet 인스턴스: 1개
  └─ OrderServlet 인스턴스: 1개

"웹 앱당 하나"의 의미:

  • 한 서버에 하나의 서블릿만 존재하는 것이 아니라 서블릿 클래스마다 인스턴스가 하나씩 생성됨
  • UserServlet 100개가 아니라, UserServlet 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 값
1count 읽음 (0)0
2count + 1 = 1count 읽음 (0)0
3count에 1 저장count + 1 = 11
4count에 1 저장1 ← 문제 발생! 실제값은 2여야 하는데 1

결과: 두 번 증가했는데 count는 1! (Race Condition)

해결 방법

방법 1: 지역 변수 사용 (권장)

public class UserServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        int count = 0;  // ← 지역 변수 (각 스레드가 독립적으로 가짐)
        count++;
        resp.getWriter().write("Count: " + count);
    }
}

방법 2: Thread-safe 객체 사용

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

방법 3: synchronized 사용 (비권장 - 성능 저하)

public class UserServlet extends HttpServlet {
    private int count = 0;

    @Override
    protected synchronized void doGet(HttpServletRequest req, HttpServletResponse resp) {
        count++;  // ← 한 번에 한 스레드만 실행
        resp.getWriter().write("Count: " + count);
    }
}

9. Tomcat, Servlet, DispatcherServlet의 관계 정리

정리

  1. Servlet: 요청을 처리하는 자바 클래스의 스펙 (인터페이스)
  2. Tomcat: 서블릿을 실행하는 환경 (컨테이너)
  3. DispatcherServlet: Servlet 인터페이스를 구현한 특별한 서블릿 (Spring의 Front Controller)
  4. 일반 Servlet: Servlet 인터페이스를 구현한 일반 서블릿 (UserServlet, ProductServlet 등)

비유:

Servlet = 배우 (역할)
Tomcat = 극장 (무대를 제공)
DispatcherServlet = 주연 배우 (모든 씬에 등장, 다른 배우에게 역할 배분)
일반 Servlet = 조연 배우들 (각자 한 씬씩 담당)

Spring Boot에서의 역할

Spring Boot 애플리케이션
  ├─ 내장 Tomcat (서블릿 컨테이너)
  │   └─ DispatcherServlet (하나의 서블릿, 모든 요청 처리)
  │       └─ Controller들 (서블릿이 아님, 일반 클래스)
  │           ├─ UserController
  │           ├─ ProductController
  │           └─ OrderController
  └─ Service, Repository 등 (비즈니스 로직)

요약:

  • Tomcat과 Servlet은 같은 계층이 아니라 "실행 환경"과 "실행 대상"의 관계
  • DispatcherServlet은 Servlet의 구현체 중 하나
  • Spring Boot는 Tomcat(컨테이너) + DispatcherServlet(서블릿) + Controller(로직)의 조합
profile
문제에 도전하고 성장하는 백엔드 개발자입니다.

0개의 댓글