기본 콘텐츠로 건너뛰기

톰캣이 하는 일: 서블릿 컨테이너부터 요청 처리 흐름까지 이해하기

톰캣이 하는 일: 서블릿 컨테이너부터 요청 처리 흐름까지 이해하기

빠른 답

  • 톰캣은 HTTP 요청을 받아 서블릿 코드로 연결하고 응답까지 돌려주는 자바 웹 실행 환경이다.
  • 핵심 흐름은 요청 수신, URL 매핑, 필터 실행, 서블릿 호출, 응답 반환 순서로 이해하면 된다.
  • 서블릿은 매 요청마다 새로 만들어지는 것이 아니라 컨테이너가 생명주기를 관리한다.
  • 실무에서는 포트 설정, 스레드 풀, 배포 경로, 로그 확인 포인트를 함께 알아야 운영이 쉬워진다.

흐름으로 보기

톰캣이 하는 일: 서블릿 컨테이너부터 요청 처리 흐름까지 이해하기 흐름 다이어그램

톰캣을 이해할 때는 이 다섯 단계를 먼저 잡는 것이 가장 좋습니다. 브라우저나 API 클라이언트가 보낸 요청은 톰캣의 포트로 들어오고, 톰캣은 어떤 애플리케이션과 어떤 서블릿이 이 요청을 처리해야 하는지 찾습니다. 그다음 필터 같은 공통 처리 단계를 거쳐 실제 자바 코드가 실행되고, 최종 결과가 다시 HTTP 응답으로 나갑니다.

스프링 부트를 쓰더라도 이 흐름이 사라지는 것은 아닙니다. 눈앞에 보이는 코드가 @RestController로 바뀔 뿐, 아래쪽에서는 여전히 톰캣이 요청을 받고 서블릿 체인으로 넘겨주는 구조가 유지됩니다.

톰캣은 무엇이고 왜 자바 웹 개발에서 자주 등장할까

톰캣은 흔히 WAS라고 부르지만, 실제로는 HTTP 서버 기능과 서블릿 컨테이너를 함께 제공하는 자바 웹 실행 환경이라고 보는 편이 더 정확합니다. 핵심 역할은 단순합니다. HTTP 요청을 읽고, 그 요청을 처리할 자바 웹 컴포넌트를 찾아 실행한 뒤, 결과를 다시 HTTP 응답으로 포장해 돌려보냅니다.

정적 파일만 내려주는 서버와 비교하면 차이가 분명해집니다. /logo.png 같은 요청은 파일 응답으로 끝날 수 있지만, /users, /orders, /login 같은 요청은 서버 쪽 코드 실행이 필요합니다. 톰캣은 이런 동적 요청을 서블릿이나 프레임워크 진입점으로 연결해 주는 역할을 맡습니다.

자바 웹 개발에서 톰캣이 자주 등장하는 이유도 여기 있습니다.

  • 서블릿과 JSP 실행 기반을 제공합니다.
  • 표준 자바 웹 구조와 잘 맞습니다.
  • 스프링 MVC, 스프링 부트 같은 프레임워크가 이 위에서 동작합니다.
  • 외장 설치형과 내장형 모두 널리 쓰여 학습 자료와 운영 경험이 많습니다.

즉, 톰캣은 “자바 웹 로직을 실행해 주는 무대”에 가깝습니다. 비즈니스 로직을 대신 만들어 주지는 않지만, 요청과 응답이 오가는 전체 실행 사이클을 책임집니다.

브라우저 요청이 톰캣 안에서 처리되는 전체 흐름

브라우저가 http://localhost:8080/users로 요청을 보내면 먼저 톰캣의 커넥터가 연결을 받습니다. 여기서 요청 라인, 헤더, 바디가 읽히고, 내부적으로 HttpServletRequest, HttpServletResponse 같은 객체가 준비됩니다.

그다음 톰캣은 요청이 어느 애플리케이션으로 가야 하는지, 어느 URL 패턴과 매칭되는지 확인합니다. 내부적으로는 Engine -> Host -> Context -> Wrapper 같은 구조를 따라 대상 서블릿을 찾습니다. 이 단계에서 경로를 못 찾으면 404가 나고, HTTP 메서드가 맞지 않으면 405가 발생하기 쉽습니다.

매핑이 끝나면 요청은 필터 체인을 통과합니다. 필터는 인증 확인, 문자 인코딩 설정, 공통 로깅, CORS 헤더 추가, 응답 시간 측정 같은 공통 처리를 넣는 위치입니다. 필터가 chain.doFilter()를 호출해야 다음 단계로 넘어가며, 여기서 요청을 끊어버리면 서블릿까지 도달하지 못합니다.

그 후에야 실제 서블릿의 service()가 호출되고, HTTP 메서드에 따라 doGet(), doPost() 같은 메서드로 분기됩니다. 서블릿이 상태 코드, 헤더, 응답 본문을 채우면 톰캣이 이를 소켓으로 다시 내보냅니다. 이때 응답 버퍼가 커밋된 뒤에는 상태 코드나 헤더를 바꾸기 어렵기 때문에, 예외 처리 시점도 중요합니다.

