본문 바로가기

Spring

spring MVC 패턴

개발을 공부하면서 spring MVC을 계속해서 써왔지만 그 원리와 흐름에 대해서는 자세하게 알아본 적이 없다. 원리와 흐름을 알고 있다면 코드를 설계하고 버그가 발생했을 때 더 바르게 해결할 것이라고 생각해 알아보려고 한다.

 

 

 

아주 옛날 JSP 모델 1을 사용할 때는 Controller, view를 구분하지 않았다고 합니다. 그래서 한 파일에 모든 코드가 존재하게 됩니다. 이로 인해 규모가 큰 프로젝트에서 JSP 모델 1을 사용한 코드를 유지보수하는 것은 힘든 일이었다고 합니다.

 

MVC 패턴의 핵심은 역할의 분리라고 생각합니다. 기존 파일 한 곳에 존재 했던 것을 Controller - Model - View 세가지로 나누어 개발합니다. 

 

 

Controller - HTTP 요청을 받고 비즈니스 로직을 수행합니다. 

Model - Controller에서 처리한 데이터를 담아둡니다.

View - 브라우저(client)에 화면을 그립니다.

 

하지만 고전적인 MVC 패턴에서도 한계점이 존재했습니다.

Controller가 HTTP 요청을 받고 비즈니스 로직을 수행하는 과정에서 너무 많은 역할을 수행하게 됩니다. 

또한 공통으로 처리해야되는 사항들이 많아 Controller 에는 중복되는 코드들이 존재합니다.

 

spring에서는 dispatcherServlet 객체를 통해  위 문제점을 해결합니다. 물론 dispatcherServlet 가 모든 것을 다 해결하는 것은 아닙니다. 하지만 모든 Controller는 dispatcherServlet 를 통해 로직이 수행됩니다.

 

한편 순수 MVC패턴에는 Controller 내 비즈니스 로직과 데이터베이스 접근에 대한 코드가 캡슐화되지 않은 채로 존재합니다. spring에서는 이러한 문제를 @Component 나아가 @Service @Repository 애노테이션을 통해 효율적으로 해결합니다.


SPRING MVC 패턴

 

흐름을 설명하기 전에 디스패처 서블릿에 대해 짚고 넘어가겠습니다. 디스패처 서블릿은 우선 서블릿입니다.

 

스프링 부트 환경에서 build.gradle에 아래 의존성을 추가하면

implementation 'org.springframework.boot:spring-boot-starter-web'

디스패처 서블릿을 서블릿으로 자동으로 등록하면서 모든 경로에 대해 매핑합니다.

서블릿이 호출되면 HttpServlet에서 제공하는 service() 매서드를 호출합니다.

 

service() 매서드 호출을 시작으로 doDispatch()가 실행됩니다. doDispatch()가 실행되면 위 다이어그램의 1번 부터 차례대로 로직이 수행됩니다.


1. 핸들러 매핑(URL 매핑 정보 조회)

 

예를 들면 URL에 /hello 로 요청하여 들어왔을 때

이에 해당하는 컨트롤러(핸들러)가 있는지 확인하는 것입니다.

 

어디서 찾냐면 아래와 같은 String, Controller 객체 구조를 가진 맵이 존재합니다.

private Map<String, Controller> controllerMap = new HashMap<>();

 

key value
"/hello" new HelloController()
"/member" new MemberController()

 

디스패처 서블릿에서 클라이언트가 요청한 URI를 받아옵니다.

1 . /hello 로 요청했네?

2. controllerMap에 "/hello" 라는 key 가 있는지 볼까?

3. 있네? new HelloController() 를 가져온다.

 

 

여기까지 오면 디스패처 서블릿은 Controller 를 가지고 있게 됩니다.

 

이 부분은 spring MVC 흐름을 배우지 않더라도 인지할 수 있는 경우가 있습니다.

컴파일 과정에서 같은 url로 여러개의 컨트롤러를 매핑하려고 하면 스프링에서 requestHandlerMappingHandler 객체에서 ambigous 하다는 오류를 발생시킵니다.

 

쉽게 말해 controllerMap에 중복되는 key가 존재해서 어떻게 처리할지 스프링 입장에서는 애매하다는 뜻입니다.

 


2. 핸들러(컨트롤러) 어뎁터 목록 조회

사실 이 부분이 저는 제일 아리송했습니다. 처음부터 스프링 MVC 패턴을 적용하면 스프링에서 다 해줘서 이 부분이 존재하는지는 확인하기조차 힘들다고 생각합니다.

 

