본문 바로가기
BackEnd/java spring

[Spring MVC] MVC 프레임워크 만들기

by kkkdh 2023. 5. 6.
728x90

프론트 컨트롤러 패턴이란?

이전까지 강의를 따라서 MVC 패턴을 적용하는 과정을 거치며, 반복되는 로직이 많아서 불편하다는 단점이 있었다.

 

이러한 반복되는 로직을 공통으로 처리하기 위해서는 프론트 컨트롤러 패턴을 적용해야 한다고..

프론트 컨트롤러 도입 전, 후 비교 (출처: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술)

강의 자료의 사진을 조금 가져와보면, 이런 셈이다.

 

Front controller라는 servlet을 만들어 공통 로직을 몰아서 처리하고, 각 Controller에서 처리할 작업을 담당하도록 구현하는 개발 방식이라고 한다.

항상 front controller에 의해 다른 controller가 호출되는 방식

 

FrontController 패턴의 특징

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받는다.
  • 프론트 컨트롤러가 알맞은 컨트롤러를 요청에 따라 찾아호출하는 구조
  • 입구를 하나로 만든다!
  • 따라서 공통된 입구를 통해 공통 처리 로직을 구현 가능
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다!

이와 관련된 세부적인 부분들은 이후에 코딩과 함께 자세하게 공부해봐야 더 확실하게 와닿을 것 같다.

 

스프링 웹 MVC와 프론트 컨트롤러

스프링 웹 MVC의 핵심도 바로 FrontController 스프링 웹 MVC의 DispatcherServletFrontController 패턴으로 구현되어 있음


FrontController 도입

인터페이스로 ControllerV1을 만들고, 이를 구현하는 구현체를 기능별로 인터페이스를 상속받아 구현한다.

이런 방식으로 구현하자

FrontController를 별개로 만들고, servlet으로 만들어서 "/front-controller/v1/*" 요청을 받아들이도록 구현하고, 세부 요청 URI에 따라서 다른 controller를 호출하도록 구현 방식을 변경하자.

위와 같은 형태로 구현된다.

패키지 구조는 위와 같다.
interface를 위와 같이 구현하고
이전에 구현한 각 기능들의 비즈니스 로직을 구현한 구현체들을 만듦

세부적인 구현체들의 코드는 생략하자 (어차피 이전에 사용한 코드랑 다를 게 없다.)

가장 중요한 FrontController

FrontController를 위와 같이 설계했다. 맵에서 알맞은 controller 구현체를 찾아 process method를 호출해 각 URI에 맞는 비즈니스 로직을 수행한다.

 

FrontController만 servlet으로 생성되어 사용자의 요청에 따라 알맞은 controller 구현체를 찾고 실행하는 작업을 수행하는 역할을 담당

 

이렇게만 만들면 사실 아직은 FrontController에서 중복되는 부분의 공통 처리가 적용이 안 돼있지만, 다음 단계에서 계속해서 개선해 나가 보도록 하자. (강의를 따라서)

구조를 개선할 때에는 구조만 개선하고, 기능을 개선할 때에는 기능만 개선하자. 한 번에 전부 바꾸면 더더욱 복잡해져서 감당이 안될 수도 있다.

View 분리를 통한 개선

이전 단계에서는 각 컨트롤러를 통해 view로 이동하는 부분을 구현하는데 있어서 중복이 발생했다. (dispatcher를 이용한 forward 작업이 중복되어 발생)

 

중복된 작업을 처리하는 view 객체를 별개로 만들자.

왼쪽에서 오른쪽의 구조로 변경하자 (출처: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술)

위와 같이 구조를 변경하면, controller에서는 FrontController에게 알맞은 view의 이름을 반환하고, rendering 작업을 위한 jsp 호출은 FrontController가 도맡아서 하게 될 것이다.

 

코드를 분리하는 방식은 먼저 MyView 클래스를 만들어, request 객체의 dispatcher를 이용한 forward 작업을 수행하는 render method를 구현하고, 앞으로 이 반복 작업을 view component에서 처리할 수 있도록 한다..

v2 Controller는 v1과 별다를 바가 없다.

