[1/23] TIL - WAS 안에서 일어나는 일, 기존 MVC 패턴과 Spring MVC 패턴의 차이
[웹서버]
웹 서버는 HTTP 기반으로 동작하며 정적 리소스(HTML, CSS, JS, 이미지 등)를 제공한다.
정적 리소스란 리소스가 이미 완성된 채로 서버에 존재하여 원본 그대로 응답하는 데이터를 의미한다. (html,css,js,이미지)
대표적인 웹서버 -> NGINX, Apache
[WAS(Web Application Server)]
HTTP 기반으로 동작하며 웹 서버의 기능을 포함한다. 추가적으로 코드를 실행해서 Application 로직을 수행하고 DB와 상호작용하여 동적 컨텐츠를 생성한다.

대표적인 Web Application Server -> Tomcat - Spring Boot에 내장되어 있다, Jetty, Undertow
- Web Server와 WAS(Web Application Server)의 차이점
- 실제로는 Web Server도 Application 로직을 포함할 수 있다.
- WAS는 Application 코드를 실행하는 것에 더욱 특화되어 있다.
- Java에서는 Servlet Container 기능을 제공하면 WAS 이다.
[Web system 구성]
WAS만 사용하는 경우
- WAS가 너무 많은 역할을 담당한다
- 서버 과부하 발생 가능성이 높아진다.
- 실행에 가장 중요한 Application 로직이 정적 리소스로 인해 수행되지 않을 수 있다.
- WAS에 장애가 생기면 아무런 화면도 보여주지 못한다.
- 오류 페이지를 클라이언트에게 응답할 수 없다.
실제 웹 구성

- 정적 리소스는 Web Server에서 처리한다.
- Web Server는 Application 로직이 필요한 요청만을 WAS에 전달한다.
[Servlet]
Servlet은 HTTP 프로토콜 기반 요청(Request) 및 응답(Response)을 처리하는데 사용된다. JAVA에서 Sevlet은 HttpServlet 클래스를 상속받아 구현되며, 웹 애플리케이션 개발의 핵심 기술 중 하나이다.
HTTP Request Message 예시
POST /api/users HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
userId=아이디&pssword=비밀번호
HTTP Response Message 예시
HTTP/1.1 200 OK
Content-Type: text/html;charset=UTF-8
Content-Length: 3423
<html>
<body>
...
</body>
</html>
서버에서 일어나는 일
- 서버와 TCP/IP 연결
- HTTP Request Message 필요한 형태로 변환하여 읽기
- HTTP Method 및 URL 분석
- HTTP Header 분석
- HTTP Message Body 읽기 및 변환
- 분석한 결과를 통해 프로세스 실행
- 비지니스 로직 실행
- HTTP Response Message 생성
- HTTP Start Line 생성
- Header 생성
- HTTP Message Body에 응답 데이터를 요청한 형식에 맞춰 응답
- 처리가 불가하다면 예외처리
- 응답 전달
- 연결 종료
그러나 Servlet을 지원하는 WAS를 사용한다면?
1. 비즈니스 로직 실행 (나머지는 서블릿이 해줌)
[Servlet 동작 순서]
localhost:8080/example HTTP API를 호출하면 일어나는 일
@WebServlet(name="ExampleServlet", urlPatterns = "/exmaple")
public class ExampleServlet extends HttpServlet { // HttpServlet을 상속받아 구현한다.
@Override
protected void service(
HttpServletRequest request, // HTTP 요청 정보를 쉽게 사용할 수 있게 만드는 Servlet
HttpServletResponse response // HTTP 응답 정보를 쉽게 제공할 수 있게 만드는 Servlet
) {
// application logic
}
}
동작 순서

- WAS는 HTTP 요청 메세지를 기반으로 새로운 Request, Response 객체 생성
- WAS는 만들어진 Request, Response 객체를 서블릿 컨테이너에 넘겨주며 ExampleServlet 객체 호출
- ExampleServlet에서 비지니스 로직 처리
- 응답에 필요한 정보를 개발자가 입력
- WAS는 Response 객체 정보(개발자가 입력한 정보)로 HTTP 응답 메세지 생성
- 개발자가 하는 일
- Request 객체에 담겨져있는 HTTP 요청 정보를 꺼내서 사용한다.
- 요청 정보(URL, Method, Message Body)를 통해 필요한 기능(비지니스 로직)을 수행한다.
- 생선된 Response 객체에 HTTP 응답 정보를 입력한다.
- Request 객체에 담겨져있는 HTTP 요청 정보를 꺼내서 사용한다.
[Servlet Container]
Servlet을 지원하는 WAS 내부에는 서블릿 컨테이너가 있다. 서블릿 컨테이너는 서블릿을 초기화, 생성, 관리, 호출, 종료하는 역할을 수행한다.
- Servlet 객체 생성시점
- 개발자가 직접 인스턴스화 하여 사용하는것이 아닌, 코드만 작성하면 서블릿 컨테이너가 생성한다.

