1. MVC 패턴의 기본 개념

MVC는 Model-View-Controller의 약자로 웹 어플리케이션에서 주로 사용되는 디자인 패턴이다.

웹 어플리케이션은 클라이언트의 요청이 들어오면 비즈니스 로직을 수행한 후 HTML 파일을 생성해 다시 돌려줘야 한다. 

MVC 패턴은 클라이언트 요청 받기, 비즈니스 로직 수행, 화면 생성이라는 각 책임을 분리한 디자인 패턴이다.

 

Controller는 클라이언트의 요청을 받아 url과 파라미터를 검증하고, 비즈니스 로직을 호출하거나 직접 실행한다.

Model은 화면을 그리는데 필요한 데이터들을 저장하고 전달하는 역할을 맡는다.

View는 Model의 데이터를 이용해 화면을 렌더링하는 일을 맡는다.

 

핵심은 Controller과 View의 역할 분리이다.

화면을 그리는 일과, 비즈니스 로직을 수행하는 일은 변경주기가 크게 다르다.

화면은 간단하게 자주 수정되는 반면, 비즈니스 로직은 변경이 적다.

 

비즈니스 로직과 화면 렌더링이 같은 파일에서 진행될 경우, 코드가 복잡해지고 가독성이 떨어진다.

코드 수정도 불편해지며, 화면 렌더링 코드 변경 도중 비즈니스 로직을 잘못 건드릴 경우 큰 문제가 생길 수 있다.

 

2. 서블릿과 JSP를 사용해 구현해본 MVC 패턴 + 문제점

강의에서 Servlet을 컨트롤러로, JSP를 뷰로, HttpServletRequest를 모델로 사용해 MVC 패턴을 구현했다.

//Servlet을 이용한 Controller
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
    
    private MemberRespository memberRepository = new MemberRepository.getInstance();
    
    @Override
    protected void service(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);
        System.out.println("member = " + member);
        memberRepository.save(member);
        
        // Model에 데이터 보관.
        request.setAttribute("member", member);
        
        //View(JSP) 호출
        String viewPath = "/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
<!-- /views/save-result.jsp -->
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
  성공
  <ul>
    <li>id=${member.id}</li>
    <li>username=${member.username}</li>
    <li>age=${member.age}</li>
  </ul>
  <a href="/index.html">메인</a>
</body>
</html>

회원 생성이라는 비즈니스 로직은 Controller에서, 화면 렌더링은 JSP에서 담당하게 됐다.

그러나 이러한 방식도 컨트롤러가 늘어나면 문제가 발생한다.

 

1. 컨트롤러에서 코드 중복이 너무 많음.

View 호출 코드는 모든 컨트롤러에서 수행되어야 한다.

또한 뷰 호출 경로를 컨트롤러에서 직접 입력해야하는데, View 파일 경로가 바뀔 경우 모든 컨트롤러의 경로를 직접 수정해야한다.

 

2. 컨트롤러가 HttpServlet에 의존적

HttpServlet와 request, response 등을 직접 만들어야 하므로 테스트 코드를 만들기 힘들어진다.

또 HttpServletResponse 등 컨트롤러에서 필요 없는 코드가 계속 사용된다.

 

결론은 공통 처리가 어려워지고, 테스트가 힘들어진다는 문제가 생겼다.

 

3. 문제점 해결 → 프론트 컨트롤러의 도입

공통 처리가 어려워지는 문제를 해결하기 위해선 프론트 컨트롤러를 만들면 된다.

 

모든 요청을 프론트 컨트롤러에서 받아와 공통 로직을 모두 수행한 이후,

개별 컨트롤러를 호출에 요청에 맞는 비즈니스 로직을 수행시키면 된다,

 

개별 컨트롤러에 HttpSevlet을 전달하는 것이 아닌,

필요한 정보(파라미터)만을 추출해 전달하면 컨트롤러의 HttpServlet 의존성을 제거할 수 있다.

//프론트 컨트롤러
@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 req, HttpServletResponse res) throws ServletException, IOException {
		//요청에 맞는 컨트롤러 찾기
		String reqURI = req.getRequestURI();
		ControllerV1 controller = controllerMap.get(reqURI);
		
		//필요한 데이터 추출 로직
		Map<String,String> paramData = getParamData(req);
        
		//컨트롤러 로직 호출
		controller.process(paramData);
	}
}

4. 유연한 컨트롤러 사용 → 어댑터 패턴 사용

위의 코드와 같이 프론트 컨트롤러를 구현하면, ControllerV1이 아닌 다른 종류의 컨트롤러를 사용할 수 없다.

다양한 인터페이스로 만들어진 컨트롤러를 모두 사용할 수 있는 프론트 컨트롤러를 만들기 위해선 어댑터 패턴을 사용하면 된다.

예재의 패턴
실제 스프링 mvc 패턴

