4.2 DispatcherServlet과 요청 라이프사이클 (프론트 컨트롤러 패턴)
스프링 MVC 구조가 어떻게 브라우저의 HTTP 요청을 낚아채서 우리가 작성한 @RestController 짬바구니로 안전하게 택배를 배달해주는지, 이면의 거대한 파이프라인 흐름을 완벽히 소화하는 단원입니다.
👮♂️ 1. 프론트 컨트롤러 패턴 (Front Controller Pattern)
과거 순수 JSP, Servlet 시절에는 클라이언트가 로그인 요청(/login), 구매 요청(/buy)을 쏘면 각각의 요청마다 수많은 LoginServlet, BuyServlet 객체가 1:1로 생성되어 파편화된 로직을 난해하게 처리해야만 했습니다. 공통적인 에러 처리 로직이나 보안 권한 체크 로직을 모든 서블릿 파일 시작점마다 중복해서 도배(Ctrl+C, Ctrl+V)해야 하는 엄청난 지옥이었습니다.
스프링 형님은 이 문제를 타파하기 위해 "프론트 컨트롤러 패턴(단일 관문 패턴)" 을 도입했습니다.
마치 대형 호텔 로비에 서 있는 한 명의 총괄 지배인(DispatcherServlet) 이 세상 밖에서 날아 들어오는 모든 HTTP 요청(Request)을 입구 1번 게이트에서 무조건 전부 다 독점해서 낚아챕니다. 단일 게이트이므로 여기서 인코딩 변환 작업, 전역 에러 핸들링, 보안 공통 처리를 한 큐에 몰아서 끝내버릴 수 있습니다. 그 후 적절한 직원(@Controller)을 뒤져서 찾은 다음, 로직 처리를 위임하는 방식입니다.
🔄 2. DispatcherServlet 동작 과정 (라이프사이클 5단계)
"총괄 지배인인 DispatcherServlet은 어떻게 적합한 말단 직원 컨트롤러를 찾아 지시를 내리고 퇴근(응답)시킬까?" 가 실무 면접의 가장 흔한 질문 패턴입니다.
① 핸들러 매핑 (HandlerMapping) 수색
GET /api/users/1 요청이 들어오면 DispatcherServlet은 일단 부하인 HandlerMapping (전화번호부 책)을 참견시킵니다.
"야! URL이 저건데 어떤 컨트롤러 클래스의 어느 메서드(@GetMapping)가 담당이냐?"
HandlerMapping은 메모리상의 해시 맵을 뒤져서 실제 담당자인 핸들러ExecutionChain(실행할 컨트롤러 메서드 정보와 가로챌 인터셉터 목록들 뭉치) 를 반환해 줍니다.
② 핸들러 어댑터 (HandlerAdapter) 출현
DispatcherServlet은 찾아낸 컨트롤러를 직접 실행시킬 능력이 없습니다(컨트롤러 구현 생김새가 천차만별이기 때문이죠). 그래서 110V를 220V 플러그에 꼽게 해 주는 돼지코 어댑터, HandlerAdapter 에게 "네가 나 대신 이 컨트롤러 메서드 좀 실행(invoke)시키고 결과를 뽑아다 와" 라며 위임 기동시킵니다.
③ 컨트롤러(Controller) 비즈니스 파싱 로직 실행
어댑터가 비로소 컨트롤러 내부 메서드(로직, 서비스 통신)를 펑! 실행시킵니다. 데이터베이스로부터 꺼낸 회원 정보(결과)를 돌려줍니다.
(이 시점에 파라미터 @RequestBody 바인딩이나 유효성 검사 매핑 등이 톱니바퀴처럼 개입됩니다.)
// 어댑터가 기동시키는 개발자의 코드
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}") // 매핑 레이더
public UserDto getUser(@PathVariable Long id) { // 어댑터 파라미터 자동 바인딩
return new UserDto(id, "Alice"); // 리턴 직후 어댑터가 결과를 채감
}
}
④ MessageConverter 돌림판 (또는 ViewResolver) 가동
현대 REST API(@RestController) 체제에서는 자식 객체로 반환된 자바 객체 덩어리(UserDto)를 JSON 문자열 텍스트로 곱게 갈아서 직렬화(Serialization)시키는 HttpMessageConverter(Jackson) 가 작동하여 이쁜 JSON 바디 응답 스트림을 완성해 냅니다.
// Jackson MessageConverter가 자바 UserDto를 아래와 같은 JSON으로 포매팅
{
"id": 1,
"name": "Alice"
}
⑤ HTTP Response 반환!
DispatcherServlet은 완성된 JSON 문자열 바디와 200 OK 상태값을 클라이언트(브라우저) 포트로 되돌아 쏴주며 라이프사이클을 화려하게 종료시킵니다.
🎯 3. 고수 팁 (Pro Tips)
💡 면접 단골 질문: "서블릿이 단 1개(싱글톤)인데 수천 명의 동시 요청이 섞이지 않나요?"
톰캣은 1개의 거대한
DispatcherServlet인스턴스 뼈대 클래스를 무조건 메모리에 딱 1개(싱글톤 싱글 객체)만 올려둡니다. "그런데 어떻게 초당 수만 건의 유저 트래픽 상태 변수(State)가 서로 엉키지 않고 안전하게 처리되죠?"정답:
DispatcherServlet내부 필드에는 유저의 상태값을 저장하는 조각 필드 로직이 일절 없기 때문입니다. 오로지 뼈대만 있고, 클라이언트들의 개별 리퀘스트 데이터(Request, Response 정보)는 톰캣이 내려주는 개별 스레드 캡슐 영역 안(ThreadLocal, 파라미터 객체 단위)에서 고스란히 담겨져 흘러가기 때문에 안전합니다.@RestController
public class ProductController {
// 💥 금기: 전역 필드에 카운터나 상태 유지 변수 생성
private int count = 0;
@GetMapping("/buy")
public String buy() {
// 3만 명이 동시에 이 컨트롤러(싱글톤)를 들어와서 count++ 연산을 하게 됨
// -> 동시성(Race Condition) 에러 지옥 발생!
count++;
return "구매 성공 대기번호: " + count;
}
}물론 여러분이 컨트롤러 단에서 이런 전역 빈(Bean) 필드 변수에
count++를 수동으로 박아버리면 동시성 동기화 지옥이 펼쳐집니다! 무조건 지역 변수만 사용하세요!