4-1. MVC 프레임워크 만들기 - FrontController

shin·2025년 5월 18일

Spring MVC

목록 보기
14/25

목차

  • 프론트 컨트롤러 패턴 소개
  • 프론트 컨트롤러 도입
  • view 분리
  • model 추가
  • 단순하고 실용적인 컨트롤러
  • 유연한 컨트롤러

학습 목표 : Spring MVC의 동작 원리를 직접 구현하면서 체득

  • 단계적으로 MVC 프론트 컨트롤러를 구현하다 보면, Spring MVC의 DispatcherServlet 구조와 거의 유사한 형태로 귀결됨

1. Front Controller 패턴 소개


  • 공통적인 입구가 없고 공통 서블릿을 모두 갖고 있어야 하는 구조
    • 특정 controller가 필요한 경우, 그 controller를 바로 호출

  • 서블릿을 도입을 해서 공통의 로직을 모아놓고 입구로 사용하는 구조
    • 수문장 역할을 하는 컨트롤러 하나를 만들고 그것만 호출

Front Controller 패턴이란?

  • 웹 애플리케이션에서 모든 요청을 단일 진입점을 통해 처리하는 패턴
  • 이 패턴은 요청을 공통적으로 처리하고, 적절한 실제 컨트롤러로 분배(dispatch)하는 역할을 수행함

FrontController 패턴 특징

  • 프론트 컨트롤러 서블릿 하나로 클라이언트 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 입구가 하나
  • 공통 처리 가능
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨


+ 스프링 웹 MVC와 프론트 컨트롤러


  • 스프링 웹 MVC의 핵심도 바로 FrontController
    • 스프링 웹 MVC는 웹 애플리케이션 아키텍처 중 하나인 Front Controller 패턴을 기반으로 설계되었음
    • 이 패턴을 DispatcherServlet이 구현하고 있고, 실제로는 이 서블릿이 모든 HTTP 요청의 입구 역할을 수행함

DispatcherSevelet이 중요한 이유

  • 단일 진입점(Front Controller)으로 모든 요청을 처리
  • 공통 처리 로직 삽입 가능 : 로깅, 인증, 예외처리
  • 요청을 유연하게 분기 처리 : URL 패턴, HTTP 메서드, 파라미터 등 다양한 기준으로 컨트롤러 선택 가능
  • 서블릿이지만 내부적으로는 Spring 컨텍스트에 의해 동작 -> Spring Bean으로 등록된 객체들과 연동

요청 흐름 요약

[요청]
→ [DispatcherServlet (Front Controller)]
→ [HandlerMapping (요청-컨트롤러 매핑)]
→ [핸들러 어댑터 (호출 방식 맞춤)]
→ [Controller (핸들러)]
→ [ModelAndView 반환]
→ [ViewResolver]
→ [View 렌더링]
→ [응답 반환]

DispatcherSevelet

  • 역할 : Front Controller
  • 모든 요청을 받아서 다음 단계로 전달(요청 입구)

HandlerMapping

  • 역할 : 요청 매핑
  • URL이나 어노테이션을 기반으로 어떤 controller를 호출할지 결정

HandlerAdapter

  • 역할 : 호출 보조
  • 컨트롤러 호출 방식을 유연하게 처리(@Controller, @HttpRequestHandler 등 다양한 방식 지원)

Controller

  • 역할 : 요청 처리
  • 실제 비즈니스 로직 수행(@Controller, @RestController 클래스가 여기에 해당)

ModelAndView

  • 역할 : 결과 데이터 + 뷰 이름
  • 컨트롤러가 반환, 모델(데이터)과 뷰 논리 이름 포함

ViewResolver

  • 역할 : 뷰 찾기
  • 논리 뷰 이름 -> 실제 뷰 파일 경로로 변환(ex. "home" -> "/WEB-INF/views/home.jsp")

View

  • 역할 : 렌더링
  • JSP, Thymeleaf, JSON 등 실제 응답을 생성

DispatcherServlet 동작 예시

1.	사용자가 GET /members 요청
2.	서블릿 컨테이너가 DispatcherServlet에 요청 전달
3.	DispatcherServlet이 HandlerMapping에게 물어봄 → MemberController 매핑 확인
4.	HandlerAdapter가 해당 컨트롤러를 호출
5.	MemberController가 ModelAndView("members", model) 반환
6.	ViewResolver가 "members" → /WEB-INF/views/members.jsp 해석
7.	View가 렌더링되어 HTML 반환


2. 프론트 컨트롤러 도입 - v1


  • 기존 코드를 최대한 유지하면서, 프론트 컨트롤러를 도입하는 것이 목표
  • 먼저 구조를 맞추어두고 점진적으로 리팩토링 진행

V1 구조

1. 클라이언트 요청