- Servlet Container가 하는 일
- 서블릿을 초기화, 생성, 관리, 호출, 종료하는 역할을 수행한다.
- Servlet 객체를 싱글톤으로 관리한다.
- 동시 요청에 대한 처리를 위해 Multi Thread를 지원한다.
- 서블릿을 초기화, 생성, 관리, 호출, 종료하는 역할을 수행한다.
싱글톤이 무엇인가요?
싱글톤은 객체를 하나만 생성하여 생성된 인스턴스를 공유하여 사용하는것을 의미합니다. 특정 클래스의 인스턴스가 여러개 생성되지 않도록 하여 자원의 낭비를 방지하고, 인스턴스를 공유함으로써 상태를 일관되게 유지하기 위함입니다. 하지만, 공유 변수 사용을 주의해야 합니다.
[Thread]
HTTP Request가 WAS로 전달되면 Servlet 객체는 누가 호출할까요?
클라이언트에서 Request가 전달되면 Thread가 Servlet 객체를 호출한다.

- 클라이언트 요청 및 TCP/IP 연결
- Thread 할당 후 Servlet 호출
- 응답 후 Thread 반환
Multi Thread
Single Thread 방식을 채택한다면 동시 요청이 발생한 경우, 하나의 작업이라도 지연되거나 서버에 문제가 발생한다면 나머지 요청들에도 영향을 끼치게 됩니다. 과연 이 문제를 어떻게 해결할 수 있을까요?
Multi Thread로 너무 많은 Thread를 생성하고 관리하면 Context Switching 오버헤드가 너무 크다.
Thread Pool (Thread 수를 한정)