김영한님의 MVC 강의에서는 어뎁터를 실제 플러그 어뎁터에 비유해서 알려주십니다. 여기에 조금 더 상상의 나래를 펼치겠습니다.

 

1번에서 얻어온 컨트롤러를 청소기라고 비유합니다.  위에서 어찌저찌해서 청소기를 가져왔습니다.

이제 신나게 청소를 하려고 청소기 코드를 꽂으려 보니까 코드가 이런겁니다. 한국인이라면 당황스럽겠죠

 

 

 

핸들러 어뎁터는 이런 역할을 한다고 볼 수 있지 않을까 싶습니다.

 

이런 코드를 낄 수 있는 환경이 준비되어 있니?

 

디스패처에서는 이런식으로 1에서 불러온 Controller에 대한 어뎁터가 존재하는지 아래와 같이 찾을 것입니다.

private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
adapter
new ControllerHandlerAdepterV110()
new ControllerHandlerAdepterV220()

 

handlerAdapters 리스트를 돌면서 해당 핸들러(컨트롤러)가 리스트 안에 있는 adapter 로 처리할 수 있는지 확인합니다.

 

// ControllerAdapterV110
public boolean supports(Object handler) {
    return (handler instanceof ControllerV110);
}

// ControllerAdapterV220
public boolean supports(Object handler) {
    return (handler instanceof ControllerV220);
}

 

이제 처리할 수 있다면 해당 어뎁터를 디스패처 서블릿으로 가져옵니다.

 

참고

spring에서 제공하는 @RequestMapping을 이용하면 1. 핸들러 매핑 2. 핸들러 어댑터 두 가지를 한 번에 해결합니다. 

 


 

3. Handle & 4. handler & 5. view 논리 이름 model 데이터 담음

여기서 Handle이란 2번의 결과로 가져온 어뎁터가 가지고 있는 기능이라고 보면 됩니다.

 

public interface HandlerAdapter {

    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) 
    					throws ServletException, IOException;
}

 

2번의 결과로 handler(핸들러, 컨트롤러)에 맞는 어뎁터를 가져왔으니 이제 무언가를 수행해야겠죠?

Handle 이라는 매서드를 수행합니다.

해당 매서드의 인자로 handler를 받습니다. 이 부분이 4. handler입니다

그 결과로 ModelView를 리턴합니다. ModelAndView srping MVC 에서는 ModelAndView 객체에 해당합니다.

 

ModelAndView 는 크게 두가지가 담기게 되고 디스패처 서블릿으로 보내집니다. 

1. view Template 의 논리적 이름

view template의 논리적 이름은 controller 매서드가 리턴값으로 반환하면서 답기게 됩니다.

2. service & repository 로직 수행으로 인해 만들어지는 데이터(Model 에 담기게 됩니다.)

 

이 과정이 5. view 논리 이름, model 데이터 담음입니다.

 

뭔가 추상적이여서 코드를 보면서 한 번 더 설명하겠습니다. 디스패처 서블릿은  4. handler를 통해 컨트롤러에 접근합니다. 아래 process 매서드에서 username , age 를 설정하고 save 하는 과정의  service ,repository 역할을 수행합니다.

 

public class MemberSaveControllerV4 implements ControllerV4 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        model.put("member", member);
        return "save-result";
    }
}

 

이후 모델에 데이터를 담습니다.

그리고 string 타입 "save-result"을 리턴합니다.

리턴하는 "save-result" 이 view Template 의 논리적 이름입니다. 

 


 

6. viewResolver 호출

viewResolver는 3,4,5의 결과로 얻어진 view 논리적 이름을 인자로 삼아 실행할 물리적 뷰를 찾아 반환합니다.

 

private MyView viewResolver(String viewName) {
    return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}

 


 

7. 모델 데이터를 바탕으로 rendering

 

6.의 결과로 실제 존재하는 view template을 얻었습니다.

view template 이 준비됐다고 해서 바로 랜더링 되는 것은 아닙니다.

 

아래 처럼 동적인 데이터가 들어갈 수도 있습니다.

<ul>
    <li>id=${member.id}</li>
    <li>username=${member.username}</li>
    <li>age=${member.age}</li>
</ul>

controller 에서 담은 데이터를 view 에도 적용시키려면 model에 담겨져 있는 데이터를 참조하여 rendering 합니다.