GET /members/new-form HTTP/1.1
  • 클라이언트가 HTTP 요청을 보냄
  • 사용자가 웹 브라우저에서 특정 경로로 요청을 보냄

2. Front Controller(Servlet) 요청 수신

  • 그 요청을 Front Controller라는 서블릿이 받음
  • 모든 요청이 이 서블릿 하나를 통해 들어옴(입구 일원화)

3. URL -> Controller 매핑 정보 조회

  • URL과 Controller 인스턴스 간의 매핑 정보가 저장된 공간이 존재
  • 요청 URL에 해당하는 컨트롤러를 해당 공간에서 찾아냄

4. 컨트롤러 호출

  • 찾은 컨트롤러를 호출하여 비즈니스 로직을 수행
  • request와 response를 직접 사용해서 파라미터 추출 및 응답 데이터 설정

5. 뷰(JSP)로 포워딩

  • 컨트롤러는 JSP 파일 경로로 반환하거나 바로 forward 처리
  • JSP가 모델 데이터를 받아서 HTML 생성

6. 클라이언트에게 HTML 응답 반환

  • 최종적으로 JSP가 렌더링한 HTML이 웹 브라우저로 전송됨

ControllerV1

  • 프론트 컨트롤러는 URL에 따라 해당 컨트롤러를 찾고, 그 컨트롤러를 ContrllerV1 인터페이스 타입으로 호출
package hello.servlet.web.frontcontroller.v1;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface ControllerV1 {

	void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
  • 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입

    • 각 컨트롤러들은 이 인터페이스를 구현하면 됨
    • 컨트롤러들이 이 인터페이스를 구현해서 실제 로직을 수행
  • 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있음

    • 프론트 컨트롤러는 인터페이스만 알고 있으면 됨
    • 구체적인 컨트롤러 내부 구현에는 의존하지 않음 -> OCP(개방-폐쇄 원칙) 만족
    • 새로운 컨트롤러를 추가해도 인터페이스만 지키면 프론트 컨트롤러 수정은 불필요함

MemberFormContrllerV1 - 회원 등록 컨트롤러

  • ControllerV1 인터페이스를 구현한 컨트롤러 클래스
package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.web.frontcontroller.v1.ControllerV1;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MemberFormControllerV1 implements ControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
  • 요청이 /front-controller/v1/members/new-form 같은 URL로 들어왔을 때 회원 등록 폼 JSP를 보여주는 역할을 수행함
  • implements ControllerV1 : ControllerV1 인터페이스 구현 -> 프론트 컨트롤러가 호출 가능

MemberSaveControllerV1 - 회원 저장 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MemberSaveControllerV1 implements ControllerV1 {
    
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        request.setAttribute("member", member);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

MemberListControllerV1 - 회원 목록 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

public class MemberListControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        List<Member> members = memberRepository.findAll();  // 전체 회원 목록 조회
        request.setAttribute("members", members);           // JSP에서 사용할 모델 등록

        String viewPath = "/WEB-INF/views/members.jsp";     // 뷰 경로 지정
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);              // JSP 포워딩
    }
}
  • 내부 로직은 기존 서블릿과 거의 동일함

FrontControllerServletV1 - 프론트 컨트롤러

  • 모든 요청을 받아서 해당 컨트롤러로 위임하는 프론트 컨트롤러 전체 구현
package hello.servlet.web.frontcontroller.v1;

import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    public FrontControllerServletV1() {
    
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
            
        System.out.println("FrontControllerServletV1.service");

        String requestURI = request.getRequestURI();  // 요청 URI 추출

        ControllerV1 controller = controllerMap.get(requestURI); // 매핑 정보 조회

        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404 반환
            return;
        }

        controller.process(request, response); // 컨트롤러 위임 호출
    }
}

프론트 컨트롤러 동작 흐름 정리

1. URL 패턴 매핑

  • /front-controller/v1/* -> 이 서블릿이 모든 요청 수신

  • urlPatterns = "/front-controller/v1/*" : /front-controller/v1 를 포함한 하위 모든 요청은 이 서블릿에서 받아들임

    • 예) /front-controller/v1 , /front-controller/v1/a , /front-controller/v1/a/b

2. 요청 URI 추출

  • request.getRequestURI()로 현재 경로 파악

3. 컨트롤러 매핑

  • controllerMap
    • key: 매핑 URL
    • value: 호출될 컨트롤러
  • 먼저 requestURI를 조회해서 실제 조회할 컨트롤러를 controllerMap에서 찾음
  • 만약 없다면 404(SC_NOT_FOUND) 상태 코드를 반환함

4. 컨트롤러 호출

  • 컨트롤러를 찾고 controller.process(request, response)을 호출해서 해당 컨트롤러를 실행함

5. 결과 응답

  • 각 컨트롤러에서 JSP로 포워딩하여 HTML 응답


강의 출처 : 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
Backend development

0개의 댓글