- 요청이 들어오면 Thread Pool에서 Thread를 받아 사용한다.
- 사용 완료된 Thread는 Thread Pool에 반납한다
Thread Pool에 존재하는 Thread가 모두 사용 중이라면 어떻게 되나요?
Thread Pool에 Thread가 생길 때까지 대기하거나 거절할 수 있습니다.
ex) 수강신청 대기번호 1032번
정리
- WAS는 Multi Thread를 지원한다.
- 개발자가 Multi Thread 관련 코드는 고려하지 않아도 된다.
- Multi Thread 환경이므로 싱글톤 객체(Servlet, Spring Bean)는 주의해서 사용해야한다.
- Thread Pool
- Thread는 Thread Pool에 보관 및 관리한다.
- Thread Pool에 생성 가능한 Thread 최대치를 관리한다. Tomcat은 최대 200개 기본 설정이 되어있다.(변경 가능)
- 성능테스트를 통해 적정 Thread 수를 찾으면 된다.
- 장점
- 요청 마다 Thread를 생성하는 단점을 보완하였다.
- Thread가 미리 생성되어 있어서 Thread를 생성, 종료하는 비용이 절약된다. → 응답이 빠름
- 생성 가능한 Thread 최대치가 제한되어 있어서 많은 요청이 들어와도 안전하게 처리할 수 있다.
- 단점
- Thread Pool의 최대 Thread 수를 낮게 설정한다면 응답이 지연된다.
- 최대 Thread 수가 너무 높으면 즉, 요청이 많아지면 리소스 부족으로 서버가 다운된다.
지피티한테 WAS에서 서블릿과 쓰레드가 동작하는 방식을 비유해서 설명해달라고 했다.
좋아! 웹 서버(WAS, Web Application Server) 안에서 서블릿(Servlet)과 쓰레드(Thread)가 어떻게 동작하는지, 재미있는 상황에 비유해서 쉽게 설명해줄게.
1. WAS는 레스토랑의 주방이다.
WAS는 레스토랑의 주방처럼 생각하면 돼.
- 레스토랑에는 많은 손님(사용자)이 음식을 주문(요청)하러 온다.
- 주방(WAS)은 손님이 원하는 음식을 준비해(요청을 처리해) 테이블로 가져다 준다.
2. 서블릿은 요리사이다.
서블릿(Servlet)은 이 주방의 요리사와 같아.
- 요리사는 특정 요리를 전문적으로 만든다. 예를 들어, 파스타 요리사는 파스타만 만든다.
- 마찬가지로 서블릿은 특정 작업(예: 로그인 처리, 게시판 글 보기)을 전문적으로 처리해.
3. 쓰레드는 주방의 일꾼이다.
쓰레드(Thread)는 주방에서 일하는 일꾼이라고 생각하면 돼.
- 손님이 많아지면 요리사 혼자 일을 못하니까 일꾼들이 요리사 옆에서 도와주는 거야.
- 쓰레드는 손님(요청)이 들어올 때마다 하나씩 만들어져서, 요리사(서블릿)가 음식을 빠르게 만들도록 도와준다.
4. 손님이 들어오면 어떻게 처리될까?
- 손님(사용자)이 레스토랑(WAS)에 들어온다.
- 손님은 자신이 원하는 메뉴(요청)를 주문한다.
- 예: "나는 파스타를 먹고 싶어요!"
- 주방장(WAS)은 주문을 보고 "파스타 요리사(서블릿)를 호출해야겠군!" 하고 파스타 요리사를 부른다.
- 이때, 요리사(서블릿)는 혼자 일할 수 없으니 일꾼(쓰레드)을 하나 데려와서 일을 시작한다.
5. 여러 손님이 동시에 오면 어떻게 될까?
- 레스토랑에 손님이 10명 오면?
→ 주방장(WAS)은 요리사(서블릿)를 호출하고, 쓰레드(일꾼) 10명을 만들어서 각 손님을 담당시킨다. - 손님이 많아져서 쓰레드(일꾼)가 부족하면?
→ "지금은 일손이 부족합니다. 잠시만 기다려주세요!"라고 말하면서 새 쓰레드가 생길 때까지 대기시킨다.
6. 이 상황에서 중요한 점
- 쓰레드는 한 번에 한 손님만 도와준다. 쓰레드 하나가 여러 손님을 동시에 처리할 수는 없어.
- 요리사(서블릿)는 동시에 여러 요청을 처리할 수 있다. 쓰레드가 도와주니까 가능한 거야.
7. 쉽게 정리
- WAS는 주방.
- 서블릿은 요리사.
- 쓰레드는 일꾼.
- 손님은 사용자 요청.
만약 쓰레드가 없었다면, 요리사 한 명이 손님 하나만 처리해야 해서 아주 비효율적이겠지? 하지만 쓰레드 덕분에 많은 손님을 빠르게 처리할 수 있어! 이게 바로 WAS에서 서블릿과 쓰레드가 협력해서 동작하는 방식이야. 😊
개발자의 역할을 같은 레스토랑 비유로 설명해볼게.
개발자가 해야 할 일은 주방(WAS), 요리사(서블릿), 그리고 전체 운영 프로세스를 설계하고 관리하는 거야.
1. 메뉴 설계하기 (사용자 요청 정의)
먼저, 개발자는 사용자가 무엇을 원하는지(손님이 먹고 싶어 하는 메뉴) 정의해야 해.
- 예를 들어:
- 손님은 로그인하고 싶어 한다.
- 게시판 글을 보고 싶어 한다.
- 장바구니에 물건을 넣고 싶어 한다.
이런 요청을 개발자는 명확하게 정의해야 해.
이 요청은 결국 **URL, HTTP 메서드(GET, POST 등)**로 표현돼.
- 예: /login, /view-post, /add-to-cart
2. 요리사 고용하기 (서블릿 구현하기)
요리사(서블릿)는 요청을 처리하는 핵심이야.
개발자는 각 요청(URL)에 맞는 서블릿을 만들어야 해.
- 예:
- 로그인 요청 → LoginServlet
- 게시판 글 보기 요청 → ViewPostServlet
- 장바구니 추가 요청 → AddToCartServlet
서블릿에서 해야 할 일:
- 손님(사용자)이 보낸 데이터를 받기
- 로그인 예시: ID와 비밀번호를 전달받음.
- 필요한 작업 수행하기
- 예: 데이터베이스에서 사용자 정보 확인.
- 결과를 반환하기
- 예: 로그인 성공/실패 메시지를 반환.
3. 쓰레드 효율 관리 (동시 요청 처리 전략 설계)
개발자는 여러 손님이 동시에 요청했을 때 서버가 멈추거나 느려지지 않도록 쓰레드 효율 관리도 해야 해.
- 쓰레드가 너무 많아지면 서버가 과부하에 걸릴 수 있어.
- 개발자는 쓰레드의 최대 개수와 최소 개수를 설정해 주는 식으로 서버 자원을 관리해.
WAS 설정 파일이나 코드에서 이런 부분을 세팅할 수 있어.
- 예: 톰캣(Tomcat)에서는 server.xml 파일에서 최대 쓰레드 수를 설정.
4. 안전한 동작 보장하기 (동기화와 에러 처리)
여러 쓰레드가 동시에 서블릿을 사용하면 데이터 충돌이 일어날 수 있어.
개발자는 이를 막기 위해 동기화(Synchronization) 또는 상태 관리를 잘 해야 해.
- 예:
- 같은 게시판 글을 두 사용자가 동시에 수정하려고 하면? → 충돌이 날 수 있어!
- 개발자는 "한 번에 한 사람만 수정 가능" 같은 규칙을 만들어야 해.
5. 결과를 예쁘게 포장하기 (응답 처리)
손님이 음식을 주문하면, 요리는 끝났지만 접시에 담아 예쁘게 제공해야 하잖아?
- 개발자는 서블릿이 처리한 데이터를 HTML, JSON, XML 같은 형식으로 사용자에게 전달해야 해.
- 예:
- 웹 페이지로 결과를 보여줌 (HTML).
- 앱으로 데이터를 보냄 (JSON).
- 예:
6. 문제가 생기면 바로 고치기 (디버깅과 로그 관리)
개발자가 해야 할 또 다른 중요한 일은 문제가 생기면 바로 해결하는 거야.
- 로그(Log)를 기록해 손님이 뭘 주문했는지, 주방에서 무슨 일이 있었는지 추적 가능하게 만들어야 해.
- 예:
- 누가, 언제, 어떤 요청을 했는지 기록.
- 요청 처리 중 문제가 생기면 에러 로그 작성.
- 예:
7. 레스토랑 확장하기 (새로운 기능 추가)
레스토랑이 잘 되면 더 많은 손님을 맞이하기 위해 기능을 추가해야 해.
- 개발자는 새로운 서블릿을 추가하거나, 기존 서블릿을 업그레이드하면서 시스템을 확장해.
개발자의 역할을 요약하자면...
- 요청과 서블릿 설계: 손님이 원하는 걸 처리하는 요리사(서블릿)를 설계.
- 효율적인 쓰레드 관리: 동시 요청이 많아도 빠르게 처리될 수 있게 조정.
- 데이터 충돌 방지: 여러 요청이 동시에 들어와도 안전하게 처리되도록 관리.
- 결과 제공: 사용자가 보기 좋은 형태로 결과를 반환.
- 문제 해결: 에러를 추적하고 빠르게 수정.
- 확장과 유지보수: 시스템이 잘 돌아가도록 개선.
[Template Engine]
동적인 웹 페이지를 생성하기 위해 사용되는 도구이며 템플릿을 기반으로 정적인 부분과 동적인 데이터를 결합하여 HTML, XML 등의 문서를 생성하는 역할을 수행한다.
우리가 흔히 말하는 UI(User Interface)를 만들며, SSR(Server Side Rendering)에 사용된다.

