본문 바로가기
스프링/스프링

[Spring] MVC 프레임워크를 따라 만들어보자.

by 책 읽는 개발자_테드 2021. 2. 25.
반응형

MVC 프레임워크 개발하기MVC 프레임워크를 따라 만들어보자.

 

이글은 Model1 구조와 Model2 구조를 예제 코드로 구현한 이전 글(scshim.tistory.com/271)의 코드를 개선하여 프로그램을 만든다.

 

이전 글(scshim.tistory.com/271)에서는 Model2 아키텍처를 간단히 만들기 위해 DispatcherServlet 하나로 Controller 기능을 구현했다. 하지만 이것은 클라이언트의 모든 요청을 하나의 서블릿이 처리한다는 문제점이 있다.

 

따라서 서비스가 복잡해지면, 수많은 분기 처리 로직으로 개발과 유지보수가 어려워 진다.

 

이러한 문제를 Spring(MVC)나 Structs 같은 MVC 프레임워크에서 제공하는 Controller를 사용하여 해결할 수 있다. MVC 프레임워크를 사용하면, 직접 Controller를 구현하지 않아도 된다.

 

Spring MVC를 사용하기 전에 이와 동일한 구조의 프레임워크를 직접 구현, 적용하여 Spring MVC의 구성 요소와 원리를 이해해보자.

 

다음은 여기서 구현할 MVC 프레임워크의 구조다.

 

 

✍각 클래스의 역할

 

클래스

기능

DispatcherServlet

유일한 서블릿 클래스. 모든 클라이언트의 요청을 가장 먼저 처리하는 Front Controller

HandlerMapping

클라이언트의 요청을 처리할 Controller 매핑

Controller

실질적인 클라이언트의 요청 처리

ViewResolver

Controller가 리턴한 View 이름으로 실행될 JSP 경로 완성 

 

이제 MVC 프레임워크에서 Controller를 구성하는 각 요소를 구현해보자.

 

1) Controller  인터페이스 작성 

Controller를 구성하는 요소 중 DispatcherServlet은 클라이언트의 요청을 가장 먼저 받아들이는 Front Controller다. 그리고 실질적인 요청 처리는 각 Controller에서 담당한다.

 

! 프론트 컨트롤러(Front Controller) 디자인 패턴이란?

더보기

하나의 대표 컨트롤러(프론트 컨트롤러)가 뷰에서 들어오는 모든 요청을 처리할 적절할 곳에 위임해주는 구조다. 스프링에서는 디스패처서블릿이 해당 패턴을 사용하여 만들어졌다.

 

 MVC 디자인 패턴에서 각각의 뷰에서 들어오는 요청 마다 서블릿을 만들어 처리하면 개발 효율이 너무 떨어지므로 필요한 패턴이다.

 

Controller 클래스들을 구현하기 전 모든 Controller를 같은 타입으로 관리하기 위한 인터페이스를 만들어야 한다. 클라이언트의 요청을 받은 DispatcherServlet은 HandlerMapping을 통해 Controller 객체를 검색하고, 검색된 Controller를 실행한다. 

 

이때 어떤 Controller 객체가 검색되어도 같은 코드로 실행하려면 모든 Controller의 최상위 인터페이스가 필요하다. 다음과 같이 모든 Controller의 최상위인  Controller 인터페이스를 만들자.

 

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface Controller {
	String handleRequest(HttpServletRequest request, HttpServletResponse response);
}

 

2) Controller 구현

 

Controller 인터페이스를 구현한 HelloController 클래스를 만든다. 그리고 다음 코드를 작성한다.

 

public class HelloController implements Controller {

	@Override
	public String handleRequest(HttpServletRequest request, HttpServletResponse response) {

		//사용자 정보 생성
		Users user = new Users("홍길동","010-0000-0000",28);

		//사용자 정보 생성 결과를 세션에 저장하고 화면을 리턴 
		HttpSession session = request.getSession();
		session.setAttribute("user", user);

		return "hello";
	}
}

 

3) HanderMapping 클래스 작성

 

HandlerMapping은 모든 Controller 객체들을 저장하고 있다가, 클라이언트의 요청이 들어오면 요청을 처리할 특정 Controller를 검색하는 기능을 제공한다.

 

HandlerMapping 객체는 DispatcherServlet이 사용하는 객체로, DispatcherServlet이 생성되고 init() 메소드가 호출될 때 단 한 번 생성된다.

 

WEB-INF 디렉토리에 EL을 적용하여 자바 코드를 제거한 hello.jsp 파일을 만든다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>

<body>
<h1>Hello World!</h1>
<p>${user.name}</p>
<p>${user.phone}</p>
<p>${user.age}</p>
</body>
</html>

✍jsp에서 자바 코드를 제거하기 위해 JSP에서 제공하는 EL, JSTL을 사용할 수 있다.

