[Spring]스프링 기초 - MVC 프레임워크 따라만들기(1)

Inung_92·2023년 1월 31일
1

Spring

목록 보기
2/15
post-thumbnail

Front Controller 패턴이란?

📖애플리케이션 계층의 요청이 발생할 경우 가장 먼저 클라이언트의 요청을 받고 요청 유형에 맞는 컨트롤러들에게 위임하는 방식

아래의 MVC 구조는 클라이언트로부터 하나의 요청이 발생할 경우에 대한 것을 그림으로 표현한 것이다. 만약 요청이 하나가 아닌 대량으로 들어오고, 요청수만큼 컨트롤러가 사용된다면 어떨지 생각해보자.

이전 게시글에서 알아보았던 MVC 모델의 핵심 목적은 유지보수를 원활하게 하는 것이다. 하지만 위에서 말한 것처럼 요청에 1:1로 대응하는 컨트롤러를 사용할 경우 컨트롤러가 너무 많아져 오히려 분리로 인해 유지보수가 어려운 상황이 발생할 수 있다.
이러한 점을 보완하기 위해 Front Controller 패턴을 통해 요청이 들어올때마다 유형을 분석해 해당 요청에 적합한 컨트롤러에게 요청을 위임하는 방법을 알아보자.

Front Controller 패턴 사용해보기

⚡️컨트롤러의 역할

Front Controller 패턴을 사용하기 전에 우선 MVC 모델에서 컨트롤러의 역할에 대해서 다시 한번 알아보고 넘어가자.

  • 클라이언트로부터 요청이 발생하면 요청을 받는다.
  • 요청의 유형을 분석한다.
  • 요청 유형에 알맞는 로직 수행을 지시한다.
  • 결과가 있다면 결과를 저장한다.
  • 해당 결과를 출력한다.

이러한 역할 수행을 하기 위하여 컨트롤러들은 각각 요청 유형을 분석해서 모델에게 요청 처리에 알맞는 로직 수행을 지시하게 된다. 다음은 Front Cotroller 패턴을 사용할 경우 나눠진 역할이다.

  • view에서 클라이언트 요청이 발생
  • Front Controller에서 요청을 받고 유형을 분석
  • 해당 요청 유형에 맞는 하위 컨트롤러에게 요청 위임
  • 요청 유형에 대한 전문성을 가진 컨트롤러는 해당 요청 처리 수행을 위한 로직 수행을 model에게 지시
  • model이 수행한 로직에서 결과가 있는 경우 결과를 저장
  • 저장된 결과를 Front Controller를 경유하여 view로 전달
  • view에서 해당 결과의 데이터를 가공해 클라이언트에게 시각화

⚡️ MVC 구조(Front Controller 패턴 적용)

다음 그림은 Front Controller 패턴을 적용한 MVC 구조이다. 각 객체의 역할을 알아보자.

클래스명내용
DispatcherServletFront Controller로서 클라이언트의 요청을 가장 먼저 받고 하위 컨트롤러에게 요청 위임
HandleMapping요청 URI을 분석하여 요청에 적합한 컨트롤러를 매핑하여 반환
ViewResolver컨트롤러가 반환한 String을 분석하여 JSP 경로 지정
Controller클라이언트의 요청을 실질적으로 처리하는 객체 자료형

⚡️ Front Controller 패턴 장점

  • 요청별로 공통적인 코드 처리를 통해 중복코드 발생 방지
  • 오직 하나의 Servlet으로 대량 요청 처리 가능
  • 요청 유형에 맞는 컨트롤러 매핑을 통해 코드 간소화

위와 같은 장점들로 인해 Spring이나 Structs 같은 MVC 프레임워크에서도 동일한 패턴을 사용한다. 이제 본격적으로 MVC 프레임워크를 따라 만들어보자.

⚡️ 예제

🖥️ Controller 인터페이스 정의

Controller 인터페이스는 Front Controller로부터 위임되는 요청을 분석하여 실질적인 요청 처리를 하기 위한 컨트롤러들을 같은 타입의 구현객체로 관리하기 위하여 정의한다.

public interface Controller(){
	//모든 구현객체에서 공통적으로 사용할 추상메서드 선언
	String handleRequest(HttpServletRequest request, HttpServletResponse response);
}

인터페이스 정의가 완료되었으니 구현 객체인 Controller들을 정의해보자.

🖥️ Controller 구현 객체들 정의

각 컨트롤러들을 요청 URI를 DispatcherSerlet을 통해 분석되어 요청 유형에 맞는 컨트롤러를 HandleMapping 객체를 통해서 반환받을 것이다.

//blood 컨트롤러
public class BloodController implements Controller{
	//로직 수행 model 보유
	BloodAdvisor bloodAdvisor = new BloodAdvisor();
    