- 대표적인 템플릿 엔진
- Thymeleaf
- Spring과 통합이 잘 되어있다.
- 다양한 기능을 포함하고 있다.
- JSP(Java Server Pages)
- 예전엔 많이 사용했으나, 현재 안 쓰는 추세
- FreeMarker
- Velocity
- Mustache
- Thymeleaf
[MVC 패턴]
Servlet이나 JSP만으로 비지니스 로직과 View Rendering 까지 모두 처리하면 너무 많은 역할을 하게 되고 유지보수가 굉장히 어려워져서(책임이 너무 많음) 고안된 패턴이다. Web Application은 일반적으로 MVC(Model View Controller) 패턴을 사용한다.
- Servlet, JSP 방식의 문제점
- Servlet만을 사용한 경우 View를 위한 코드와 비지니스 로직을 처리하는 코드가 Servlet에 모두 존재하여 유지보수가 어려워진다.
- JSP를 사용하여 View를 분리하였지만 비지니스 로직의 일부가 JSP 파일안에 존재한다. 여전히 책임이 많아 유지보수가 어렵다.
그래서 하나의 Servlet이나 JSP로 처리하던 것들을 Model, View, Controller 영역으로 나눴다.
- 핵심 내용
- View가 분리된 이유의 핵심은 변경이다.
- 기획이 변하지 않는 이상 비지니스 로직과 View의 수정 원인은 별개로 발생한다.
- 화면 구성에 수정이 발생하면 View만 변경
- 요구사항에 수정이 발생하는 경우 비지니스 로직 변경
- 즉, 서로 연관이 없는 코드끼리 함께 존재할 필요가 없다. 완전히 분리하자