이 흐름을 알면 장애를 볼 때도 판단이 빨라집니다.

  • 요청이 톰캣까지 오지 않았다면 네트워크, 포트, 프록시 문제일 가능성이 큽니다.
  • 톰캣에는 왔지만 매핑이 안 되면 URL, 컨텍스트 경로, 배포 문제일 수 있습니다.
  • 서블릿 전에 실패하면 필터, 바인딩, 검증 단계부터 봐야 합니다.
  • 서블릿 안에서 예외가 터지면 애플리케이션 코드와 하위 의존성을 의심해야 합니다.

서블릿 생명주기와 톰캣이 객체를 관리하는 방식

톰캣을 이해할 때 가장 중요한 개념 중 하나가 서블릿 생명주기입니다. 서블릿은 요청마다 새 객체가 만들어지는 구조가 아닙니다. 보통 톰캣이 애플리케이션 시작 시점이나 첫 요청 시점에 서블릿 인스턴스를 만들고 init()을 한 번 호출한 뒤, 여러 요청에서 같은 인스턴스를 재사용합니다. 애플리케이션이 내려갈 때는 destroy()를 호출합니다.

실무에서 이 점이 중요한 이유는 같은 서블릿 인스턴스를 여러 스레드가 동시에 사용할 수 있기 때문입니다. 요청마다 달라지는 값을 서블릿 필드에 넣어두면 경쟁 상태가 생기기 쉽습니다. 요청별 데이터는 메서드 내부 지역 변수로 다루고, 필드에는 읽기 전용 설정이나 스레드 안전한 객체만 두는 편이 안전합니다.

간단한 서블릿은 다음처럼 작성합니다.

import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/greet")
public class GreetServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("GreetServlet init");
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        String name = request.getParameter("name");

        if (name == null || name.isBlank()) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.setContentType("text/plain;charset=UTF-8");
            response.getWriter().write("name 파라미터가 필요합니다.");
            return;
        }

        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("안녕하세요, " + name + "님");
    }

    @Override
    public void destroy() {
        System.out.println("GreetServlet destroy");
    }
}

이 예제에서 name은 요청마다 달라지므로 메서드 내부에서만 다루고 있습니다. 반대로 private String lastName; 같은 필드를 두고 요청 값을 저장하면 다른 요청과 뒤엉킬 수 있습니다. 입문 단계에서 자주 놓치는 포인트지만, 장애와 직결되는 중요한 기본기입니다.

바인딩, 검증, 직렬화는 어디서 일어날까

서블릿만 직접 쓸 때는 요청 파라미터를 꺼내고, 형 변환하고, 검증하고, 응답 본문을 만드는 일을 개발자가 거의 직접 처리합니다. 위의 request.getParameter("name") 같은 코드가 바로 그 예입니다.

하지만 스프링 MVC를 쓰면 이 과정의 상당 부분이 자동화됩니다. 여기서 중요한 점은 톰캣이 사라지는 것이 아니라, 톰캣이 요청을 먼저 받아 스프링의 DispatcherServlet으로 넘긴다는 사실입니다. 즉, 바인딩과 검증, 직렬화는 보통 프레임워크가 담당하지만, 그 작업이 실행되는 기반 요청-응답 사이클은 여전히 톰캣 위에 있습니다.

아래 예시는 그 흐름을 잘 보여줍니다.

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public UserResponse create(@Valid @RequestBody UserRequest request) {
        return new UserResponse(request.name(), "CREATED");
    }

    public record UserRequest(@NotBlank String name) {}
    public record UserResponse(String name, String status) {}
}

이 코드를 호출할 때 실제 내부 흐름은 대략 다음과 같습니다.

  • 톰캣이 HTTP 요청과 바디를 읽습니다.
  • 요청이 DispatcherServlet으로 전달됩니다.
  • JSON 바디가 UserRequest 객체로 바인딩됩니다.
  • @Valid에 따라 검증이 수행됩니다.
  • 반환된 UserResponse가 JSON으로 직렬화됩니다.
  • 톰캣이 최종 응답을 클라이언트로 전송합니다.

여기서 400 에러가 났다면 컨트롤러 로직보다 먼저 바인딩이나 검증 단계에서 실패했을 가능성이 큽니다. 예를 들어 JSON 형식이 잘못됐거나 name이 비어 있으면 컨트롤러 본문에 진입하기도 전에 예외가 발생할 수 있습니다. 이 구분이 되면 “톰캣 문제인가, 스프링 문제인가, 내 코드 문제인가”를 훨씬 빨리 가를 수 있습니다.

설정 포인트로 보는 톰캣 운영 감각

개발 환경에서는 내장 톰캣 덕분에 설정이 잘 안 보이지만, 운영이나 장애 대응에서는 몇 가지 지점을 꼭 알아야 합니다. 대표적으로 자주 보는 위치는 다음과 같습니다.

  • conf/server.xml: 포트, 커넥터, 엔진, 호스트 설정
  • conf/web.xml: 전역 웹 기본 설정
  • webapps/: 배포된 애플리케이션 위치
  • logs/: 톰캣 로그 위치
  • bin/: 시작과 종료 스크립트

