2025. 4. 29. 12:31ㆍSpring Framework/스프링 MVC
[목차]
직접 만든 MVC 프레임워크와 스프링 MVC의 구조 비교
1. 전체 구조
2. 스프링 코어와 스프링 MVC 구분
3. 직접 MVC 프레임워크 개발 → 스프링 MVC 사용 (관점 전환)
4. Controller 개발 방식: 인터페이스 구현 vs 어노테이션 기반
1) 인터페이스 구현 컨트롤러
2) 스프링 MVC의 어노테이션 기반 컨트롤러
3) 어노테이션 방식에 대한 자세한 설명
4) 인터페이스 구현과 어노테이션 방식 비교
5. DispatcherServlet 구조 살펴보기
1) 스프링 MVC의 프론트 컨트롤러: DispatcherServlet
2) 요청 흐름
3) 스프링 MVC가 제공하는 인터페이스 - 유연한 확장 가능
6. HandlerAdapter
1) 기존과의 차이점
2) Controller 인터페이스 사용 컨트롤러
3) HttpRequestHandler 인터페이스 사용 컨트롤러
4)@RequestMapping 어노테이션 사용 컨트롤러
1. 전체 구조
스프링 MVC는 직접 만든 프론트 컨트롤러 기반 MVC 프레임워크를 어노테이션 기반, 자동 맵핑으로 확장한 것일 뿐, 구조 자체는 똑같다.
직접 만든 MVC 프레임워크 구조
스프링 MVC 구조
스프링 MVC는 기본적으로 우리가 직접 만든 프레임워크의 역할을 동일하게 수행하지만,
이름과 세부 구현이 스프링 표준에 맞게 정리되어 있다.
직접 만든 MVC | 스프링 MVC |
FrontController | DispatcherServlet |
handlerMappingMap | HandlerMapping |
MyHandlerAdapter | HandlerAdapter |
ModelView | ModelAndView |
viewResolver | ViewResolver |
MyView | View |
2. 스프링 코어와 스프링 MVC 구분
DI 컨테이너와 DispatcherServlet의 세계관 구분
- Spring Core: 객체(빈)를 만들고 관리해주는 시스템
- 핵심 주제는 DI(의존성 주입), 즉 객체를 대신 만들어주고 주입해주는 역할
- 중심은 스프링 컨테이너 ApplicationContext, 빈을 등록/관리/주입
- Spring MVC: 스프링 코어 시스템을 기반으로 웹 요청 흐름을 처리하는 추가 기능
- 스프링 MVC는 프론트 컨트롤러로 DispatcherServlet를 사용한다.
- 스프링 MVC에서 사용하는 클래스들도 모두 스프링 DI 컨테이너의 빈으로 등록되어 관리된다
DispatcherServlet, Controller, View 등등 모두 컴포넌트 스캔에서 빈으로 등록됨 - @Component 어노테이션을 사용하면 DI 컨테이너가 컴포넌트 스캔 시 해당 클래스를 빈으로 등록하여 관리한다.
@Controller도 @Component 계열 어노테이션이기 때문에 빈으로 등록됨 - 단, Controller, View 와 같은 클래스는 어노테이션을 활용하여 컴포넌트 스캔시 빈 등록이 되지만,
DispatcherServlet은 Spring Boot의 자동 설정을 통해 @Bean으로 등록된다
3. 직접 MVC 프레임 워크를 개발 → 구현된 스프링 MVC를 사용
만드는 입장에서 봤을 때에서 → 사용하는 입장에서 봤을 때로 관점 전환

