[Spring] 스프링 MVC 1편, 백엔드 웹 개발 핵심 기술 - 4
👩💻 MVC 프레임워크 만들기
📌프론트 컨트롤러 패턴 소개
✏️프론트 컨트롤러 패턴에 대한 소개
:프론트 컨트롤러 도입 전과 후


"FrontController 패턴 특징"
1. 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
2. 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
3. 입구를 하나로!
4. 공통 처리 가능
5. 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
📌서블릿으로 회원 관리 웹 애플리케이션 만들기
✏️프론트 컨트롤러 도입 - v1
:프론트 컨트롤러 단계적 도입
"v1의 구조"

✏️ControllerV1.interface

서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다.
각 컨트롤러들은 이 인터페이스를 구현하면 된다. 프론트 트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을
가져갈 수 있다.
✏️MemberFormControllerV1.Java

회원 등록 컨트롤러
✏️MemberFormControllerV1.Java

회원 저장 컨트롤러
🚨여기서 주의 할 점 🚨
1. "request.getParameter()"으로 꺼내는 값은 항상 "String(문자열)"타입이기 때문에 형변환 해주어야 한다.
ex) int Age = Integer.parseInt(request.getParameter("age"));
2. Model에 데이터를 보관하는 방법
request.setAttribute("member", member); 으로 임시 저장소에 데이터를 저장할 수 있다.
request.getAttribute("member", member); 으로 임시 저장소에 데이터를 저장할 수 있다.
✏️MemberListControllerV1.Java

회원 목록 컨트롤러
✏️FrontControllerServletV1 - 프론트 컨트롤러

프론트 컨트롤러
Map<String, ControllerV1> controllerMap = new HashMap<>();
➜ HashMap 객체를 생성하여 key 데이터 타입으로 String을 지정하여 url을 저장하도록 하고, value로 ControllerV1 인터페이스 데이터 타입이 올 수 있도록 지정
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
controllerMap.put("/front-controller/v1/members/save", new MemberSaveController());
controllerMap.put("/front-controller/v1/members", new MemberListController());
}
➜프론트 컨트롤러의 생성자를 통해서 Map에 컨트롤러 url과, 컨트롤러 객체를 생성함
🚨여기서 주의 할 점 🚨
1. "서블릿 컨테이너"는 "싱글톤"을 보장 함
2. FrontControllerServletV1()이라는 생성자는 서블릿 컨테이너가 서블릿을 등록하기 위해 호출 할 때 사용 됨
urlPatterns = "/front-controller/v1/*"`
➜ /front-controller/v1를 포함한 하위 모든 요청 은 이 서블릿에서 받아들인다.
ex) /front-controller/v1` , `/front-controller/v1/a` , `/front-controller/v1/a/b`controllerMap
key: 매핑 URLvalue: 호출될 컨트롤러
"service()"
먼저 requestURI 를 조회해서 실제 호출할 컨트롤러를 controllerMap에서 찾는다.
만약 없다면 404(SC_NOT_FOUND) 상태 코드를 반환한다.
컨트롤러를 찾고 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행한다.
📌View 분리 - v2
✏️View를 분리해보자
모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않다.

이 부분을 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만들자.
"V2 구조"

1. 클라이언트가 HTTP를 요청하면, FrontController에서 URL 매핑 정보를 조회한다. (request.getRequestURI())
2. 조회한 URL의 컨트롤러를 호출한다
3. 컨트롤러는 MyView 객체를 반환한다 ( 컨트롤러는 MyView 객체를 생성하는 역할만 한다)
4. 프론트 컨트롤러는 MyView를 받아 render()를 호출한다.
5. render()를 호출하면 JSP forward가 되어 HTML 응답을 보낸다.
코드를 보며 이해해보자
✏️V2코드 해석


