
저번 글에서 MVC패턴이란 무엇이며 왜 사용되는지 쭉 알아보았었고 View를 분리해내고 구현 컨트롤러에서는 View에 주소만 리턴해주고 렌더링은 프론트 컨트롤러에서 처리하도록 설계해봤었다. 프론트 컨트롤러는 클라이언트의 요청에 맞는 구현 컨트롤러를 매핑처리해서 호출해주고 호출하는 과정은 다형성을 통해 유지보수에 효과적이도록 설계했다.
MVC패턴과 변천사 - 1
이전 글에서 MVC패턴 등장계기는 JSP를 사용할 때, 비즈니스 로직코드인 Java코드와 HTML코드가 한 파일안에 같이 있기때문에 유지보수에 어려움을 겪었고 이 문제를 해결하기위해 MVC패턴을 사용한
lee-dev-log.tistory.com
Model 도입
Model은 클라이언트로부터 요청받은 데이터나 서버(DB서버)로부터 제공해야할 데이터를 Model안에 넣어서 View로 전달해주는 역할을 하며 View로 전달된 Model안에 들어있는 데이터는 UI에서 클라이언트에게 표현될 때 사용된다.
지금까지 알아 본 MVC패턴에서는 Model의 역할을 request객체의 setAttribute메서드를 통해서 수행했다. 이제 본격적으로 Model이라는 개념을 추가해서 어떤식으로 프론트 컨트롤러에서 뷰로 Model을 전달하는지 살펴보자.
이전에는 MyView라는 클래스를 만들어서 컨트롤러 구현체에서는 View를 반환하도록 설계하여 반환된 View를 프론트 컨트롤러에서 받아 렌더링 하도록 설계했었다.
Model을 설계하기위해서는 View를 반환하는 것이 아닌 ModelView라는 클래스를 만들어서 이 클래스타입을 반환하도록 설계해야한다. 코드로 먼저 살펴보자
@Getter @Setter //lombok사용
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
ModelView클래스의 멤버변수를 살펴보자면,
viewName
viewName은 반환할 viewPath를 담고있으며 나중에 ModelView.getViewName을 통해 이전 MVC에서 View를 반환했던 것과같이 어떤 view를 반환할지 결정해주게된다.
한마디로 이전 MVC패턴에서 View객체 자체를 반환했지만 ModelView는 viewName이라는 필드로 반환하는 것이다.
model
이전에는 이 model멤버변수의 역할을 request.setAttribute를 통해 처리했었다.
Member member = new Member(name, age);
request.setAttribute("member", member)
위와 같이 코드를 사용했던 것을 떠올려보자 setAttribute의 파라미터를 보면 (String, Member)인 것을 확인할 수 있는데
String타입으로 값을 꺼내올 키를 정의하고 member라는 값을 넣어준다.
이 역할을 수행하는게 바로 model멤버변수로 Map컬렉션으로 설계되고 String의 키와 Object의 값을 갖는다.
밸류에는 member가 들어올 수도있고 다른 참조타입이 들어올 수 있기 때문에 모든 클래스의 최상위 클래스인 Object로 밸류타입을 설정하는 것이다.
request의 역할을 수행하는 paramMap
컨트롤러를 구현할 인터페이스로 ModelView타입을 반환하며 파라미터로는 <String, String>제너릭을 갖는 Map을 갖는다
(이하 paramMap)
public interface Controller {
ModelView process(Map<String, String> paramMap);
}
paramMap의 제너릭이 String, String인 이유는 HTTP요청은 모두 String타입으로 들어오기때문이다.
후에 첫번째 String은 "name"의 key를 갖고, 두번째 String은 "hong"이라는 value를 갖게된다.
age와 같은 int타입은 "age"라는 key값과 "20"이라는 실제나이 입력데이터를 String으로 받지만 구현 컨트롤러에서 파싱처리하여 사용된다.
!!!!!!!!!중요!!!!!!!!!!!!!
paramMap과 Model
필자는 이 부분의 개념이 정확하게 잡히지 않았던 것 같아서 이해하는데 어려움을 겪었던 것 같다.
paramMap은 이전에도 언급했듯이 request의 역할을 대신 수행한다.
//request로 요청을 처리했던 코드 String name = request.getParameter("name"); int age = Integer.parseInt(request.getParamter("age")); Member member = new Member(name, age);
//Map<String, String> paramMap으로 요청을 처리한 코드 String name = paramMap.get("name"); //hong int age = Integer.parseInt(paramMap.get("age")); //20 Member member = new Member(name, age);
paramMap은 요청받은 데이터 즉, HTML form태그로 입력받거나 HTTP message body를 통해 넘어온 쿼리파라미터 형식의 문자열을 처리해주는 역할을 수행할 뿐이며 Model과는 다른 역할을 수행합니다.
Model은 FrontController에서 View로 데이터를 보낼때 데이터를 담고있는 바구니정도로 생각하면 이해에 도움이 될거라고 생각합니다.
즉 Model객체에는 비즈니스로직을 수행한 데이터의 결과를 Model에 담아서 View로 전송하고 View에서는 이 Model에 담긴 데이터를 UI로 표시해줍니다.
아래 그림으로 정리해보겠습니다.먼저 전체적인 흐름을 다시한번 살펴보자면
1. 클라이언트에서 HTML Form태그를 통해 "name", "age"를 입력
2. 프론트 컨트롤러에서 request.getParameter를 통해 "name", "age"값을 꺼내 paramMap에 입력
private Map<String, String> createParamMap(HttpServletRequest req) { Map<String, String> paramMap = new HashMap<>(); req.getParameterNames().asIterator() .forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName))); return paramMap;
3. "name", "age"값이 입력된 paramMap을 비즈니스 로직컨트롤러로 전달(saveController)
controller.process(paramMap); //실제 코드는 아래와 같음 이해하기 쉽기위해 위에 코드만 볼 것 //ModelView modelView = controller.process(paramMap);
4. 비즈니스 로직 컨트롤러에서는 전달받은 paramMap을 Model로 변환처리한다.
String name = paramMap.get("name"); //hong int age = Integer.parseInt(paramMap.get("age")); //20 Member member = new Member(name, age);
여기서 member가 바로 object타입
5. 변환한 Model을 프론트 컨트롤러로 반환한다.
modelView.getModel().put("member", member); return modelView;
여기서 model이 <String, Object> 제너릭을 갖는지 이해할 수 있다.
비즈니스 로직 컨트롤러에서 ModelView를 반환하기 때문에 프론트 컨트롤러에서 pramMap을 model로 변환요청을 할 때 ModelView타입의 변수로 반환받는 것ModelView modelView = controller.process(paramMap);
6. 프론트 컨트롤러에서 MyView객체를 생성하고 MyView에 생성자 파라미터로 ModelView에 담긴 viewPath를 준다.
MyView view = new MyView(modelView.getViewName());
7. 프론트 컨트롤러에서 view로 모델을 전달하고 view에서는 렌더링된다.view.rend(modelView.getModel(), req, resp);
8. MyView에서 model에 있는 데이터를 setAttribute메서드를 통해 key-value형태로 처리하고 forward한다.
public void rend(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { modelToRequestAttribute(model, request); RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response); } private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) { model.forEach((key, value) -> request.setAttribute(key, value)); }
본론으로 돌아와서 그림에서 1,2,3,4숫자가 있는데
1의 상태에서는 쿼리파라미터 형식의 문자열로 name=hong&age=20의 문자열 형태이다.
2의 과정에서는 프론트 컨트롤러를 통해 paramMap형태로 변환되고
3,4에서는 비즈니스 로직처리 과정에서 model형태로 바뀌게 된다.
request와 paramMap의 차이를 설명하려고 했을뿐인데 Model의 도입과정을 모두 다 설명해버린듯하다..
ViewResolver를 도입하면서 전체코드를 정리해보겠다.
ViewResolver 도입
public ModelView process(Map<String, String> paramMap) {
return new ModelView("/WEB-INF/views/new-form.jsp");
}
ModelView modelView = new ModelView("/WEB-INF/views/save.jsp");
modelView.getModel().put("member", member);
return modelView;
ModelView modelView = new ModelView("/WEB-INF/views/members.jsp");
modelView.getModel().put("members", members);
return modelView;
현재 Controller구현클래스 즉 비즈니스 로직 컨트롤러는 ModelView가 생성될 때 viewPath필드에 경로의 모든 주소를 직접넣어서 반환해주게 된다.
이는 "/WEB-INF/views/"와 ".jsp"같이 중복코드가 발생할 뿐더러 후에 만약 /views의 경로가 바뀔경우에는 위 3개의 비즈니스 컨트롤러의 /views코드를 일일이 바뀐 경로로 수정해주어야하는 유지보수에 불편함이 생기게된다.
이러한 문제점을 해결하기위해 ViewResolver가 사용된다.
먼저 위의 3개 코드들에서 viewPath를 논리 뷰이름만 매개변수로 갖도록 설정해준다. (중복되는 부분을 제거)
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
ModelView modelView = new ModelView("save");
modelView.getModel().put("member", member);
return modelView;
ModelView modelView = new ModelView("members");
modelView.getModel().put("members", members);
return modelView;
위와 같이 수정만 해주더라도 가독성이 향상되는 것을 알 수 있다.
이제 FrontController에서 ViewResolver를 통해 중복된 코드들을 하나로 통합시켜보자
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
논리 뷰 이름을 파라미터로 받고 view를 생성해서 반환하는 함수를 생성하고 이 함수는 중복되어(고정되어) 사용되는 view의 경로를 문자열로 결합하고 파라미터로 받는 논리 뷰 이름을 결합시켜주면 끝이다.
리졸버 함수를 호출하고 View를 생성하며 이 View를 이용하여 렌더링을 하면된다.
String viewName = modelView.getViewName();
MyView view = viewResolver(viewName);
만약 유지보수 과정에서 views라는 경로가 바뀌더라도 뷰 리졸버에서 /views부분만 수정하면되므로 변경지점을 하나로 통합하여 유지보수성을 크게 향상시켰다.
ViewResolver함수 작성
처음에 ViewResolver를 혼자서 만들어 볼 때 아래와 같은 코드가 나왔는데 수강하는 강의 코드(위에서 설명한)와는 다르게 나왔는데
//호출 영역
String viewName = viewResolver(modelView);
MyView view = new MyView(viewName);
private String viewResolver(ModelView modelView) {
return "/WEB-INF/views/" + modelView.getViewName() + ".jsp";
}
차이점을 GPT에게 질문하니 아래와 같은 대답이 나왔다
두 설계 방식을 비교해보자면, 두 번째 설계 방식이 조금 더 효율적이라고 볼 수 있습니다. 그 이유는 다음과 같습니다.
첫 번째 설계 방식:
- ModelView 객체를 viewResolver 메서드의 파라미터로 전달하고, 이 메서드 내에서 ModelView 객체로부터 뷰 이름을 추출하여 최종적으로 뷰 경로를 생성합니다.
- 이 방식은 ModelView 객체가 뷰 경로 생성에 필요한 모든 정보(이 경우에는 뷰 이름)를 이미 가지고 있기 때문에 유효합니다.
두 번째 설계 방식:
- 뷰 이름을 먼저 ModelView 객체로부터 추출한 후, 이 뷰 이름을 viewResolver 메서드의 파라미터로 전달합니다. 그리고 이 메서드는 뷰 이름을 기반으로 새로운 MyView 객체를 생성하여 반환합니다.
- 이 방식은 viewResolver 메서드가 뷰 이름만을 파라미터로 받기 때문에, 뷰 이름을 기반으로 한 다양한 처리를 더 유연하게 할 수 있습니다. 예를 들어, 특정 뷰 이름에 대해 다른 로직을 적용하거나, 다른 형태의 뷰 객체를 생성하는 등의 확장성이 더 높습니다.
비교 및 결론:
두 번째 방식은 viewResolver 메서드의 역할을 더 명확하게 하고, 이 메서드가 단순히 뷰 이름을 기반으로 MyView 객체를 생성하고 반환하는 역할에 집중하게 합니다. 또한, 뷰 이름을 사용하는 다른 종류의 처리나 확장이 필요할 경우 더 유연하게 대응할 수 있습니다. 이러한 이유로 두 번째 설계 방식이 조금 더 효율적이라고 볼 수 있으며, 코드의 유지보수성과 확장성 측면에서도 이점을 가집니다.
간단히 정리하자면 viewResolver는 함수의 역할을 ViewPath를 논리 뷰 이름과 고정된 경로를 결합하여 View의 주소를 반환하는 역할을 하므로 ViewResolver함수 자체에서 경로를 결합함과 동시에 View를 생성하여 호출영역으로 반환하는 것이다.
이렇게하여 함수의 역할과 목표를 명확히하고 유지보수측면에서 효율적이라는 얘기이다.
위 정리한 내용들은 모두 인프런에서 김영한님의 강의를 듣고 학습한 내용을 스스로 정리한 것임을 밝힙니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1#
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!