외장 톰캣을 직접 다룰 때 가장 먼저 보는 설정은 커넥터입니다.

<Connector port="8080"
           protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           maxThreads="200"
           acceptCount="100" />

여기서 port는 요청을 받는 포트, connectionTimeout은 연결 대기 시간, maxThreads는 동시 요청 처리량에 직접 영향을 주는 스레드 풀 크기입니다. 요청이 몰릴 때 응답이 느려진다면 단순히 CPU만 볼 것이 아니라 스레드 수, 대기열, 느린 DB 호출, 외부 API 지연을 함께 봐야 합니다.

스프링 부트에서는 같은 개념을 더 익숙한 설정 파일로 다룹니다.

server:
  port: 8080
  servlet:
    context-path: /api
  tomcat:
    threads:
      max: 200

이 설정만 바뀌어도 실제 호출 URL은 크게 달라집니다. 예를 들어 컨텍스트 경로가 /api라면 /users가 아니라 /api/users로 호출해야 합니다. 개발 중 404가 나는 대표적인 이유가 바로 이 지점입니다.

추가로 실무에서 자주 챙기는 설정 포인트도 기억해 둘 만합니다.

  • 포트 충돌 여부
  • 컨텍스트 경로와 리버스 프록시 경로 일치 여부
  • 문자 인코딩 설정
  • 접근 로그와 애플리케이션 로그 분리 여부
  • 세션 타임아웃과 업로드 크기 제한
  • 커넥션 풀, 스레드 풀, 외부 의존성 타임아웃 균형

예외와 디버깅에서 먼저 볼 단서

톰캣 관련 문제를 볼 때는 “어디 단계에서 실패했는가”를 먼저 나눠야 합니다. HTTP 상태 코드와 로그 메시지는 그 구분에 매우 유용한 단서입니다.

  • 404: URL 매핑 오류, 컨텍스트 경로 착각, 배포 누락
  • 405: 허용되지 않은 HTTP 메서드 호출
  • 400: 요청 파라미터 누락, 형 변환 실패, JSON 파싱 실패, 검증 실패
  • 500: 애플리케이션 내부 예외, DB 오류, 직렬화 실패, 널 포인터

예를 들어 톰캣 시작 자체가 안 된다면 포트 충돌부터 의심할 수 있습니다. 이때 로그에는 java.net.BindException: Address already in use 같은 문구가 자주 보입니다. 반대로 404라면 톰캣은 떠 있지만 요청 경로를 처리할 대상이 없다는 뜻에 가깝습니다.

실제로는 아래 정도의 명령만으로도 원인 범위를 많이 좁힐 수 있습니다.

curl -i http://localhost:8080/api/users
lsof -i :8080
tail -f logs/catalina.out

curl -i는 상태 코드와 헤더를 바로 확인할 수 있어 가장 빠른 1차 점검 도구입니다. lsof -i :8080은 누가 포트를 점유 중인지 확인할 때 유용하고, catalina.out이나 애플리케이션 로그를 함께 보면 톰캣 시작 실패인지, 매핑 실패인지, 컨트롤러 내부 예외인지 구분하기 쉬워집니다.

디버깅할 때는 다음 질문 순서로 좁혀 가는 것이 효율적입니다.

  • 톰캣 프로세스가 실제로 떠 있는가
  • 내가 호출한 포트와 경로가 맞는가
  • 컨텍스트 경로를 빼먹지 않았는가
  • 필터에서 요청을 차단하고 있지 않은가
  • 바인딩이나 검증에서 먼저 실패한 것은 아닌가
  • 서블릿이나 컨트롤러 안에서 예외가 난 것은 아닌가
  • 톰캣 로그와 애플리케이션 로그를 모두 확인했는가

이 순서를 익혀 두면 “컨트롤러가 안 타니까 톰캣 문제겠지” 같은 막연한 추측 대신, 요청이 어느 단계까지 왔는지를 기준으로 원인을 더 빨리 좁힐 수 있습니다.

톰캣을 이해하면 스프링도 더 잘 보인다

톰캣은 단순히 오래된 자바 서버 프로그램 이름이 아닙니다. HTTP 요청을 받아 애플리케이션 코드로 연결하고, 필터와 서블릿 체인을 거쳐 응답을 다시 내보내는 실행 구조 그 자체입니다. 그래서 톰캣을 이해하면 스프링 MVC의 동작도 더 잘 보이고, 404·400·500이 각각 어디에서 생기는지도 더 선명해집니다.

특히 입문자라면 톰캣을 이렇게 기억하면 좋습니다. “이 요청은 어디로 들어왔고, 어떤 매핑을 거쳐, 어디서 검증되고, 어떤 코드가 응답을 만들었는가?” 이 질문에 답할 수 있으면 프레임워크를 바꿔도 서버 동작을 훨씬 안정적으로 이해할 수 있습니다.

원문 참고

https://www.maeil-mail.kr/question/22

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...