/front-controller/v2/members/new-form이 들어오면
url패턴이 <urlPatterns = "/front-controller/v2/*">인 "frontControllerServletV2" 서블릿이 호출된다.
FrontControllerServletV2() 생성자는 이미 호출 된 상태이다. (서블릿을 등록할 때 생성됨. 호출할 때 x)


request.getRequestURI(); 를 통해서 요청이 온 URI 정보를 얻어낸 뒤, 맨 처음 HashMap객체에 넣어둔 key값과 똑같은 uri의 value를 꺼낸다 (MemberFormControllerV2, MemberSaveControllerV2 객체 등)


이후 컨트롤러 객체의 process메소드를 실행한 값을 MyView 데이터 타입인 view에 저장한다.
만약 "/front-controller/v2/members/new-form" url이 들어와 MemberFormControllerV2.process가 호출됐다고 가정해보자.
process가 실행되어 MyView 객체를 생성하고 viewPath를 매개변수로 받아 리턴한다.


MyView 객체를 리턴한 controllerV2.process() 값을 view 변수에 저장하고, view변수에 render()메소드를 호출한다.
-> 끝
📌Model 추가 - v3
✏️Model을 추가 해보자
1. 서블릿 종속성 제거**
컨트롤러 입장에서 HttpServletRequest, HttpServletResponse이 꼭 필요할까?
요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다.
그리고 request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다.
우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경해보자.
2. 뷰 이름 중복 제거
컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다.
컨트롤러는"뷰의 논리 이름"("/new-form")을 반환하고, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화 하자.
이렇게 해두면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다.

✏️V3의 구조

지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다.
그리고 Model도 `request.setAttribute()` 를 통해 데이터를 저장하고 뷰에 전달했다.
서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체를 만들어보자.
(이번 버전에서는 컨트롤러에서 HttpServletRequest를 사용할 수 없다. 따라서 직접`request.setAttribute()` 를 호출할 수 도 없다. 따라서 Model이 별도로 필요하다.)
참고로 `ModelView` 객체는 다른 버전에서도 사용하므로 패키지를 `frontcontroller` 에 둔다.
✏️V3 정리 및 해석

1. FrontControllerServletV3는 클라이언트의 HTTP 요청이 있을 때 실행 된다.
: @WebServlet(urlPatterns = "/front-controller/v3/*") 애노테이션을 보면 알 수 있듯이 , 이 서블릿은 "/front-controller/v3/*")로 시작하는 모든 URL패턴의 요청을 처리한다.
2. 클라이언트의 HTTP 요청이 오면 서블릿 컨테이너는 서블릿을 등록하기 위해 FrontControllerServletV3의 생성자를 호출한다.
: private Map<String, ControllerV3> controllerV3Map = new HashMap<>();
public FrontControllerServletV3(){
controllerV3Map.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
controllerV3Map.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
controllerV3Map.put("/front-controller/v3/members", new MemberListControllerV3());
}

3. 클라이언트의 요청이 발생하면 'service'메서드가 호출된다.
: protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {}
[정보] 서블릿 컨테이너는 서블릿의 생명 주기를 관리하고, 클라이언트의 HTTP 요청이 있을 때 마다 적절한 메소드를 호출해주는 역할을 한다. "service()"메서드는 HttpServlet 클래스에 정의 된 메서드로, 클라이언트의 요청이 들어오면 서블릿 컨테이너가 이 메서드를 호출한다.
(내가 호출 하지 않아도, 서블릿 컨테이너가 HTTP 요청이 들어오면 자동 호출)
4. 요청 URI를 request.getRequestURI()로 확인하여 해당 URI에 대응하는 ControllerV3를 찾는다.
: ControllerV3 controllerV3 = controllerV3Map.get(requestURI);
[현재 URI의 정보가 담긴 requestURI가 만약 "/front-controller/v3/members/new-form"이여서 MemberFormControllerV3() 객체를 controllerV3 변수에 담았다고 가정해보자.]
5. 만약 controllerV3의 값이 null값이라면 상태코드 404를 띄운다.
: if (controllerV3 == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return;}