- Controller
- 예시 코드에서 Servlet에 해당하는 영역이다.
- HTTP Request를 전달받아 파라미터를 검증한다.
- 비지니스 로직을 실행한다.
- 비지니스 로직을 Controller에 포함하게되면 Controller가 너무 많은 역할을 담당하게 되어 일반적으로 Service Layer를 별도로 만들어서 처리한다.
- Database와 상호작용 하는 Layer를 따로 구분하여 Repository Layer를 추가로 구성한다.
- Controller도 비지니스 로직을 포함할 수 있지만 일반적으로 Service Layer를 호출하는 역할을 담당한다.
- View에 전달할 결과를 조회하여 Model 객체에 임시로 저장한다.
- Model
- View에 출력할 Data를 저장하는 객체이다.
- View는 비지니스 로직이나 Data 접근을 몰라도 되고 View Rendering에만 집중하면 된다.(책임 분리)
- View
- 예시 코드에서 JSP에 해당하는 영역이다.
- Model 객체에 담겨져 있는 Data를 사용하여 화면을 Rendering 한다.
[MVC 패턴의 문제점]
MVC 패턴을 적용 후 View의 역할은 필요한 데이터를 Model 에서 참조하여 화면을 그리는 역할만 수행하면 된다. 하지만 Controller에 해당하는 부분은 여전히 문제를 가지고 있다.
- 문제점
- dispatcher.forward(request, response) View로 이동하는 forward가 항상 중복 호출된다.
- String path= “/WEB-INF/views/new-form.jsp” View의 path를 입력(중복 작업)한다.
- jsp 파일의 경로 혹은 이름이 바뀌면 해당 코드가 변경되어야 한다.
- JSP 이외의 확장자를 사용하려면 전체가 변경되어야 한다.
- HttpServletResponse 객체를 사용하는 경우가 적다. (JSP에서 모두 해결하기 때문)
- HttpServletRequest와 HttpServletResponse는 Test 코드를 작성하기도 매우 힘들다.
- 공통 기능이 추가될수록 Controller에서 처리해야 하는 부분들이 많아진다.
- 공통 기능 처리
- 모든 컨트롤러에서 공통으로 적용되는 기능을 뜻한다.

프론트 컨트롤러 패턴
Servlet(Controller)이 호출되기 전에 공통 기능을 하나의 Servlet에서 처리해주는 패턴이다. 프론트 컨트롤러(Servlet) 하나에 모든 클라이언트측 요청이 들어온다.

- 프론트 컨트롤러의 역할
- 모든 요청을 하나의 프론트 컨트롤러가 받는다.
- 공통 기능을 처리한다.
- 요청을 처리할 수 있는 Controller를 찾아서 호출한다.(Controller Mapping)
- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 Servlet을 사용하지 않아도 된다.
- 일반 Controller들은 HttpServlet을 상속받거나, @WebServlet을 사용하지 않아도 된다.
- 프론트 컨트롤러 의문점
- 프론트 컨트롤러를 사용하면 모든 컨트롤러에서 같은 형태의 응답을 해야하는가?