다음으로 controller에서는 앞서 만든 MyView 클래스를 호출해, 원하는 jsp의 파일 경로만을 이용해 다음과 같이 호출함으로써 중복된 html page rendering 작업을 대신하도록 구현한다.

MyView 클래스의 코드는 다음과 같다.

MyView에서는 render method를 포함하고 있는데, 이름 그대로 사용자에게 제공될 페이지를 렌더링 하는 작업을 수행한다. 

 

정확하게는 request 객체에서 dispatcher를 뽑아내 forward method를 호출하는 중복 작업을 처리한다.

 

이런 방식으로 구조를 변경하면, MyView 클래스에서 view 역할만을 수행하도록 구현 상의 책임을 분할할 수 있게 되고, controller에서는 비즈니스 로직의 구현과 알맞은 viewPath를 view에 전달하는 작업만을 수행하도록 변경할 수 있게 된다.


Model 추가를 통한 개선

이번 과정에서는 controller에서 온전하게 비즈니스 로직을 수행하도록 개선하기 위해 model을 도입한다.

 

기존에는 controller에서 불필요하게 HttpServletRequest(+ Response) 객체를 전달 받아서 사용했는데, 이를 별도의 model 객체를 만들어 사용하는 경우 servlet에 대한 불필요한 종속성을 없앨 수 있다.

ModelView를 새로 설계하여 불필요한 servlet 종속성을 제거하자 (출처: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술)

이번 버전에서는 controller에서 HttpServletRequest를 이용해 attribute에 접근하는 방식이 불가능하기 때문에, ModelView 객체를 만들어 사용해야 한다. (이 ModelView 객체를 다른 버전에서도 사용하므로 frontcontroller 패키지에 둔다.)

MyView와 같은 위치에 둔다.
MemberListController로 예를 들자.

위의 컨트롤러를 보면 알 수 있듯이, 이제 controller의 process method에서는 paramMap을 parameter로 받고, ModelView를 반환하는 형식을 띠며, 더 이상 servlet에 의존하지 않는 모습을 확인할 수 있다.

ModelView 객체

여기서 반환하는 ModelView 객체는 위와 같은 형태이다.

 

viewName field는 jsp 파일의 논리적 이름 (/WEB-INF/views/ + .jsp 이런 중복되는 부분들을 제거하고 파일의 이름만 기입), model은 view에게 전달할 model을 의미한다.

 

다시 돌아와서 controller를 보면, paramMap을 인자로 받아 ModelView를 만들어 반환하는데, 간단히 정리하면 각 컨트롤러에서의 구현하고자 하는 비즈니스 로직에 따라 인자로 받은 paramMap의 정보를 사용한 로직을 실행하고, view로 넘기고 싶은 데이터를 model에 담아 반환한다고 볼 수 있다.

새로운 member 객체를 생성하고, 이에 대한 정보를 view component로 전달

MemberListController 같은 경우는 별도로 paramMap에 전달된 데이터를 사용하지 않으나, 위와 같이 MemberSaveController는 paramMap으로 넘어온 member 정보를 기반으로 새로운 member 객체를 생성하고 저장한 뒤 멤버에 대한 정보를 view component로 넘기려 함을 확인 가능하다.

 

FrontController의 코드가 조금 복잡하다.

FrontController의 service method

controllerMap은 이전 버전과 동일하므로 생략하고 각 부분은 다음과 같이 정리할 수 있다.

  • paramMap은 request 객체를 이용해 만들어내고, string type으로 key, value 모두 구성한다.
  • 이를 controllerMap을 통해 획득한 알맞은 url에 따른 controller의 process method 호출에 전달한다.
  • controller process method 실행 결과로 알맞은 ModelView 객체가 생성
  • ModelView 객체의 논리 이름을 viewResolver method를 이용해 실제 jsp file이 저장된 경로로 변경
  • myView의 render method에 획득한 ModelView의 model(view에 전달할 data 저장)과 request, response 객체를 전달해 view page를 rendering 하도록 한다.