    // 요청 처리 메서드
    @override
    public String handleRequest(HttpServletRequest request, HttpServletResponse response){
    	//파라미터 값에 따라 결과를 반환하는 model 메서드 호출
    	String result = bloodAdvisor.getAdvice(request.getParameter("blood"));
        //메서드 실행 결과를 request 객체에 저장
        request.setAttribute("result", result);
        
        //view 경로명 반환
        return "blood/result";
    }
}

//movie 컨트롤러
public class MovieController implements Controller{
	// 로직 수행 model 보유
	MovieAdvisor movieAdvisor = new MovieAdvisor();
    
    // 요청 처리 메서드
    @override
    public String handleRequest(HttpServletRequest request, HttpServletResponse response){
        //파라미터 값에 따라 결과를 반환하는 model 메서드 호출
    	String result = movieAdvisor.getAdvice(request.getParameter("movie"));
        //메서드 실행 결과를 request 객체에 저장
        request.setAttribute("result", result);
        
        //view 경로명 반환
        return "movie/result";
    }
}

컨트롤러에서는 요청에 대한 로직을 수행하고 결과를 출력할 jsp 경로를 반환한다. 그렇다면 요청에 맞는 컨트롤러를 매핑하기 위한 객체를 생성하여 보자.

🖥️ HandleMapping 객체 정의

해당 객체는 DispatcherServlet으로부터 전달받은 URI을 분석하여 요청에 적합한 컨트롤러를 반환해주는 역할을 한다. 또한, 프로그램 수행 시 필요한 컨트롤러들을 보관하는 역할도 병행한다.

public class HandleMapping{
	// 컨트롤러를 보관하기 위한 Map 보유
	private Map<String, Controller> mappings;
    
    // 생성과 동시에 컨트롤러 보유
    public HandleMapping(){
    	// 인스턴스 생성
    	mappings = new HashMap<String, Controller>();
        mappings.put("blood", new BloodController());
        mappings.put("movie", new MovieController());
    }
    
    public Controller getController(String path){
    	//분석된 URI 중 path를 획득하여 map의 key값으로 controller 반환
    	return mappings.get(path);
    }
}

이렇게 분석된 요청 유형(URI)을 통해 해당 컨트롤러가 반환되면 컨트롤러는 handleRequest() 메서드를 통해 요청을 처리하고 view의 경로를 반환한다. 그렇다면 반환된 view의 경로를 어떻게 완성시키는지 알아보자.

🖥️ ViewResolver 정의

해당 객체는 view의 경로를 분석하기 위해 접두사와 접미사를 멤버변수를 가지며, 최종적으로 파라미터로 전달받은 viewName(컨트롤러의 String반환값)을 포함하여 최종 forwording할 view의 경로를 반환한다.

public class ViewResolver(){
	public String prefix; //접두사
    public String suffix; //접미사
    
    public void setPrefix(String prefix){
    	this.prefix = prefix;
    }
    
    public void setSuffix(String suffix){
    	this.suffix = suffix;
    }
    
    // view경로 반환 메서드
    public String getView(String viewName){
    	//접두사 + viewName + 접미사 조합하여 반환
        //여기서 viewName은 "blood", "movie" 등 mappings의 키값과 동일
    	return prefix + viewName + suffix;
    }
}

이렇게해서 Front Controller 패턴에 필요한 객체를 모두 정의하였다. 그렇다면 이제 DispatcherServlet의 코드를 보면서 어떤 원리로 동작하는지 알아보자. 눈 크게 뜨고 보길 바란다.

🖥️ DispatcherServlet 정의

코드를 보기전에 전체적인 로직 수행 순서를 간략히 정리하겠으니 훑어보고나서 코드를 보자.

  • init()을 통해 HandleMapping, ViewResolver 객체 생성
  • VierResolver의 멤버변수 값 초기세팅
  • doXXX()에 공통된 메서드 입력
  • request 객체 인코딩 설정
  • 요청이 들어오면 URI 분석
  • 요청 유형에 맞는 컨트롤러 매핑 및 요청 위임
  • 위임받은 컨트롤러가 model에 요청처리 로직 수행 지시
  • 결과가 있는 경우 결과 요청객체에 저장
  • FrontController를 경유하여 결과를 출력할 View로 forwording
  • 결과를 클라이언트에게 출력
//import 생략