- 위 그림처럼 공통 처리 로직에 모든 컨트롤러가 연결되기 위해서는 모든 컨트롤러가 return 하는 결과의 형태가 동일해야 한다.
- 하지만, Controller 마다 로직이나 응답해야하는 결과는 당연히 다를테고 응답을 동일하게 맞추려고 한다면 해당 애플리케이션은 확장성, 유지보수성을 잃는다.
- 공통 로직에서 응답별로 퍼즐을 다시 하나하나 처리할 수 있으나 공통 부분의 책임이 너무 커지게된다. 또한, 컨트롤러에서 반환되는 결과가 달라지면 공통처리 부분의 변경또한 불가피하다.
어댑터패턴
다양한 컨트롤러(Handler)를 유연하게 만들기위해 어댑터 패턴을 도입하게 되었다. 컨트롤러들은 동일한 인터페이스를 구현하도록 하고 해당 인터페이스와 공통 로직 사이에 어댑터를 두어 유연하게 만든다. 서로 다른 인터페이스를 갖는 두 클래스를 연결해주는 패턴이다.

- 컨트롤러(Handler)는 비지니스 로직을 처리하고 알맞은 결과를 반환한다.
- 어댑터는 공통 로직과 컨트롤러(Handler)가 자연스럽게 연결되도록 한다.
- 프론트 컨트롤러는 공통으로 처리되는 로직을 수행한다.
- 어댑터 패턴 장점
- 프론트 컨트롤러, 어댑터, 핸들러 모두 각자의 역할만 수행한다. (책임 분리)
- 새로운 컨트롤러(Handler)가 추가되어도 컨트롤러와 어댑터만 추가한다면 공통 로직의 변경이 발생하지 않는다.
- 요약
- Servlet 사용
- 비지니스 로직을 처리하는 코드와 화면을 그리는 View 코드가 함께 존재하는 문제
- JSP 사용
- View 에 해당하는 코드를 분리하였지만, 여전히 비지니스 로직을 JSP에 포함하는 문제
- MVC 패턴 사용
- 공통 로직을 처리하는것에 코드가 중복되는 문제
- 프론트 컨트롤러 패턴 사용
- 공통 로직을 하나의 입구에서 처리하기 위해서 프론트 컨트롤러 패턴 적용
- 각각의 핸들러 호출 후 응답을 프론트 컨트롤러에 맞게 변형시켜야 하는 문제
- Spring MVC 사용
- 프론트 컨트롤러 패턴, 어댑터 패턴이 모두 적용된 현재
- 우리가 사용하는 Spring을 이용한 Web Application 개발 방식에 사용됨
- Spring은 MVC 패턴에 프론트 컨트롤러 패턴, 어댑터 패턴이 적용되어 있다.
- Servlet 사용
[Spring MVC]
Spring은 MVC 패턴에 프론트 컨트롤러 패턴, 어댑터 패턴이 적용된 구조를 가지고 있다.
Dispatcher servlet 이 프론트 컨트롤러, Handler Adapter가 어댑터 역할을 한다.