더보기

 

  • EL(Expression Language)은 JSP 2.0에서 새로 추가된 스크립트 언어로, 기존의 표현식을 대체한다. 예를 들어 세션에 저장된 사용자 이름을 ${username} 형태로 표현한다.

  • JSTL(JSP Standard Tag Library)는 Scriplet에서 if, for, switch 등의 코드를 태그 형태로 사용할 수 있도록 지원한다.

이전에 만들었던(scshim.tistory.com/271)세션에서 유저 정보를 가져오는 자바코드, User 클래스를 import하는 코드가 사라졌다!

 

여러 화면에 대한 요청을 처리하는 걸 확인하기 WEB-INF에 hello2.jsp 파일을 생성하자.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!222222</title>
</head>

<body>
<h1>Hello World!222222</h1>
</body>
</html>

 

그리고 HandlerMapping 클래스를 작성하자.

public class HadlerMapping {

private Map<String, Controller> mappings;

	public HandlerMapping() {

		mappings = new HashMap<String, Controller>();
		mappings.put("/hello.do", new HelloController());
		mappings.put("/hello2.do", new Hello2Controller());
	}

	// 매개변수로 받은 path에 해당하는 Controller 객체를 HashMap 컬렉션으로부터 검색하여 리턴
	public Controller getController(String path) {
		return mappings.get(path);
	}
}



4) ViewResolver 클래스 작성

 

ViewResolver 클래스는 Controller가 리턴한 View 이름에 접두사(preffix)와 접미사(suffix)를 결합하여 실행될 View 경로와 파일명을 완성한다. 

 

HandlerMapping과 같이 DispatcherServlet의 init() 메소드가 호출될 때 생성된다.

 

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;
	}

	public String getView(String viewName) {
		return prefix + viewName + suffix;
	}
}



5) DispatcherServlet 수정

 

Front Controller 기능을 하는 DispatcherServlet을 수정하자.

/**
 * Servlet implementation class DispatcherServlet
 */

public class DispatcherServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private HandlerMapping handlerMapping;
	private ViewResolver viewResolver;

	public void init() throws ServletException{

		handlerMapping = new HandlerMapping();
		viewResolver = new ViewResolver();
		viewResolver.setPrefix("./");
		viewResolver.setSuffix(".jsp");
	}

	/**
	* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	*/
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		process(request, response);
	}

	/**
	* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	*/
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	
		request.setCharacterEncoding("EUC-KR");
		process(request, response);
	}

	private void process(HttpServletRequest request, HttpServletResponse response) throws IOException{

		// 1. 클라이언트의 요청 path 정보를 추출
		String uri = request.getRequestURI();
		String path = uri.substring(uri.lastIndexOf("/"));

		// 2. HandlerMapping을 통해 path에 해당하는 Controller를 검색
		Controller ctrl = handlerMapping.getController(path);

		// 3. 검색된 Controller를 실행
		String viewName = ctrl.handleRequest(request, response);

		// 4.ViewResolver를 통해 viewName에 해당하는 화면을 검색
		String view = null;

		if(!viewName.contains(".do")) {
			view = viewResolver.getView(viewName);
		}else {
			view = viewName;
		}

		// 5. 검색된 화면으로 이동
		response.sendRedirect(view); 

	}
}

 

수정한 코드를 설명해보자. 

 

init() 메소드서블릿 객체가 생성된 후에 멤버변수를 초기화하기 위해 자동으로 실행된다. 따라서 init() 메소드에서 DispatcherServlet이 사용할 HandlerMapping, ViewResolver 객체를 초기화한다. 

 

그리고 DispatcherServlet은 이렇게 생성된 HandlerMapping과 ViewResolver를 이용하여 사용자의 요청을 처리한다.

 

process() 메소드는 요청 path에 해당하는 Controller를 검색하기 위해 HandlerMapping 객체getController() 메소드를 호출한다. 이후 검색된 ControllerhandleRequest() 메소드를 호출하면 요청에 해당하는 로직을 처리하고, 이동할 화면 정보가 리턴된다.


마지막으로 Controller가 리턴한 View 이름을 이용하여 실행될 View를 찾아 해당 화면으로 이동한다.

 

전체 처리 로직

 

톰캣서버를 재시작하고, 브라우저로 localhost:8080/hello.do와 localhost:8080/hello2.do 링크로 이동하면 화면이 정상적으로 나오는 걸 확인할 수 있다.

 

 

✍이러한 방식으로 코드를 작성하면, DispatcherServlet 클래스는 유지보수 과정에서 절대 수정되지 않는다

 

예를 들어 hello3 페이지를 추가한다고 가정하면, Hello3Controller 클래스를 추가하고 HandlerMapping에 해당 클래스의 객체를 등록하면 된다.

 

이렇게 기능 추가, 유지보수에 DIspatcherServlet을 수정하지 않아야 프레임워크에서 DispatcherServlet을 제공할 수 있다.

 

스프링 프레임워크에서 제공하는 DispatcherServlet 클래스를 사용하려면 새로운 기능이 추가되더라도 DispatcherServlet 클래스의 소스를 변경할 필요가 없도록 개발해야 할 것이다.



 

 

 

 

반응형

댓글