- 원래는 프론트 컨트롤러(Dispatcher Servlet)와 핸들러 어댑터, 뷰 반환 등의 과정을 모두 직접 구현하여 프레임워크를 만듦
- 그러나, 스프링 MVC라는 프레임워크를 사용하는 사람의 입장에서는
스프링 MVC에서 컨트롤러를 사용하는 방법만 파악하면 된다. (+그에 따른 내부 동작 원리) - 스프링 MVC는 요청-응답 처리를 표준화하고, 개발자는 비즈니스 로직에만 집중할 수 있게 해준다.
4. Controller 개발 방식: 인터페이스 구현 vs 어노테이션 기반
1) 인터페이스 구현 컨트롤러
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
public class MemberListControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("members");
mv.getModel().put("members", members);
return mv;
}
}
public class MemberListControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
}
@WebServlet(name="frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
...
}
- 각각의 컨트롤러의 형식을 인터페이스로 정의해놓고 구현함
- 프론트 컨트롤러에서는 직접 ‘이 URL이면 이 컨트롤러 써’라고 수동으로 URL을 명시함
그래서 컨트롤러 클래스에는 URL에 대한 정보가 필요 없었음!
2) 스프링 MVC의 어노테이션 기반 컨트롤러
@Controller
public class MemberController {
@RequestMapping("/springmvc/members")
public String members() {
return "members";
}
}
- 스프링 MVC에서 제공하는 Controller: 어노테이션 기반 컨트롤러 @Controller
이건 직접 만든 V1~V4 스타일 컨트롤러들과는 형태가 다른, 최신 방식의 고급 컨트롤러이다. - 형식적으로 보면, 이 컨트롤러는 어떤 인터페이스도 구현하지 않는다.
대신, 스프링은 리플렉션 + 어노테이션 메타데이터를 사용함 - DispatcherServlet은 우리가 만든 프론트 컨트롤러처럼 동작하지만, 개발자가 직접 코드 건드릴 필요 없음!
- 이전에는 프론트 컨트롤러에 URL과 컨트롤러를 하나하나 직접 맵핑했지만
- 스프링에서 개발자는 프론트 컨트롤러에 코드를 추가할 필요 없고
각각의 컨트롤러에 "이 URL 요청은 이 컨트롤러야!"라고 어노테이션을 통해 힌트만 주면
나머지는 스프링이 자동으로 핸들러 매핑 등록 + 어댑터 처리까지 다 해준다.
3) 어노테이션 방식에 대한 자세한 설명
- 어노테이션(Annotation)은 "코드에 붙이는 추가 정보"를 의미
- 어노테이션 자체는 기능을 추가하더나 실행하는 코드가 아니고,
클래스, 메서드, 필드 등에 대한 추가적인 설명을 제공하는 메타 데이터이다. - 이것을 스프링 같은 프레임워크가 런타임에 읽고(리프렉션) 해석해서 적절한 동작을 수행한다.
- 즉, 어노테이션은 코드에 '힌트 메모' 같은 설명을 붙여서 프레임워크가 동작을 조정하도록 만드는 것
- 어노테이션 자체는 기능을 추가하더나 실행하는 코드가 아니고,
- 어노테이션 예시
- @Controller → "이 클래스는 웹 요청을 처리하는 컨트롤러야!"
- @RequestMapping → "이 메서드는 특정 URL 요청을 받아야 해!"
- @Autowired → "이 필드에 스프링이 자동으로 빈을 주입해줘!"
- 이러한 어노테이션 기반 컨트롤러 방식은 개발자가 직접 복잡한 설정을 하지 않아도, 간단한 힌트만으로 유연하고 편리하게 웹 요청 처리를 구성할 수 있다.
- 클래스 레벨 어노테이션: @Component, @Controller, @Service, @Repository 등
- DI 컨테이너(ApplicationContext)가 빈 등록을 하기 위한 정보!
- ApplicationContext 생성 (스프링 DI 컨테이너 시작)
→ 스프링 컨테이너 초기화 단계에서 컴포넌트 스캔
→ 클래스 레벨의 어노테이션을 읽고 빈 등록
- 메소드 레벨 어노테이션: @RequestMapping, @GetMapping, @PostMapping 등
- 컴포넌트 스캔 이후 DispatcherServlet이 사용하는 핸들러 매핑(HandlerMapping)들의 초기화 시점에 메소드 레벨의 어노테이션들이 읽혀서 처리된다.
- 각각의 핸들러 매핑은 DI 컨테이너에 등록된 컨트롤러 빈들을 쭉 스캔하면서 자신이 관심 있는 어노테이션만 찾아서 매핑 정보를 만들어둔다.
- 핸들러 매핑 종류
RequestMappingHandlerMapping: @RequestMapping 붙은 메서드를 가진 빈을 찾아서 URL과 메서드를 매핑
SimpleUrlHandlerMapping: 특정 URL에 단순히 서블릿이나 핸들러 클래스를 매핑
BeanNameUrlHandlerMapping: 빈 이름을 URL처럼 사용해서 매핑
- 따라서 스프링 MVC는 어노테이션 방식을 활용하여 컴포넌트 스캔 과정에서 필요한 객체(빈)를 등록하고, 이후 핸들러 매핑 과정을 통해 요청 경로와 실행할 메서드를 연결함으로써, DispatcherServlet이 웹 요청을 빠르게 처리할 수 있도록 준비한다.
4) 인터페이스 구현과 어노테이션 방식 비교
구분 | 인터페이스 기반 컨트롤러 | 어노테이션 기반 컨트롤러 |
구조 | 명시적으로 인터페이스를 implements 해서 컨트롤러 작성 | 아무 인터페이스 없이 자유롭게 메서드 작성 |
URL 매핑 방식 | 프론트 컨트롤러에 직접 URL-컨트롤러 객체 매핑 | @RequestMapping 어노테이션으로 컨트롤러의 메서드에 직접 맵핑 정보 작성 |
리턴 타입 고정 여부 | 인터페이스에 정의된 리턴 타입 따라야 함 (ex. ModelView, String 등) |
자유롭게 리턴 가능 (String, ModelAndView, 객체 등) |
매개변수 고정 여부 | 인터페이스에 정의된 파라미터를 따라야 함 | 자유롭게 파라미터 선언 가능 (@RequestParam, @ModelAttribute 등) |
처리 방식 | 개발자가 모든 규칙에 맞춰 작성해야 함 | 스프링이 어노테이션 + 리플렉션으로 자동 처리 |
유연성 | 낮음 (인터페이스에 종속) | 높음 (개발자 자유 ↑) |
스프링 MVC에서 제공하는 어노테이션 기반의 컨트롤러가 도입되면서
인터페이스 기반으로 강제되던 개발 방식에서 벗어나,
개발자는 어노테이션만으로 간편하게 웹 요청 흐름을 설계할 수 있게 되었다.
5. DispatcherServlet 구조 살펴보기
1) 스프링 MVC의 프론트 컨트롤러: DispatcherServlet
org.springframework.web.servlet.DispatcherServlet
- 스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어 있다.
스프링 MVC의 프론트 컨트롤러가 바로 디스패처 서블릿(DispatcherServlet) → 스프링 MVC의 핵심 - DispatcherServlet도 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작한다.
DispatcherServlet → FrameworkServlet → HttpServletBean → HttpServlet - 스프링 부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로( urlPatterns="/")에 대해서 매핑한다.
참고: 더 자세한 경로가 우선순위가 높다. 그래서 기존에 등록한 서블릿도 함께 동작한다.
2) 요청 흐름
- 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다.
스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해 두었다. - FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispatcherServlet.doDispatch() 가 호출된다.
- DispatcherServlet의 핵심인 doDispatch() 코드
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
ModelAndView mv = null;
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
// 뷰 렌더링 호출
render(mv, request, response);
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);
}
3) 스프링 MVC가 제공하는 인터페이스 - 유연한 확장 가능
- 스프링 MVC의 큰 강점은 DispatcherServlet 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다 는 점이다.
- 스프링 MVC가 제공하는 인터페이스들만 구현해서 DispatcherServlet 에 등록하면 본인만의 컨트롤러를 만들 수도 있다.
- 주요 인터페이스 목록
- 핸들러 매핑: org.springframework.web.servlet.HandlerMapping
- 핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter
- 뷰 리졸버: org.springframework.web.servlet.ViewResolver
- 뷰: org.springframework.web.servlet.View
6. HandlerAdapter
1) 기존과의 차이점
직접 만든 MVC 프레임워크의 프론트 컨트롤러에서는 | 스프링 MVC의 DispatcherServlet에서는 |
handlerMappingMap에 모든 종류의 컨트롤러와 URL 맵핑 정보를 한꺼번에 저장함 |
맵핑 방식에 따라 HandlerMapping 구현체를 나눠서 관리 handlerMappings에 여러 HandlerMapping 구현체들을 저장함 |
URL과 컨트롤러 객체를 프론트 컨트롤러에 직접 맵핑해서 handlerMappingMap에 넣었음 |
각각의 컨트롤러에 매핑 정보를 입력해놓고, HandlerMapping의 초기화 시점에 각 매핑 정보들이 저장됨 |
2) Controller 인터페이스 사용 컨트롤러
//과거의 스프링 컨트롤러
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
//빈의 이름을 URL 패턴으로 맞춤. 즉, 빈의 이름으로 URL을 매핑
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return new ModelAndView("new-form");
}
}
-
요청: http://localhost:8080/springmvc/old-controller
- 핸들러 매핑으로 핸들러 조회 (handlerMappings 순회)
- HandlerMapping을 순서대로 실행해서, 핸들러를 찾는다.
- 이경우 빈 이름으로 핸들러를 찾아야 하기 때문에
- 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping가 실행에 성공하고
- 핸들러인 OldController를 반환한다.
- 핸들러 어댑터 조회 (handlerAdapters 순회)
- HandlerAdapter 의 supports() 를 순서대로 호출한다.
- SimpleControllerHandlerAdapter 가 Controller 인터페이스를 지원하므로 대상이 된다.
- 핸들러 어댑터 실행
- 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter 를 실행하면서 핸들러 정보도 함께 넘겨준다.
- SimpleControllerHandlerAdapter 는 핸들러인 OldController 를 내부에서 실행하고, 그 결과를 반환한다.
- OldController를 실행하면서 사용된 객체 정리
- HandlerMapping = BeanNameUrlHandlerMapping
- HandlerAdapter = SimpleControllerHandlerAdapter
3) HttpRequestHandler 인터페이스 사용 컨트롤러
//HttpRequestHandler: 서블릿과 가장 유사한 형태의 핸들러이다.
public interface HttpRequestHandler {
void handleRequest(HttpServletRequest request, HttpServletResponse response)
}
//빈 이름과 URL 패턴을 맞춰서 매핑
@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("MyHttpRequestHandler.handleRequest");
}
}
-
요청: http://localhost:8080/springmvc/request-handler
- 핸들러 매핑으로 핸들러 조회 (handlerMappings 순회)
- HandlerMapping을 순서대로 실행해서, 핸들러를 찾는다.
- 이경우 빈 이름으로 핸들러를 찾아야 하기 때문에
- 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping가 실행에 성공하고
- 핸들러인 MyHttpRequestHandler를 반환한다.
- 핸들러 어댑터 조회 (handlerAdapters 순회)
- HandlerAdapter 의 supports() 를 순서대로 호출한다.
- HttpRequestHandlerAdapter가 HttpRequestHandler 인터페이스를 지원하므로 대상이 된다.
- 핸들러 어댑터 실행
- 디스패처 서블릿이 조회한 HttpRequestHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다.
- HttpRequestHandlerAdapter는 핸들러인 MyHttpRequestHandler를 내부에서 실행하고, 그 결과를 반환한다.
- OldController를 실행하면서 사용된 객체 정리
- HandlerMapping = BeanNameUrlHandlerMapping
- HandlerAdapter = HttpRequestHandlerAdapter
4) @RequestMapping 어노테이션 사용 컨트롤러
- 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter
- @RequestMapping 의 앞글자를 따서 만든 이름인데, 이것이 바로 지금 스프링에서 주로 사용하는 어노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터
- 실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.