- DispatcherServlet : Spring의 프론트 컨트롤러
- View : 인터페이스로 구성되어 있다, 확장성을 가지고 있다.
- 실행순서
- Client로 부터 HTTP 요청(Request)을 받는다.
- Handler 조회
- Handler Mapping을 통해 요청 URL에 Mapping된 Handler(Controller)를 조회
- Handler를 처리할 Adapter 조회
- Handler를 처리할 수 있는 Handler Adapter를 조회
- Handler Adapter 실행(handle)
- 알맞은 ****어댑터가 존재한다면 ****Handler Adapter에게 요청을 위임한다.
- Handler 실행(호출)
- Handler Adapter가 실제 Handler(Controller)를 호출하여 실행 및 결과 반환
- Model And View 반환(return)
- Handler Adapter는 Handler가 반환 하는 정보를 ModelAndView 객체로 변환하여 반환
- viewResolver 호출(알맞은 View 요청)
- View Resolver를 찾고 실행
- View 반환
- View Resolver는 View의 논리 이름을 물리 이름으로 전환하는 역할을 수행하고 Rendering 역할을 담당하는 View 객체를 반환
- View Rendering
- View를 통해서 View를 Rendering
- 요약
- DispatcherServlet
- 클라이언트 HTTP Request를 알맞게 파싱하고 클라이언트에게 알맞은 응답을 반환
- 핸들러 목록 정보를 알고있다.
- 핸들러 어댑터 목록 정보를 알고있다.
- HandlerAdapter
- 자신이 처리할 수 있는 Handler인지 확인할 수 있는 기능(Method)이 필요하다.
- 프론트 컨트롤러에서 요청을 위임받았을 때 핸들러에게 요청을 지시하는 기능이 필요하다.
- return 시 Handler로부터 전달받은 결과를 알맞은 응답으로 변환한다.
- Handler
- 요청에 대한 로직을 수행하는 기능이 필요하다.
- DispatcherServlet
Dispatcher Servlet
Spring MVC의 프론트 컨트롤러는 Dispatcher Servlet(Servlet의 한 종류)이다.
- Dispatcher Servlet은 HttpServlet을 상속 받아서 사용하고 Servlet의 한 종류이다.
- Spring Boot는 Dispatcher Servlet을 서블릿으로 자동으로 등록(내장 Tomcat WAS를 실행하면서 등록한다)하고 모든 URL 경로에 대해서 Mapping 한다. → (urlPatterns=”/”)
- 더 자세한 URL 경로가 높은 우선순위를 가진다.
- 개발자가 만들 Servlet이 항상 우선순위가 높아서 실행된다.
- Servlet이 호출되면 HttpServlet이 제공하는 service()가 호출된다.
- Spring MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 Override 해두었다.
- FrameworkServlet.service()를 시작으로 여러 메서드가 호출됨과 동시에 가장 중요한DispatcherServlet.doDispatch()가 호출된다.
protected void doDispatch() {
...
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response); // NotFound 404
}
// 2. 핸들러 어댑터 조회 : 핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 핸들러 어댑터 실행
// 4. 핸들러 어댑터를 통해 핸들러 실행
// 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 여기 안에서 render
processDispatchResult(processedRequest, response, mappedHandler, mv,dispatchException);
...
}
// processDispatchResult()
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
if (mv != null && !mv.wasCleared()) {
// View Render 호출
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
}
// render()
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
// 6. ViewResolver를 통해 View 조회
// 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. View Rendering
view.render(mv.getModelInternal(), request, response);
}
Spring MVC는 DispatcherServlet 코드의 변경 없이 기능변경 및 확장이 가능하다. 기능들이 대부분 Interface로 만들어져 있기 때문이다.
[Controller Interface]
Controller Interface를 implements 하여 구현하게되면 개발자가 원하는 Controller(Handler)를 사용할 수 있게됩니다.
package com.example.springbasicmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
// Spring Bean 이름을 URL로 설정
@Component("/example-controller")
public class ExampleController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("example-controller가 호출 되었습니다.");
return null;
}
}
- @Component
- Spring Bean에 등록하는 역할을 수행한다.
- Spring Bean은 애플리케이션의 구성 요소를 정의하는 객체이다.
- 마치 Servlet이 Servlet Container에 등록되는 것과 같다.
- Spring Bean에 등록하는 역할을 수행한다.
- Handler Mapping
- 핸들러 매핑에서 ExampleController를 찾을 수 있어야 한다.
- Handler Adapter
- Handler Mapping을 통해 찾은 핸들러를 실행할 수 있는 Handler Adapter가 필요
Spring Boot의 Handler Mapping, Handler Adapter
Spring Boot를 사용하면 개발에 필요하여 자동으로 등록되는 HandlerMapping과 HandlerAdapter들이 있다.
HandlerMapping, HandlerAdapter 모두 우선순위대로 조회한다.
- HandlerMapping
- 우선순위 순서
- RequestMappingHandlerMapping
- 우선순위가 가장 높다
- Annotation 기반 Controller의 @RequestMapping에 사용
- BeanNameUrlHandlerMapping(위 예시코드에 사용)
- Spring Bean Name으로 HandlerMapping
- HandlerAdapter
- 우선순위 순서
- RequestMappingHandlerAdapter
- Annotation 기반 Controller의 @RequestMapping에서 사용
- HttpRequestHandlerAdapter
- HttpRequestHandler 처리
- SimpleControllerHandlerAdapter(위 예시코드에 사용)
- Controller Interface 처리
[HttpRequestHandler로 알아보는 Spring MVC 동작 순서]
// 인터페이스
public interface HttpRequestHandler {
void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
// 구현체
@Component("/request-handler")
public class ExampleRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("request-handler Controller 호출");
// 구현 로직
}
}
실행 순서