public DispatcherServlet extends HttpServlet{
	// 매핑 및 경로분석 객체 멤버로 보유
	HandleMapping handleMapping;
    ViewResolver viewResolver;
	
    public void init() throws ServletException{
    	// 멤버 인스턴스화
		handleMapping = new HandleMapping();
        viewResolver = new ViewResolver();
        // view경로의 접두사와 접미사 세팅
        viewResolver.setPrefix("/");
        viewResolver.serSuffix(".jsp");
    }
   	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doRequest(request, response);
	}

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doRequest(request, response);
	}
    
    // 공통수행 메서드
    protected void doRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		// 요청 객체 인코딩
        request.setCharacterEncoding("utf-8");
        
        // 요청 유형 분석
        String uri = request.getRequestURI(); // 요청 URI
        // URI내에서 key값으로 사용할 경로 추출
        String path = uri.substring(uri.lastIndexOf("/"));
        
        // 컨트롤러 매핑 및 요청 위임
        // 이 시점에서 위임받은 컨트롤러가 model에 로직 수행을 지시하고 결과를 반환
        Controller controller = handleMapping.getController(path); //key값 전달
        String viewName = controller.handleRequest(request, response);
        
        // 최종 결과를 출력할 view를 forwording
        String view = null; //view 경로를 담을 변수
        if(!viewName.contains(".do")){ //저장할 결과가 있다면 조합된 경로 대입
        	view = viewResolver.getView(viewName);
        } else{ //저장할 결과가 없다면 현재 viewName 대입
        	view = viewName;
        }
        //결과 출력 view로 forwording
        RequestDispatcher dis = request.getRequestDispathcer(view);
        dis.forword(request, response);
	}
}

이렇게해서 Front Controller까지 코드를 작성하여 모든 코드가 완성되었다. 아래는 요청을 보내는 blood의 send.jsp이다.

🖥️ send.jsp(요청)

<%@ page contentType="text/html;charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div>
      <form name="form1">
          <select name="blood">
              <option value="A">A</option>
              <option value="B">B</option>
              <option value="O">O</option>
              <option value="AB">AB</option>
          </select>
      </form>
      <button onClick="send()">분석</button>
	</div>
</body>
<script>
const send = () => {
  form1.action="/blood.do";
  form1.method="POST"
  form1.submit();
}
</script>
</html>

form을 통해 요청하는 action 경로는 "/blood.do"이다. 여기서 .do를 DispatcherServlet이 인식하고 요청을 수락하는 것이다. 요청을 보내는 페이지와 결과를 출력하는 페이지의 모습을 아래를 통해 확인하자.

🖥️ 요청 전송

🖥️ 결과 출력

결과의 주소를 보면 action으로 요청한 주소가 그대로 유지되는 것을 볼 수 있다. 이것은 forwording을 통해 result.jsp로 요청 및 응답 객체를 전송하고, 해당 페이지에서 표현식으로 데이터를 출력한 뒤 웹 컨테이너에 의해서 응답을 클라이언트에게 전송했기 때문이다. 이제 마지막으로 위의 코드를 모두 보았으니 최종 로직을 아래 그림을 통해 확인해보자.

이렇게해서 Front Controller 패턴을 적용한 MVC 프레임워크에 대하여 알아보았다. 대량의 요청을 접수하여 요청 유형별로 하위 컨트롤러에게 위임하는 역할 덕분에 모든 컨트롤러가 Servlet으로 정의될 필요없어 졌으며, 해당 요청에 대한 로직을 수정할 경우 해당 컨트롤러만 수정하면 되기에 유지보수 측면에서도 훨씬 가벼워진 것이다. 위 코드가 이해가 되지 않는다면 이전 게시글에서 예제를 한번 작성하고 넘어오면 어느정도 이해가 될 것이다.


마무리

Front Controller 패턴은 현실세계에서 대형 서비스센터와 유사한 개념이라고 할 수 있다.

  • 고객이 전화를 걸면 안내원이 전화를 받음.
  • 요청하는 서비스부서로 안내원이 고객의 전화를 위임.
  • 해당 부서에서는 고객의 불편사항에 대한 처리를 수행.
  • 고객의 불편사항이 처리되면 결과를 고객에게 전달.

이렇게 중앙에서 컨트롤러들에게 요청을 위임하고 처리된 결과를 반환받아주다보니 개발하는 입장에서는 요청을 분석하여 어떻게 처리해야하는지만 고민하면된다.

느낀점

  • 이제까지는 요청을 처리할 때 조건만을 판단하여 조건에 따른 처리만을 생각했었다. 하지만 조건보다는 요청에 적합한 객체를 매핑하고, 해당 객체에게 요청을 위임하여 처리하는 방법이 더 유연하고 개발자가 개입해야하는 요소가 더 적어짐을 경험함.
  • 각 객체간의 결합도가 낮아져 하나의 객체를 수정하더라도 다른 객체가 받는 영향이 감소한다는 것을 느꼈음.
  • 객체를 항상 로직을 수행하는 부분에서 인스턴스화(생성)하여 사용하였으나 생성된 객체를 상황에 맞게 호출하여 사용하는 것에 대해 알게됨.

이상으로 마무리하겠다.👊🏽

profile
서핑하는 개발자🏄🏽

0개의 댓글