여기서 myView의 render method 인자가 세 개로 변했는데, 이는 다음과 같이 request 객체에 직접 attribute를 controller가 담아주던 흐름의 변경에 따라 다음과 같이 변경되었기 때문이다.

modelToRequestAttribute method가 request 객체에 데이터를 담는 작업을 별도로 수행

이런 과정을 거쳐서 model을 도입하는 과정을 통해 servlet에 대한 불필요한 의존성을 제거할 수 있었다.


단순하고 실용적인 컨트롤러 - v4

이번 버전에서 개선하고자 하는 사항

  • v3 컨트롤러는 servlet 종속성 제거, view path 중복 제거 등의 개선이 이루어진 잘 설계된 컨트롤러
  • 하지만, 사용하는 개발자의 입장에서는 ModelView 객체를 항상 생성하고 반환해야 하는 부분이 번거롭다.
  • 좋은 프레임워크는 architecture도 중요하지만, 사용자의 편의성 또한 고려해야..
  • 따라서 다음과 같은 구조로 간단한 개선을 진행한다.

(출처: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술)

언뜻 봐서는 동일한 구조이지만, 다른 점이 존재한다.

  • FrontController에서 Controller에 paramMap + model을 전달
  • controller에서 model 객체에 view에 실을 정보를 paramMap을 이용해 생성
  • 이와 더불어 ModelView를 반환하던 구조에서 ViewName을 반환하는 구조로 변경

이러한 변경 사항으로 사용자는 controller를 다음과 같이 더욱 간단하게 설계할 수 있게 된다.

MemberSaveController

비즈니스 로직 구현 + view path 전달의 두 가지 작업만 수행하는 것으로 controller 설계에 대한 부담이 줄어든다. (기존에는 ModelView 객체를 생성해서 이를 담아야 했다.)

FrontController도 조금 변경됨

FrontController 또한 표시된 부분이 약간 변경되었다.

 

기존 ModelView 객체가 담당하던 것을 controller에 model 객체를 생성해 전달하여, view에 담을 정보를 직접 관리한다는 점과 viewResolver, rendering에 사용할 model, viewName을 직접 관리한다는 점이 달라졌다.

 

이처럼 약간의 개선을 통해 프레임워크의 사용성을 개선할 수 있었다.


유연한 컨트롤러

하나의 프로젝트 안에서 다양한 형태의 컨트롤러를 사용하고 싶다면??

controllerMap을 이용해서 controller들을 관리하고 있다.

지금은 controllerMap을 이용해서 컨트롤러를 관리하고 있기 때문에, 단일한 형태를 갖는 컨트롤러만을 사용하는 방식으로 개발이 되었다.

 

어댑터 패턴

위에서 개선한 방식은 버전에 따라 ControllerV3, ControllerV4 등이 있었다. 이런 컨트롤러들은 모두 다른 타입을 가져 호환이 불가능하다.

 

이런 상황에서 활용할 수 있는 것이 바로 어댑터라고 한다.

 

개발에서 두 개가 안 맞을때, 두 개를 맞춰서 끼워 넣는 것을 어댑터 패턴이라고 부른다고 한다.

새로운 구조

기존에는 FrontController에서 controller를 바로 호출할 수 있었지만, 이제는 핸들러 어댑터를 거쳐서 호출하는 구조로 변경되어야 한다. (다양한 형태의 컨트롤러를 사용하기 위해서)

  • 핸들러 어댑터 (handler adapter)
    • FrontController와 핸들러(controller) 사이에서 어댑터의 역할을 수행
    • 핸들러 어댑터로 인하여 다양한 컨트롤러를 호출할 수 있게 된다.
    • 핸들러를 호출한 결과로 ModelView 반환, 핸들러가 ModelView 반환하지 않아도 ModelView를 자체적으로 반환해야 한다.
  • 핸들러 (handler)
    • 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경.
    • 어댑터로 인하여 어떠한 종류의 핸들러이든, 어댑터만 있으면 처리할 수 있게 된다.

 