- HandlerMapping 으로 핸들러 조회
- BeanName으로 Handler 조회(BeanNameUrlHandlerMapping 실행)
- ExampleRequestHandler 반환
- HandlerAdapter 조회
- HandleAdapter의 supports()를 우선순위 순서대로 호출
- HttpRequestHandlerAdapter가 HttpRequestHandler Interface를 지원한다
- HttpRequestHandlerAdapter.supports()

- HandlerAdapter 실행
- DispatcherServlet이 조회한 HttpRequestHandlerAdapter를 실행하며 Handler 정보도 넘긴다
- HttpRequestHandlerAdapter 는 ExampleRequestHandler를 내부에서 실행 후 결과를 반환
- HttpRequestHandlerAdapter.handle() → 단순히 handleRequest를 호출한다 = 오버라이딩된 handleRequest() 호출

- DispatcherServlet에서 호출 → ha.handle()

요약하자면
웹 브라우저에서 특정 페이지를 요청하면, DispatcherServlet이 그 요청을 받습니다. DispatcherServlet은 HandlerMapping을 통해 해당 요청을 처리할 수 있는 ExampleRequestHandler를 찾습니다.
그리고 HttpRequestHandlerAdapter를 사용하여 ExampleRequestHandler의 handleRequest() 메서드를 호출합니다. 이 메서드에서 실제 페이지 처리 로직이 구현되어 있습니다.
HttpRequestHandlerAdapter는 ExampleRequestHandler의 handleRequest() 메서드 호출 결과를 DispatcherServlet에 반환합니다.
[View Resolver]
반환된 ModelAndView 객체를 알맞은 View로 전달하기 위해 DispatcherServlet에서 ViewResolver를 호출하여 View 정보를 설정하는 역할을 수행한다.
package com.example.springbasicmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
// Spring Bean 이름을 URL로 설정
@Component("/view-controller")
public class ViewController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("view-controller가 호출 되었습니다.");
// "test"는 논리적인 ViewName이다. ViewResolver가 물리적인 이름으로 변환해야 한다.
return new ModelAndView("test");
}
}
- Template Engine JSP
- webapp/WEB-INF/form.JSP
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title>블로그 포스트 작성 페이지</title>
</head>
<body>
<h1>블로그 글쓰기</h1>
<form action="save" method="post">
title: <input type="text" name="title" placeholder="제목" />
content: <input type="text" name="content" placeholder="내용" />
<button type="submit">저장</button>
</form>
</body>
</html>
- ViewResolver
- application.properties 설정
- 설정을 기반으로 Spring Boot가 InternalResourceViewResolver 를 만든다.
spirng.mvc.view.prefix=/WEB-INF/views/
spirng.mvc.view.suffix=.jsp

ViewName으로 View를 찾지 못하는 경우(View가 존재하지 않음)

[Spring Boot의 ViewResolver]
Spring Boot를 사용하면 개발에 필요하여 자동으로 등록되는 ViewResolver들이 있다.
- 우선순위 순서
- 아래 두 가지 이외에도 많은 ViewResolver가 존재한다.
- BeanNameViewResolver
- Bean Name으로 View를 찾아 반환
- InternalResourceViewResolver(위 예시코드)
- application.properties 설정 파일에 등록한 prefix, suffix 설정 정보를 사용하여 ViewResolver 등록
[InternalResourceViewResolver(prefix, suffix 설정 정보를 사용하는 ViewResolver)로 알아보는 Spring MVC 동작 순서]

- HandlerAdapter 호출
- HandlerAdapter를 통해 “test” 논리 View Name 얻음
- ViewResolver 호출
- ”test” 이라는 View Name으로 viewResolver를 우선순위 대로 호출
- BeanNameViewResolver는 View를 찾지 못한다.
- InternalResourceViewResolver 호출
- ”test” 이라는 View Name으로 viewResolver를 우선순위 대로 호출
- InternalResourceViewResolver
- InternalResourceViewResolver.buildView(String viewName)

- InternalResourceView
- JSP와 같이 서버에서 이동하는 forward()를 호출하는 경우와 같을 때 사용한다.
- renderMergedOutputModel() → Model을 Request로 바꾼다.


- view.render()
- 외부에서 view.render()를 호출 후 ****RequestDispatcher를 가져와 forward()한다.
Thymeleaf는 View와 Resolver가 이미 존재한다. 라이브러리 의존성만 추가해주면 SpringBoot가 모두 자동으로 해준다. 즉, return “viewName”; 만으로 View가 Rendering 된다.