-
Tomcat,Spring Boot에서 GET,POST 요청은 어떻게 처리될까?카테고리 없음 2026. 1. 2. 08:16728x90반응형
Tomcat → Spring MVC → Controller → Response 전체 흐름 완전 해부
1️⃣ 들어가며
Spring Boot로 개발하다 보면 우리는 보통 이렇게만 생각한다.
브라우저 → @GetMapping → 응답하지만 실제로는 이 한 줄의 요청이 처리되기까지
Tomcat, Servlet Container, Filter, DispatcherServlet, HandlerMapping, MessageConverter 등
수많은 컴포넌트와 메소드 호출을 거친다.이 글에서는 GET 요청(@GetMapping) 을 기준으로
Tomcat에서 시작해 → Spring MVC 내부를 통과 → 다시 Tomcat을 통해 응답이 반환되는 전체 흐름을
메소드 단위까지 내려가며 정리한다.

[ Browser ] | | HTTP GET /hello?name=kim v [ Tomcat Connector ] | v [ Servlet Container ] | v [ Filter Chain ] | v [ DispatcherServlet ] | v [ HandlerMapping ] | v [ Controller Method ] | v [ HttpMessageConverter (Jackson) ] | v [ HttpServletResponse ] | v [ Tomcat ] | v [ Browser ]3️⃣ Tomcat 단계 – HTTP 요청 수신과 파싱
3-1. 소켓 수신 & HTTP 파싱
Tomcat은 내부적으로 Coyote HTTP 커넥터를 사용한다.
대표 호출 흐름:
AbstractProtocol.ConnectionHandler.process() → Http11Processor.process()이 단계에서 수행되는 작업:
- TCP 소켓에서 바이트 수신
- HTTP 요청 라인 파싱 (GET /hello HTTP/1.1)
- 헤더 파싱
- 내부 Request/Response 객체 생성
3-2. Coyote → Servlet API 변환
CoyoteAdapter.service()- Tomcat 내부 Request → HttpServletRequest
- Tomcat 내부 Response → HttpServletResponse
- Servlet 컨테이너 파이프라인으로 전달
4️⃣ Servlet Container 파이프라인
Tomcat은 요청을 Valve 체인으로 전달한다.
StandardEngineValve → StandardHostValve → StandardContextValve → StandardWrapperValve여기서 FilterChain + Servlet 호출이 시작된다.
ApplicationFilterChain.doFilter()실행되는 필터 예시:
- CharacterEncodingFilter
- HiddenHttpMethodFilter
- (Spring Security 사용 시)
- DelegatingFilterProxy
- Security Filter Chain
⚠️ Filter는 DispatcherServlet “밖”에서 동작한다
6️⃣ DispatcherServlet – Spring MVC의 관문
Filter 체인을 통과하면 드디어 Spring MVC 진입
FrameworkServlet.service() → processRequest() → DispatcherServlet.doService() → doDispatch()7️⃣ HandlerMapping – @GetMapping 찾기
DispatcherServlet.getHandler() → RequestMappingHandlerMapping.getHandlerInternal() → lookupHandlerMethod()매칭 기준:
- URL Path (/hello)
- HTTP Method (GET)
- params / headers / produces 조건
결과:
- HandlerMethod (컨트롤러 메서드)
8️⃣ HandlerAdapter & Interceptor
DispatcherServlet.getHandlerAdapter() → RequestMappingHandlerAdapter.handle()Interceptor 호출 순서
- preHandle() → Controller 실행
- postHandle() → View/Response 처리 전
- afterCompletion() → 응답 완료 후
9️⃣ Controller 메서드 실행 & 파라미터 바인딩
InvocableHandlerMethod.invokeForRequest()@RequestParam 처리
RequestParamMethodArgumentResolver.resolveArgument()- request.getParameter("name")
- 타입 변환은 ConversionService 사용
실제 컨트롤러 호출
InvocableHandlerMethod.doInvoke() → helloController.hello()🔟 반환값 처리 – JSON 응답 생성
@RestController 기준
RequestResponseBodyMethodProcessor.handleReturnValue() → writeWithMessageConverters()Jackson 선택 과정
MappingJackson2HttpMessageConverter.canWrite() → writeInternal()- Object → JSON 직렬화
- response OutputStream에 write
- Content-Type: application/json
1️⃣1️⃣ 다시 Tomcat으로 – HTTP 응답 전송
Spring이 HttpServletResponse에 기록한 내용을
Tomcat → HTTP Response 직렬화 → Socket write브라우저는 이를 받아:
- JSON 처리 (JS)
- HTML 렌더링
POST
@RestController class AuthController { @PostMapping("/login") public TokenRes login(@RequestBody LoginReq req) { ... } }하지만 이 한 줄 뒤에서는 다음과 같은 질문들이 숨어 있다.
- HTTP Body(JSON)는 언제 읽히는가?
- 왜 @RequestBody가 있어야 JSON이 객체가 되는가?
- Jackson은 누가, 어떤 기준으로 선택하는가?
- 왜 Filter에서 Body를 읽으면 Controller에서 에러가 날까?
이 글에서는 POST + JSON 요청이 들어와서 @RequestBody 객체가 만들어지고 응답이 반환되기까지의 전체 내부 흐름을
Tomcat → Spring MVC → Jackson → Tomcat 순서로 차분히 정리한다.POST /login (JSON) → Tomcat (HTTP 파싱, InputStream 준비) → Filter Chain → DispatcherServlet → HandlerMapping (컨트롤러 찾기) → HandlerAdapter (실행 준비) → @RequestBody 처리 → Jackson 역직렬화 → Controller 실행 → Jackson 직렬화 → Tomcat 응답 전송1️⃣ Tomcat 단계 – HTTP 요청을 Servlet Request로 만들기
클라이언트가 다음과 같은 요청을 보낸다고 가정하자.
POST /login HTTP/1.1 Content-Type: application/json {"id":"kim","pw":"1234"}Tomcat에서 일어나는 일은 다음과 같다.
✔ Tomcat의 역할
- 소켓에서 HTTP 바이트 수신
- 요청 라인, 헤더 파싱
- Content-Type: application/json 저장
- Body는 아직 JSON 객체가 아님
- 단지 InputStream 형태로만 준비됨
📌 중요
Tomcat은 JSON을 해석하지 않는다.
단지 “읽을 수 있는 상태”로만 만들어 준다.2️⃣ Filter Chain – Spring MVC 진입 전 단계
Tomcat은 요청을 Filter Chain으로 전달한다.
ApplicationFilterChain.doFilter()여기서 실행되는 것들:
- CharacterEncodingFilter
- (있다면) Spring Security Filter Chain
- 사용자 정의 Filter
⚠️ 이 시점에서도:
- JSON은 아직 읽히지 않았다
- Body는 여전히 InputStream 상태다
3️⃣ DispatcherServlet – Spring MVC의 시작점
Filter 체인을 통과하면 Spring MVC의 관문인 DispatcherServlet에 도착한다.
DispatcherServlet.doDispatch()이 메서드 안에서 Spring MVC의 핵심 흐름이 시작된다.
4️⃣ HandlerMapping – 어떤 컨트롤러가 처리할지 결정
가장 먼저 수행되는 단계는 HandlerMapping이다.
getHandler(request) → RequestMappingHandlerMapping이 단계에서 Spring은 다음을 기준으로 컨트롤러 메서드를 찾는다.
- URL (/login)
- HTTP Method (POST)
- consumes / produces
- params / headers 조건
결과:
- HandlerMethod (예: AuthController#login)
- 적용될 Interceptor 목록
📌 정리
HandlerMapping의 역할은
**“이 요청을 누가 처리할지 찾는 것”**이다.
5️⃣ HandlerAdapter – 컨트롤러를 실행할 수 있는 실행기 선택
컨트롤러를 찾았다고 바로 실행하지 않는다.
먼저 이 handler를 실행할 수 있는 Adapter를 찾는다.getHandlerAdapter(handler) → RequestMappingHandlerAdapter📌 정리
- HandlerMapping = 대상 찾기
- HandlerAdapter = 실행 방법 결정
6️⃣ @RequestBody 처리 시작 지점 (핵심)
이제 HandlerAdapter가 실제로 컨트롤러를 호출한다.
RequestMappingHandlerAdapter.handle() → invokeHandlerMethod()컨트롤러를 실행하기 전에,
Spring은 메서드 파라미터를 먼저 만들어야 한다.7️⃣ @RequestBody를 처리하는 진짜 주인공
@RequestBody LoginReq req를 담당하는 클래스는 바로 이것이다.
RequestResponseBodyMethodProcessorInvocableHandlerMethod.invokeForRequest() → HandlerMethodArgumentResolverComposite.resolveArgument() → RequestResponseBodyMethodProcessor.resolveArgument()이 시점에서 Spring은 이렇게 판단한다.
“아, 이 파라미터는 request body를 읽어야 하는구나”
8️⃣ HttpMessageConverter 선택 과정
Spring은 등록된 HttpMessageConverter들을 순서대로 확인한다.
선택 기준은 다음과 같다.
- 요청의 Content-Type
- 파라미터 타입 (LoginReq)
- converter.canRead(type, contentType)
readWithMessageConverters() → MappingJackson2HttpMessageConverter 선택9️⃣ Jackson 역직렬화가 실제로 일어나는 순간
Jackson이 선택되면 여기서 실제 변환이 일어난다.
MappingJackson2HttpMessageConverter.readInternal() → ObjectMapper.readValue(InputStream, LoginReq.class)이때 발생하는 일:
- request InputStream 한 번 소비됨
- JSON → Java Object 변환
- 실패 시 HttpMessageNotReadableException
⚠️ 중요 포인트
Request Body는 한 번만 읽을 수 있다.
Filter에서 미리 읽으면 여기서 깨진다.
🔟 컨트롤러 메서드 실행
이제 모든 파라미터가 준비되었다.
AuthController.login(LoginReq req)이 시점부터는:
- 순수 자바 코드
- 비즈니스 로직
- DB / 외부 API 호출
1️⃣1️⃣ 응답 반환 – Jackson 직렬화 (WRITE)
컨트롤러가 객체를 반환하면, 다시 Jackson이 등장한다.
RequestResponseBodyMethodProcessor.handleReturnValue() → writeWithMessageConverters() → MappingJackson2HttpMessageConverter.writeInternal() → ObjectMapper.writeValue(OutputStream, response)- Java Object → JSON
- HttpServletResponse에 write
- Content-Type 설정
1️⃣2️⃣ 다시 Tomcat으로 – HTTP 응답 전송
Spring이 HttpServletResponse에 써 둔 내용을
- Tomcat이 HTTP 응답 포맷으로 직렬화
- 소켓을 통해 클라이언트로 전송
브라우저/클라이언트는 이를 받아:
- JSON 파싱
- 화면 갱신 또는 후속 처리
마무리 – 핵심 정리
POST JSON 요청 → Tomcat은 파싱만 한다 → HandlerMapping이 컨트롤러를 찾는다 → HandlerAdapter가 실행을 담당한다 → @RequestBody가 있어야 Body를 읽는다 → HttpMessageConverter가 Jackson을 선택한다 → ObjectMapper가 JSON을 객체로 만든다 → 응답도 같은 경로로 다시 JSON이 된다@RequestBody는 단순한 어노테이션이 아니라
Spring MVC의 파라미터 해석 전략 + 메시지 컨버터 + Jackson이 만나는 지점이다.이 흐름을 이해하면:
- JSON 파싱 에러 원인을 정확히 알 수 있고
- Filter / Security / Logging 설계를 실수 없이 할 수 있으며
- Spring MVC가 왜 이렇게 설계되었는지도 자연스럽게 보인다.
728x90반응형