이번 버전에서 새로 구현한 코드

  • MyHandlerAdapter interface
    • supports, handle 추상 메서드를 보유
    • supports 메서드는 해당 핸들러 어댑터가 다룰 수 있는 어댑터인지 여부를 판단
    • handle 메서드는 핸들러를 호출하는 역할
  • ControllerV3HandlerAdapter
    • MyHandlerAdater 인터페이스의 구현체로 ControllerV3 핸들러를 실행할 수 있는 어댑터를 구현
  • FrontControllerServletV5
    • 핸들러 어댑터를 통한 핸들러 호출 방식을 적용한 front controller

 

MyHandlerAdapter
version 3 controller에 대한 handler adapter

handle method는 controllerV3에 맞는 작업(paramMap 생성)을 실행하고 controller process method를 호출(핸들러 호출) 후, ModelView를 반환

version 5 front controller

이전에 구현했던 메서드를 그대로 가져와서 구현했다.

 

실행 흐름은 다음과 같다.

  1. "/front-controller/v5/*"에 대한 URI 요청을 처리하기 위해 service method 호출
  2. 요청에 맞는 핸들러 어뎁터와 핸들러를 찾는다.
    • 생성자에 의해 "/front-controller/v5/*"URI에 대한 controller 매핑 정보를 handlerMappingMap에 저장
    • handlerAdapters는 핸들러를 호출하기 위한 핸들러 어댑터를 보관하는 자료 구조
  3. handler adapter에 handler를 전달하고, handle method 호출
  4. ModelView를 반환받고, ModelView를 viewResolver method를 이용해서 MyView 반환
  5. MyView의 render method를 호출해 HTML page를 rendering 하고 사용자에게 전달

 

이어서 version 4의 controller에 대한 handler adapter 구현과 적용 과정은 다음과 같다.

  • ControllerV4를 호출하기 위한 ControllerV4 HandlerAdapter를 새롭게 구현한다.
  • ControllerV4는 process method의 인수로 paramMap과 view로 전달할 model을 받기 때문에, 이 부분을 고려해야 한다.
  • 추가적으로 논리적 view name만을 반환하는 형태로 변경되어 handler adapter가 이 정보를 기반으로 ModelView를 만들어 반환해줘야 한다.

위의 사항들을 고려하여 설계된 ControllerV4HandlerAdapter

ControllerV4HandlerAdapter

특별한 사항은 없으나, 주석 처리된 부분 아래에 구현 사항이 adapter pattern을 적용함으로써 여러 가지의 핸들러를 호환 가능하게 처리한다는 핵심 부분에 해당

 

ControllerV4가 논리적 view name만을 반환하므로 이를 이용해 ModelView를 새롭게 생성하고, controller에 의해 생성된 model을 setter를 통해 ModelView에 setting 해줘야 하는 과정을 추가적으로 핸들러 어댑터가 처리하게 된다.

FrontControllerServletV5의 변경 사항들

다른 코드를 전혀 변경하지 않았다. 다만, handlerMappingMap, handlerAdapters에 ControllerV4 처리를 위한 mapping 정보와 새로이 구현한 handler adpater 정보를 등록했을 뿐이다.

 

이처럼 코드를 개선해서 스프링 핵심원리 기본 편에서 배웠던 OCP, DIP 등의 객체 지향 설계 원칙 또한 지키고 있음을 확인할 수 있다. (MyHandlerAdapter에 의존하는 방식, ControllerV* interface에 의존하는 방식 등...)

 

이렇게 이번 강의를 통해서 스프링 MVC가 만들어지고 개선된 과정을 약식으로나마 따라서 구현해 보며, 각각의 객체들이 어떤 목적으로 설계되었고, 어떤 개선을 위해 구성되었는지 이해할 수 있었다.

 

또한 어댑터 패턴에 대해서도 경험할 수 있었다.

728x90

'BackEnd > java spring' 카테고리의 다른 글

[Spring MVC] 기본 기능  (1) 2023.05.20
[Spring MVC] 구조 이해  (0) 2023.05.15
[Spring MVC] MVC 패턴에 대해서  (0) 2023.03.20
[Spring MVC] HttpServletRequest, HttpServletResponse  (0) 2023.03.06
Servlet에 대한 개념 정리  (0) 2023.03.01

댓글