6. Map<Stirng, String> 데이터 타입인 paramMap 변수에 createParamMap(request)값을 넣음
7. createParamMap 메서드를 살펴보면 HashMap객체를 생성하고 HTTP 요청의 매개변수(parameter)를 추출하여 Map에 저장한다.
request.getParameterNames().asIterator().forEachRemaining (paramName -> paramMap.put(paramName, request.getParameter(paramName)));
자세한 설명은 다음과 같다
ㅇ request.getParameterNames(): 현재 요청에 포함된 모든 매개변수의 이름을 나타내는 Enumeration을 반환
ㅇ asIterator(): Enumeration을 Iterator로 변환함.
ㅇ Iterator는 반복(iteration)을 도와주는 자바의 인터페이스이다.
ㅇ forEachRemaining(paramName -> paramMap.put(paramName,request.getParameter(paramName)))
: Iterator를 이용하여 Enumeration에 포함된 모든 매개변수의 이름에 대해 주어진 람다 표현식을 실행한다.
각 매개변수의 이름을 가져와서 paramMap이라는 Map 객체에 (매개변수의 이름, 매개변수의 값) 형태로 저장한다.
이 때, request.getParameter(paramName)은 주어진 매개변수의 값을 반환하는 메서드입니다.
8. 그럼 현재 HTTP에서 요청한 파라미터들이 "paramMap"이라는 Map에 담겼을 것이다 ! ! !
9. 이후 controllerV3 (컨트롤러 객체).process(paramMap)을 하여, ModelView데이터 형태인 mv에 저장한다
(process의 데이터 반환값은 ModelView 데이터 타입이다)


10. MemberFormControllerV3은 ControllerV3 인터페이스의 구현체이며, ModelView 타입을 리턴하는 process()메서드를 구현하고 있다. 매개변수로 Map<String, String>을 받으며 Map의 이름은 "pramMap"
헷갈리지말것 !!!!!!!!!
Map<String, String> paramMap에는 "/front-controller/v3/members/new-form", new MemberFormControllerV3()가 들어있는것이 아니라 파라미터의 key와 value가 들어갈것임 (username과 age)
---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- -----
여기서 잠시 헷갈림을 없애기 위해 "ControllerV3 controllerV3" 변수에 담긴 MemberFormControllerV3 객체의 코드를 살펴보자. MemberFormControllerV2와 비교를 해보면 process메소드의 매개변수가 Map으로 바뀐 걸 볼 수 있다.
왜 매개변수를 바꿨을까 ?
"process의 매개변수를 request, response가 아닌, Map<String, String>으로 받은 이유"
1. 우선 간결하고 유연하다.
:HTTP 요청 파라미터는 일반적으로 문자열 형태 이며, 키와 값의 쌍으로 이루어져 있다. => 따라서 이를 Map으로 표현
(request.getParameter() -> String으로 반환함 / 그래서 age값 파라미터로 바꿀때 Integer로 형변환 한 것)
(아래서 사진으로 자세한 설명 추가)
2. 단순한 데이터 전달
: 'HttpServletRequest'와 'HttpServletResponse'를 사용하는 것은 일반적인 경우에 잘 사용하지 않음. => 복잡성 초래(우리 예제의 V3과 V2 FormController도 살펴보면 request와 response를 사용하지 않음)
---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- -----
10. ModelView 객체를 생성하고 viewName을 전달한다.
"process의 매개변수를 request, response가 아닌, Map<String, String>으로 받은 이유"


