서블릿과 JSP를 통해 컨트롤러와 뷰를 분리했었다. 하지만 이것으로도 완벽하지 않다. 서블릿의 코드 안에서는 계속해서 request로부터 dispatcher를 꺼내서 forward메소드를 호출해줘야 한다. 이는 매우 번거로운 작업이다. 또한 뷰 타겟이 될 jsp파일의 경로도 java코드에 박혀있기 때문에, 만약 jsp대신 Thymeleaf같은 다른 템플릿엔진을 사용하려고 한다면 모든 java코드를 다 뜯어고쳐야 한다. 여러분은 앞으로 많은 웹 서비스를 개발해야 할텐데, 이러한 상황에서 만족할 수 있겠는가?
service(request, response){
param1 = request.getParam("param1");
//비즈니스 로직 작성
request.setAttribute("모델데이터", "데이터")
Dispatcher = request.getDispatcher("jsp파일의 경로");
dispatcher.forward();
}
지금 서블릿 코드의 구성을 보면, 먼저 첫번째로 request 객체로부터 파라미터를 추출한다. 그리고 이를 바탕으로 비즈니스로직을 수행한다. 여기까지는 모든 컨텐츠마다 다를 것이므로 굳이 공통화할 부분이 없다.
하지만 그 뒤는 어떨까? 우선 request에 데이터를 욱여넣고 이것을 모델로 뺐다고 주장하는것 부터가 기분 나쁘다. 이렇게 했기 때문에, request로부터 RequestDispatcher를 받아서 forwar 메소드를 호출해서 특정한 jsp파일로 전달해줘야 했다. 또한, 애초에 저렇게 전달해줘야 할 jsp파일의 경로도 코드에 박혀있다.
우선 쉬운것부터 통합해보자. 일단 각각의 컨텐츠마다 서블릿을 구현하는게 너무 무겁다. 비즈니스로직을 담당하는 '컨트롤러'들을 별도의 java코드로 구현하고, 서블릿은 하나만 만든 뒤 서블릿 내의 비즈니스로직 호출 부분에서 요청에 따라 적절한 컨트롤러를 불러주기만 하면 어떨까? 즉, 서블릿은 컨트롤러들의 입구 역할을 하는 '프론트 컨트롤러'가 되고, 비즈니스로직들은 각각의 컨트롤러에 구현한다.
*이렇게 서블릿을 입구 역할로 빼는 패턴을 Front Controller 패턴이라고 한다. 참고로 이렇게 빠진 컨트롤러를 Spring프레임워크에서는 DispatcherServlet이라고 부른다. 관련 공식 문서 : https://docs.spring.io/spring-framework/docs/2.0.x/reference/mvc.html
//프론트 컨트롤러
service(request, response){
controllerMap[request.URL](request, response); //미리 등록해둔 컨트롤러 맵의 로직 실행
}
//각각의 컨텐츠마다 컨트롤러를 작성
controller(request, response){
//비즈니스 로직
request.setAttribute("모델데이터", "데이터")//비즈니스로직 결과 뷰에서 그려줄 값
request.dispatcher.forward("jsp파일 경로")
}
로직 자체는 아직 별다른 차이가 없어 보인다. 아직은 시작에 불과하다. 이를 시작으로 많은것을 개선할 것이다.
하지만 아직은 부족하다. 모든 컨트롤러들에서 마지막에 request로부터 dispatcher를 가져와서 request에 욱여넣은 데이터들을 forward 해줘야 한다. 그리고 이때 대상이 되는 jsp파일의 경로가 하드코딩 되어있는 문제도 여전히 가지고 있다.
이 문제를 해결하기 위해, View객체를 따로 만든다. 이 객체는 대상이 될 뷰 파일의 경로(jsp 파일을 사용하는 경우 jsp파일의 경로)를 속성으로 가지고 있으며, render 메소드같은걸 하나 만들어서 dispatcher forward로직을 여기에 집어넣는다.
그리고, 각각의 컨트롤러에서는 뷰 경로를 지정하고 request의 데이터를 저 경로로 포워딩 하는 로직을 빼버리고, 그냥 View객체를 생성해서 리턴하면 된다.
class View {
String path;
render(request, response){
request.dispatcher.forward(path);
}
}
service(request, response){
View view = controllerMap[request.URL](request, response);
view.render(request, response);
}
controller(request, response){
//비즈니스 로직
request.setAttribute("모델데이터", "데이터")//비즈니스로직 결과 뷰에서 그려줄 값
return new View("jsp경로");
}
위 로직을 보면, 컨트롤러에서 굳이 request와 response를 받아야 하는지 의문이 든다. 물론 파라미터 정보는 필요하지만, 꼭 HttpServletRequest로 받아야 할까? 그냥 Map으로 받을 수 있다면 테스트를 자동화 하는데도 큰 도움이 된다. 지금은 컨트롤러를 테스트하려면 임의로 HttpServletRequest를 만들어서(혹은 Mock객체를 사용해서) 테스트해야 한다. 이를 개선해보자.
그리고 모델을 request.setAttribute로 욱여넣는것도 보기 싫으니, 이를 뷰에 합쳐보자. 어차피 모델은 뷰에서 그리기 위해 필요하기 때문이다.
ModelView {
String path;
Map model;
render(request, respone){
request.setAttribute(model);//controller에 있던 로직이 이 공통 클래스로 옮겨왔다!
request.dispatcher.forward(path);
}
setModel(key, value){
model.set(key, value)
}
}
service(request, response){
Map paramMap <- request.param
modelview = controllerMap[request.URL](paramMap);
modelview.render(request, response);
}
controller(paramMap){
//비즈니스 로직
ModelView modelView = new ModelView("jsp경로")
modelView.setModel('jsp에 그리고 싶은 값들')//비즈니스로직 결과 뷰에서 그려줄 값
return modelView
}
이정도만 되어도 많은것이 개선되었다. ModelView클래스와 서블릿(service메소드)은 한번만 개발하면 되는 것이기 때문에, 각각의 컨텐츠들에 대한 비즈니스로직은 controller에 편하게 작성할 수 있다. 하지만 아직 거슬리는 것들이 두가지 남았다. 하나는 controller안에 jsp경로가 하드코딩 되어있어서, 템플릿 엔진을 바꾸면 모든 컨트롤러를 수정해야 한다는 것이다. 다른 하나는 모든 컨트롤러마다 매번 마지막에 ModelView객체를 생성해서 리턴해야 한다는 것이다.
우선 간단한 문제부터 해결해보자. jsp경로가 하드코딩되어 있다는 문제 말이다. 이렇게 하는 대신, 뷰의 논리적 이름을 키로 하고 실제 경로를 값으로 하는 맵을 하나 만들자. 그리고 이걸 '뷰 리졸버'라는 녀석이 들고 있으면서, 각 컨트롤러는 이녀석에게 요청해서 실제 경로를 받아가게 하면 된다. 그러면 템플릿 엔진을 교체하더라도, 뷰리졸버의 맵만 수정하면 컨트롤러들은 건드릴 필요가 없다.
ModelView {
String path;
Map model;
render(request, respone){
request.setAttribute(model);//controller에 있던 로직이 이 공통 클래스로 옮겨왔다!
request.dispatcher.forward(path);
}
setModel(key, value){
model.set(key, value)
}
}
service(request, response){
Map paramMap <- request.param
modelview = controllerMap[request.URL](paramMap);
modelview.render(request, response);
}
controller(paramMap){
//비즈니스 로직
ModelView modelView = new ModelView(ViewResolver.getViewPath("content1"))
modelView.setModel('jsp에 그리고 싶은 값들')//비즈니스로직 결과 뷰에서 그려줄 값
return modelView
}
ViewResolver {
map = {
//키는 논리적 이름(불변)
//값은 물리적 경로(템플릿 엔진에 따라, 혹은 경로 변경에 따라 변함)
"content1": "/path/context1.jsp"
"content2": "/content2.jsp"
}
getViewPath(key){
return map[key]
}
}
이제 조금 더 복잡한 문제를 해결해보자. 모든 컨트롤러가 마지막에 ModelView객체를 생성해서 리턴해야 하는 문제 말이다. 이렇게 하지 말고, 이 역할을 공통 모듈인 뷰 리졸버에게 미뤄버릴 수 없을까?
컨트롤러에서 직접 모델뷰를 생성해서 리턴하지 말고, 컨트롤러는 그냥 뷰의 논리적 이름만 리턴하고 뷰 리졸버가 이를 받아서 적절한 모델뷰를 생성해 리턴하도록 말이다. 물론 이렇게 하면 컨트롤러에서 modelView.setModel메소드를 호출할 수 없으므로, 컨트롤러에서 모델에 값을 집어넣을 다른 방법이 필요하다. 이건 그냥 서블릿(service메소드)에서 제공하자.
ModelView {
String path;
render(model, request, respone){
request.setAttribute(model);//controller에 있던 로직이 이 공통 클래스로 옮겨왔다!
request.dispatcher.forward(path);
}
}
service(request, response){
Map paramMap <- request.param
Map model = new Map();
String viewName = controllerMap[request.URL](paramMap, model);
modelview = ViewResolver.getModelview(viewName);
modelview.render(model, request, response);
}
controller(paramMap, model){
//비즈니스 로직
model.set('jsp에 그리고 싶은 값들')//비즈니스 로직의 결과 보여주고 싶은 값들
return "content1"//그냥 뷰의 논리적 이름만 반환한다.
}
ViewResolver {
map = {
"content1": "/path/context1.jsp"
"content2": "/content2.jsp"
}
getModelview(name){
return new ModelView(map[name]);
}
}
이렇게 하면, 이제 앞으로 컨텐츠들이 추가될 때는 컨트롤러만 추가하면 된다. 그리고 여기에는 오로지 비즈니스 로직만 작성하면 되며, 마지막으로 응답으로 내려줄 웹문서의 논리적 이름만 리턴해주면 된다.
7. 뷰 템플릿 (0) | 2021.10.22 |
---|---|
6. HTML을 작성하기가 너무 불편하다. (0) | 2021.10.22 |
4. 구매완료 페이지 보내주기 (0) | 2021.10.21 |
3. 웹 서버와 서블릿 (0) | 2021.10.21 |
2. 당신의 회사에서 웹 서비스를 제공해보자. (0) | 2021.10.20 |