지난 2월, 웹 애플리케이션 서버(WAS)의 내부 동작 원리를 깊이 있게 이해하고자 순수 Java만을 이용하여 WAS를 직접 구현해 보는 프로젝트를 진행했습니다.
요즘은 Spring Boot가 내장 톰캣(Embedded Tomcat)을 제공하기 때문에, 개발자들은 단순히 main 메서드만 실행하면 뚝딱 웹 서버가 뜨는 '마법'에 익숙해져 있습니다. 하지만 이 마법 상자 안에서 HTTP 요청이 어떻게 파싱되고, 어떤 과정을 거쳐 우리가 만든 @Controller에 도달하는지 그 블랙박스를 열어보는 것은 백엔드 개발자로서 한 단계 성장하기 위한 필수 코스라고 생각합니다.
본격적인 코드를 짜기 전에, 오늘은 첫 번째 시간으로 우리가 만들 WAS(톰캣)가 도대체 무슨 일을 하는 녀석인지, 그리고 거대한 스프링 프레임워크와 어떻게 협력하는지 그 웅장한 아키텍처의 흐름을 파헤쳐 보겠습니다.
1. 스프링은 웹 서버가 아니다?
가장 먼저 깨야 할 편견은 "스프링이 웹 서버 역할을 한다"는 생각입니다. 엄밀히 말해 스프링 프레임워크 자체는 웹 서버가 아닙니다.
클라이언트(웹 브라우저)가 보내는 데이터는 본질적으로 단순한 텍스트 덩어리(HTTP 메시지)일 뿐입니다. 누군가는 이 네트워크 단의 바이트(Byte) 스트림을 읽어서 자바가 이해할 수 있는 객체로 변환하고, 요청마다 스레드를 할당해 주어야 합니다. 이 궂은일을 담당하는 것이 바로 WAS(Web Application Server), 그중에서도 우리가 가장 잘 아는 아파치 톰캣(Tomcat) 입니다.
2. WAS(톰캣)의 진짜 역할: 서블릿 컨테이너
톰캣은 기본적으로 '서블릿 컨테이너(Servlet Container)'입니다. 서블릿이란 자바를 사용하여 웹 페이지를 동적으로 생성하는 서버 측 프로그램을 말합니다.
톰캣은 다음과 같은 아주 중요한 인프라적 역할을 수행합니다.
- 통신 지원: 소켓(Socket)을 열고 클라이언트의 연결을 기다립니다.
- HTTP 메시지 파싱: 헤더(Header), 바디(Body), URI 등을 파싱하여 자바 객체인 HttpServletRequest와 HttpServletResponse를 생성합니다.
- 스레드 관리 (Thread Pool): 다수의 클라이언트 요청을 동시에 처리하기 위해 스레드 풀을 관리하고, 요청이 들어올 때마다 유휴 스레드를 할당합니다.
- 서블릿 생명주기 관리: 서블릿 클래스의 인스턴스를 생성(init), 호출(service), 소멸(destroy)시킵니다.
우리가 순수 Java로 구현할 목표가 바로 이 부분입니다. 소켓을 열고, HTTP 스펙에 맞춰 문자열을 자르고, 스레드를 할당하는 과정을 직접 구현하게 될 것입니다.
3. 스프링의 등장: DispatcherServlet이라는 거대한 프론트 데스크
그렇다면 스프링은 어디서 등장할까요?
톰캣이라는 거대한 컨테이너 안에는 여러 서블릿이 존재할 수 있습니다. 스프링은 웹 요청을 처리하기 위해 톰캣 안에 DispatcherServlet 이라는 아주 특별하고 거대한 서블릿을 딱 하나 띄워놓습니다. 이것을 프론트 컨트롤러(Front Controller) 패턴이라고 합니다.
톰캣이 HTTP 요청을 예쁘게 객체(HttpServletRequest)로 포장해서 DispatcherServlet에게 던져주면, 그때부터 우리가 아는 스프링의 생명주기가 시작됩니다.
- DispatcherServlet 수신: 톰캣으로부터 모든 요청을 가로채어 받습니다.
- HandlerMapping 조회: "이 URL(/api/users)을 처리할 컨트롤러가 누구야?" 하고 찾아봅니다.
- HandlerAdapter 실행: 찾은 컨트롤러의 메서드를 실행할 수 있도록 파라미터를 맞추어 실행(Invoke)합니다.
- Controller (비즈니스 로직): 드디어 우리가 작성한 코드(@GetMapping, @PostMapping 등)가 실행되고 데이터를 반환합니다.
- ViewResolver / MessageConverter: 반환된 데이터를 화면(HTML)으로 만들지, 아니면 JSON으로 변환할지 결정하여 최종 응답 형태를 만듭니다.
4. 전체적인 데이터의 여행 흐름 (Summary)
정리하자면 사용자가 버튼을 클릭했을 때의 데이터 흐름은 다음과 같습니다.
브라우저 ➔ 네트워크(Socket) ➔ Tomcat (HTTP 파싱 & 스레드 할당) ➔ 필터(Filter) ➔ Spring DispatcherServlet ➔ 인터셉터(Interceptor) ➔ Controller (우리가 짠 코드)
마무리하며
이번 글에서는 스프링과 톰캣의 명확한 역할 분담과, 요청이 컨트롤러까지 도달하는 큰 그림을 그려보았습니다. 우리가 작성한 비즈니스 로직이 실행되기까지 뒷단에서 얼마나 많은 일들이 벌어지고 있는지 감이 오시나요?
다음 포스팅에서는 이 이론을 바탕으로, 톰캣이 해주던 궂은일(소켓 통신, HTTP 파싱)을 순수 Java의 ServerSocket과 InputStream을 활용하여 직접 코드로 구현해 보는 과정을 다뤄보겠습니다. 블랙박스를 직접 열어보는 다음 여정을 기대해 주세요!
'개발 공부 > 스프링' 카테고리의 다른 글
| SpringApplication에 대한 정리 (0) | 2026.03.25 |
|---|---|
| 백엔드 계층 구조 정리 2 (0) | 2026.03.11 |
| 백엔드 계층 구조 정리 1 (0) | 2026.03.11 |
| 스프링 기초 (0) | 2026.02.17 |
| 스프링 정리 (0) | 2026.02.16 |