📖애플리케이션 계층의 요청이 발생할 경우 가장 먼저 클라이언트의 요청을 받고 요청 유형에 맞는 컨트롤러들에게 위임하는 방식
아래의 MVC 구조는 클라이언트로부터 하나의 요청이 발생할 경우에 대한 것을 그림으로 표현한 것이다. 만약 요청이 하나가 아닌 대량으로 들어오고, 요청수만큼 컨트롤러가 사용된다면 어떨지 생각해보자.
이전 게시글에서 알아보았던 MVC 모델의 핵심 목적은 유지보수
를 원활하게 하는 것이다. 하지만 위에서 말한 것처럼 요청에 1:1로 대응하는 컨트롤러를 사용할 경우 컨트롤러가 너무 많아져 오히려 분리로 인해 유지보수가 어려운 상황이 발생할 수 있다.
이러한 점을 보완하기 위해 Front Controller 패턴
을 통해 요청이 들어올때마다 유형을 분석해 해당 요청에 적합한 컨트롤러에게 요청을 위임
하는 방법을 알아보자.
Front Controller 패턴을 사용하기 전에 우선 MVC 모델에서 컨트롤러의 역할에 대해서 다시 한번 알아보고 넘어가자.
요청을 받는다.
유형을 분석
한다.로직 수행을 지시
한다.결과를 저장
한다.결과를 출력
한다.이러한 역할 수행을 하기 위하여 컨트롤러들은 각각 요청 유형을 분석해서 모델에게 요청 처리에 알맞는 로직 수행을 지시하게 된다. 다음은 Front Cotroller 패턴을 사용할 경우 나눠진 역할이다.
요청을 받고 유형을 분석
요청 위임
model에게 지시
결과를 저장
Front Controller
를 경유하여 view
로 전달다음 그림은 Front Controller 패턴을 적용한 MVC 구조이다. 각 객체의 역할을 알아보자.
클래스명 | 내용 |
---|---|
DispatcherServlet | Front Controller로서 클라이언트의 요청을 가장 먼저 받고 하위 컨트롤러에게 요청 위임 |
HandleMapping | 요청 URI을 분석하여 요청에 적합한 컨트롤러를 매핑하여 반환 |
ViewResolver | 컨트롤러가 반환한 String을 분석하여 JSP 경로 지정 |
Controller | 클라이언트의 요청을 실질적으로 처리하는 객체 자료형 |
중복코드
발생 방지대량 요청
처리 가능컨트롤러 매핑
을 통해 코드 간소화위와 같은 장점들로 인해 Spring이나 Structs 같은 MVC 프레임워크에서도 동일한 패턴을 사용한다. 이제 본격적으로 MVC 프레임워크를 따라 만들어보자.
Controller 인터페이스는 Front Controller로부터 위임되는 요청을 분석하여 실질적인 요청 처리를 하기 위한 컨트롤러들을 같은 타입의 구현객체로 관리하기 위하여 정의한다.
public interface Controller(){
//모든 구현객체에서 공통적으로 사용할 추상메서드 선언
String handleRequest(HttpServletRequest request, HttpServletResponse response);
}
인터페이스 정의가 완료되었으니 구현 객체인 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 경로를 반환한다. 그렇다면 요청에 맞는 컨트롤러를 매핑하기 위한 객체를 생성하여 보자.
해당 객체는 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의 경로를 어떻게 완성시키는지 알아보자.
해당 객체는 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의 코드를 보면서 어떤 원리로 동작하는지 알아보자. 눈 크게 뜨고 보길 바란다.
코드를 보기전에 전체적인 로직 수행 순서를 간략히 정리하겠으니 훑어보고나서 코드를 보자.
init()
을 통해 HandleMapping, ViewResolver 객체 생성doXXX()
에 공통된 메서드 입력URI 분석
컨트롤러 매핑 및 요청 위임
로직 수행 지시
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이다.
<%@ 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 패턴은 현실세계에서 대형 서비스센터와 유사한 개념이라고 할 수 있다.
이렇게 중앙에서 컨트롤러들에게 요청을 위임하고 처리된 결과를 반환받아주다보니 개발하는 입장에서는 요청을 분석하여 어떻게 처리해야하는지만 고민하면된다.
이상으로 마무리하겠다.👊🏽