1. 클라이언트의 요청

2. 클라이언트의 요청을 처리할 수 있는 컨트롤러 있는지 확인

3. 컨트롤러(핸들러)를 처리할 수 있는 핸들러 어뎁터가 있는지 확인

4. 핸들러 어뎁터에 핸들러 처리를 맡김 -> 비즈니스 로직 수행 후 View 반환

5. 뷰에서 화면 만들어 응답.

 

순으로 진행된다.

핸들러 어뎁터는 컨트롤러에 맞춰서 요청 정보를 전달하고, 컨트롤러의 응답을 View로 변환하는 역할을 맞는다.

여기까지 구현한 mvc 패턴의 구조는 Spring MVC 패턴과 매우 유사한 구조를 띄게 된다.

 

실제 Spring MVC 패턴에선

프론트 컨트롤러 -> Dispatcher Servlet

viewResolver, View를 인터페이스화 -> 확장성 증가

정도의 차이만 존재한다.

 

1. 클라이언트의 요청

2. 클라이언트의 요청을 처리할 수 있는 컨트롤러가 있는지 확인 (어노테이션 기반 핸들러, 빈 이름으로 조회 순서)

3. 컨트롤러(핸들러)를 처리할 수 있는 핸들러 어뎁터가 있는지 확인 (어노테이션 기반 핸들러 어뎁터, HttpRequestHandler 순서)

4. 핸들러 어뎁터에 핸들러 처리를 맡김 -> 비즈니스 로직 수행 후 ModelAndView 반환

5. ViewResolver에서 view 이름으로 View 찾아옴(동일한 이름의 빈 있는지, application.properties 설정에서 경로 찾기)

6. View에서 화면 만들어 응답

 

 

5. 실무에서 mvc를 활용할 때

실무에선 controller를 만들때 그냥 @Controller 어노테이션을 사용한다.

@Controller
public SpringMvcController{
	private MemberRepository memberRepository = MemberRepository.getInstance();
    
	@PostMapping("/save")
	public String createMember(
		@RequestParam("username") String username,
		@RequestParam("age") int age,
		Model model) {
    
		//비즈니스 로직
		Member member = new Member(username, age);
		memberRepository.save(member);
    
		//모델에 값 추가
		model.addAttribute("member",member);
	
		//view 이름만 반환
		return "save-result";
	}
}

mvc 패턴에 대해 자세히 알고 나니 다르게 보이는 것들이 조금 있다.

 

1.  @Controller의 역할

컨트롤러 어노테이션은 단순히 빈 등록만 할 뿐 아니라 어노테이션 기반 핸들러라는 것을 등록함

 

2.  @RequestParam 의 역할

form 방식으로 넘어오는 데이터가 get방식의 파라미터 형식과 똑같이 넘어온다.

그래서 get, post 둘다 RequestParam을 사용 가능.

 

3. model 과 getAttribute의 기능

HttpServletReqeust에 setAttribute()를 구현해놓은 것.

그래서 model을 받아와서 addAttribute로 정보만 입력하고 다시 반환하지 않아도 된다.

Handler에서 HttpServletRequest에 담겨있는 정보를 다시 ModelAndView로 전달해줄거니까.

 

4. 왜 Controller가 파일 경로나 클래스가 아닌 String을 반환하도록 만들어졌을까

String만 간단하게 반환해도 ViewResolver가 view 이름으로 알아서 View를 찾아주니까 가능한거였다.

 

6. 번외 - 장고의 MTV 패턴과 비교

장고는 이러한 MVC 패턴을 MTV(Model-View-Template) 패턴으로 지원한다.

 

1. Model

장고에서 Model은 화면 정보를 저장하고 전달하는 것이 아니다.

Django ORM을 이용해 DB와 직접적으로 연결되며, 하나의 테이블 정보를 저장하고 있기도 하다.

 

2. View

장고의 View는 MVC 패턴의 View가 아닌 Controller의 역할을 한다!!!

왜 이렇게 이름을 헷갈리게 지었는지는 잘 모르겠지만, 장고의 View는 비즈니스 로직을 수행하고 템플릿을 호출한다.

 

3. Template

장고의 Template가 View역할을 맡아 화면을 렌더링하는 역할을 맡는다.

또 자체척인 Template 문법을 지원해 context(MVC패턴의 모델 역할)에서 데이터를 받아와 렌더링한다.

 

7. 리팩토링 할때 알면 좋은 점

  1. 변경 주기가 다르면 분리하자
  2. 큰 메소드 사이에 작은 구현 → 메소드로 분리하자
  3. 리팩토링시 전체적인 구조 개선이 필요할 경우, 기존 구현 코드를 재활용하며 우선 구조부터 개선하자.
    1. 구조 개선에 대한 커밋과 테스트를 진행하고 나서 세부 구현 코드를 수정하자.

 

+ Recent posts