MemberFormControllerV3 클래스는 단순히 ModelView(가입 폼)만 보여주면 되는 클래스라 Map이 필요없었지만, MemberSaveControllerV3 클래스를 보면 왜 매개변수를 Map<String, String>으로 받았는지 알 수 있다.
ㅇ MemberSaveControoerV3.process 메서드가 실행되면 위에 있는 코드에서 보면 알 수 있듯이, 프론트 컨트롤러에서의
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controllerV3.process(paramMap);
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
코드로 인해 process의 인자 값으로 paramMap이 들어와 파라미터의 key, value를 받았을 것 이다.
ㅇ 받은 파라미터의 key,value의 값으로(예제에서는 username, age 이다)
ㅇ 매개변수로 들어온 paramMap으로 파라미터 값인 username과 age의 값을 꺼낼 수 있다
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
ㅇ 또한 ModelView 객체를 생성하여 viewName을 지정하고, ModelView의 필드인 HashMap을 가져와 .put을 통해 member의 데이터를 저장할 수 있다.



11. ModelView 타입인 mv에 위에서
ModelView mv = controllerV3.process(paramMap);
을 실행하여 process의 return 값으로 받은 ModelView의 ViewName을 가져온다 ("new-form");
[참고] ModelView에선 @Getter, @Setter 애노테이션을 사용하여 Lombok을 사용하였다
12.viewResoltver 메소드를 실행하여 새로운 MyView객체를 생성하고, View의 경로는
"/WEB-INF/views/" + viewName + ".jsp" 로 지정한다.
(= viewName에는 "new-form"이 들어가, MyView 객체의 매개변수로 "/WEB-INF/views/new-form.jsp" 가 들어가게 된다.
13.이후 받은 view로 render 메서드를 실행한다.



14. 넘겨받은 model과 request를 사용해서 modelToRequestAttribute()라는 메서드를 호출하여 model의 모든 데이터를 임시 저장소에 담아둔다.
model.forEach((key, value) -> request.setAttribute(key, value));
: "위의 코드는 model을 다 꺼내서 변수명 key, value라는 이름으로 model 루프를 다 돌려서 setAttribute에 key value값으로 저장하는 거구나" 라고 이해하면 됨
: //forEach() -> 다 꺼내는 것
15.이후 아래의 코드로 클라이언트의 요청을 JSP에 전달(forward)함
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response);
끝~~~>3<
📌단순하고 실용적인 컨트롤러 - v4
✏️Model을 추가 해보자
앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다.
그런데 항상 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거롭다.
이번에는 v3를 조금 변경해서 실제 구현하는 개발자들이 매우 편리하게 개발할 수 있는 v4 버전을 개발해보자.

✏️MemberFormControllerV4.java

✏️MemberSaveControllerV4.java


V3와 다르게 process 메서드를 확인하면 매개변수로 Map<String, String> 타입인 paramMap과 Map<String, Object>타입인 Model을 받고있다.
ModelView 객체를 따로 생성하지 않고, `model.put("member", member)` 모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.
✏️MemberListControllerV4.java

1. 모델 객체 전달
`Map<String, Object> model = new HashMap<>();'
모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.
2. 뷰의 논리 이름을 직접 반환
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
:컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰를 찾을 수 있다.
📌유연한 컨트롤러 - v5
✏️"만약 어떤 개발자는 `ControllerV3` 방식으로 개발하고 싶고, 어떤 개발자는 `ControllerV4` 방식으로 개발하고 싶다면?"

ㅇ 어댑터 패턴
: 지금까지 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.
위를 보면 알 수 있듯이 "ControllerV3" , "ControllerV4" 는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다.
마치 v3는 110v이고, v4는 220v 전기 콘센트 같은 것이다. 이럴 때 사용하는 것이 바로 어댑터이다.
어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해보자.
✏️V5 구조

핸들러 어댑터: 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다. 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.
:**MyHandlerAdapter**
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException;
ㅇ `boolean supports(Object handler)`
handler는 컨트롤러를 말한다. 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드다. (반환타입 True,False)
ㅇ `ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)`
어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다. 실제 컨트롤러가 ModelView를 반환
하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다. 이전에는 프론트 컨트롤러가 실제 컨트롤러를
호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출 된다.