일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 디자인 패턴
- IT
- 알고리즘
- 컴퓨터구조
- Java
- Data Structure
- design pattern
- react
- MySQL
- 백준
- c언어
- Galera Cluster
- 네트워크
- 자바
- JavaScript
- 운영체제
- 파이썬
- Spring
- Algorithm
- C
- JPA
- Heap
- 자료구조
- Proxy
- MSA
- Kafka
- OS
- mongoDB
- spring webflux
- redis
- Today
- Total
시냅스
구현하며 이해하는 Spring MVC 본문
Spring MVC
Spring MVC 는 웹 애플리케이션 개발을 쉽게하기 위해 지원하는 모듈입니다.
Model-View-Controller 패턴을 기반으로 FrontController 패턴을 구현한
DispatcherServlet 을 활용하며 매번 Servlet 을 생성하거나 소멸시켜도 되지 않게 합니다.
흔히 Spring 이라고 하면, Spring MVC 를 떠올리게 되는 경우가 많습니다.
Spring 은 전체적인 프레임워크 자체를 말하고,
Spring MVC 는 웹 애플리케이션을 위한 모듈이라고 지칭하는 것이 옳은 표현일 것입니다.
그렇다면 Spring MVC 의 정체성을 결정하는 것에는 무엇이 있을까요?
구성요소
- DispatcherServlet
- 모든 클라이언트의 요청을 받아 적절한 컨트롤러로 분배한다.
- HanlderMappping
- Mapping 된 Uri 인지 저장시켜 둔 map
- HandlerAdapter
- Controller 마다 구현 spec 이 다를 수 있으므로, 공통된 처리를 하게 도와주는 기능을 수행
- 핸들러 메서드의 인자를 처리하고, 컨트롤러 메서드에 전달하기 위해 ArgumentResolver를 사용
- 컨트롤러 메서드를 실행하고, 반환 값을 처리하기 위해 ReturnValueHandler를 사용
- ModelAndView 객체를 생성하여 처리 결과를 뷰에 전달
- Handler(Controller)
- 클라이언트의 요청을 처리하고 응답하는 객체
- ModelAndView
- 클라이언트에 전달할 모델과 사용할 뷰를 포함한다.
- ViewResolver 가 렌더링 시에 사용한다.
- ViewResolver
- 컨트롤러가 반환한 뷰 이름을 실제 뷰로 해석하고 렌더링하는 역할을 한다.
- ArgumentResolver
- 컨트롤러의 매개변수를 해석하고 처리한다.
- 매개변수를 처리할 때 메세지 컨버터를 사용한다.
- ReturnValueHandler
- 반환 값을 처리한다.
- 반환값을 처리할 때 메세지 컨버터를 사용한다.
- MessageConverter
- 클라이언트와 서버 간에 교환되는 데이터를 변환하는 역할을 한다.
- 주로 HTTP request body 데이터를 java 객체로 변환하거나 java 객체를 HTTP response body 로 변환한다.
- ArgumentResolver, ReturnValueResolver 와 밀접한 관계를 갖는다.
작동 원리를 정리하자면, 요청이 DispatcherServlet 으로 들어오게 되면
HandlerMapping 을 통해 처리할 수 있는 URI 인지 확인한 다음,
HandlerAdapter 로 Handler 를 실행하고,
실행하기 전 ArgumentResolver 를 통해 Argument 들을 생성해서 주입합니다.
다만, converting 이 필요할 경우 MessageConveter 를 이용합니다.
비즈니스 로직이 끝나고 return 을 할 때 ReturnValueHandler 를 사용하는데,
View 가 return 될 경우 ViewResolver 를, 문자열이 반환될 경우 MessageConverter를 사용합니다.
여전히 모호하신가요?
그렇다면 아래에서 구현하며 확인하겠습니다!
구현
개괄적인 모습을 확인하기 위해 간단히 구현하겠습니다.
- HandlerMapping
- DispatcherServlet
- MessageConverter
위의 3개의 모습을 구현하며 확인하겠습니다.
Handler (Controller)
package com.example.messageconverter.controller;
import java.util.Map;
public interface Controller {
Map<String, String> process(Map<String, String> map);
}
- Interface Controller
- Handler 를 Interface 로 만드는 이유는, 추상화를 통해 동일한 동작을 하게 하기 위함입니다.
- 아래에서 DispatcherServlet 이 실행하는 모습을 보며 확인해보겠습니다.
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRestController {
String value() default "";
}
아래에서 살펴볼 HandlerMapping 을 사용하며 Mapping 의 기준이 될 애노테이션을 정의합니다.
저희가 만든 MyRestController 는 RestController 를 대체합니다.
물론, RestController 를 사용해도 무방하지만 하나라도 더 구현해 보는 것에 의의를 둡니다.
package com.example.messageconverter.controller;
import java.util.Map;
@MyRestController("/request-body")
public class RequestHttpBodyController implements Controller{
@Override
public Map<String, String> process(Map<String, String> map) {
return map;
}
}
- RequestHttpBodyController
- HTTP Body 처리를 위한 컨트롤러 입니다.
- 그저 들어온 Map 을 반환하기만 하는 역할을 합니다.
package com.example.messageconverter.controller;
import java.util.Map;
@MyRestController("/request-param")
public class RequestParameterController implements Controller {
@Override
public Map<String, String> process(Map<String, String> map) {
return map;
}
}
- RequestParamterController
- Query Parameter 를 처리하기 위한 컨트롤러 입니다.
- 마찬가지로 들어온 map 을 반환하기만 합니다.
DispatcherServlet 이 실행시킬 Controller 들을 우선적으로 정의하였습니다.
단순히 하기 위해서 query paramter 나 Http Body 로 들어온 데이터들을 Map 으로 파싱해서 보내줄 것이고,
return 또한 Map 으로 보내면 아래에서 message conveter 가 JSON 으로 response 하게 할 것입니다.
HanlderMappping
Map<String, Controller> mappingMap = new HashMap<>();
@PostConstruct
void initMappingMap() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String base = getBasePackage(); // 프로젝트의 base package 를 확인합니다.
Reflections reflections = new Reflections(base);
// reflections 라이브러리를 사용해서 제가 만든 MyRestController 애노테이션이 붙어있는 class 만 가져옵니다.
Set<Class<?>> annotatedClass = reflections.getTypesAnnotatedWith(MyRestController.class);
for (Class<?> clazz : annotatedClass) {
MyRestController myRestController = clazz.getAnnotation(MyRestController.class);
// mapping map 에 추가합니다.
mappingMap.put(myRestController.value(), (Controller) clazz.getDeclaredConstructor().newInstance());
}
}
private String getBasePackage() {
return this.getClass().getPackage().getName();
}
방금 위에서 구현한 컨트롤러들을 map 에 넣어두었습니다.
Reflections 라이브러리를 사용하여 MyRestController 를 사용하는 모든 class 를 가져오고,
class 를 reflection 을 사용하여 mapping 된 uri 정보와 class 를 인스턴스화 하여 map 에 넣어줍니다.
따라서 클라이언트가 해당 uri 로 요청하게 된다면, 맞춰 controller 가 반환될 것입니다.
DispatcherServlet
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
Controller controller = mappingMap.get(requestURI);
if (ObjectUtils.isEmpty(controller)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "fail");
return;
}
if (request.getMethod().equals("GET")) {
// ..
Map<String, String> responseMap = controller.process(map);
// ..
} else {
// ..
Map<String, String> responseMap = controller.process(bodyMap);
// ..
}
// ..
}
클라이언트의 요청이 들어오면 URI 를 확인하여 컨트롤러를 꺼내오게 합니다.
만약 없다면 404와 함께 fail 이라는 메세지를 보내줍니다.
존재하는 컨트롤러는 같은 interface 를 implement 하고 있으므로 동일한 process 메서드로 동작하게 됩니다.
(물론 실제 Spring 에서는 애노테이션을 확인해서 Dynamic proxy 로 동작할 것입니다.)
그렇기 때문에 DispatcherServlet은 조건에 따라, 위처럼 GET인지 POST인지, 단순히 실행하기만 하면 됩니다.
물론 실제 Spring MVC 에서는 훨씬 복잡하게 동작할 것입니다.
MessageConverter
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// ...
String parsedJson = "";
if (request.getMethod().equals("GET")) {
// 쿼리 파라미터 처리
Map<String, String> map = createParamMap(request);
Map<String, String> responseMap = controller.process(map);
parsedJson = mapToJson(responseMap);
} else {
// http body(json) 처리
Map<String, String> bodyMap = rawToMap(readRequestBody(request, parsedJson));
Map<String, String> responseMap = controller.process(bodyMap);
parsedJson = mapToJson(responseMap);
}
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
out.println(parsedJson);
out.flush();
}
MessageConverter 는 실제 Spring 에서는 ArgumentResolver 로 매개변수를 보내주거나,
ReturnValueHandler 로 응답을 보낼 때 필요에 의해 사용합니다.
그러므로 위 예제에서 MessageConverter 에 해당하는 부분은 createParamMap 으로 Query parameter 를 파싱하거나
Http Body 를 읽는 readRequestBody 를 사용하고 Map 으로 파싱하거나
결과물로 나온 responseMap 을 다시 json 으로 변환하여 out.println 으로 write 하는 부분이라고 할 수 있겠습니다.
아래에서 Spring MVC가 실제로 사용하는 ReturnValueHandler 와 MessageConverter 를 확인해보겠습니다.
// DispatcherServlet
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
HandlerMethod handlerMethod = ...; // 적절한 핸들러 메서드를 찾습니다.
// ...
ModelAndView mv = null;
// 핸들러 메서드를 실행합니다.
Object handlerResult = handlerMethod.invokeAndHandle(webRequest, mavContainer, providedArgs);
// 반환 값을 처리합니다.
returnValueHandlers.handleReturnValue(handlerResult, handlerMethod.getReturnType(), mavContainer, webRequest);
// ...
}
// RequestResponseBodyMethodProcessor를 생성할 때 필요한 HttpMessageConverter 목록을 받아옵니다.
public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
this.messageConverters = messageConverters;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// ...
MediaType contentType = ...; // 적절한 컨텐츠 타입을 결정합니다.
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// messageConverters를 사용하여 적절한 컨버터를 찾아 반환 값을 변환합니다.
writeWithMessageConverters(returnValue, returnType, webRequest, outputMessage, contentType);
// ...
}
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, NativeWebRequest webRequest, ServletServerHttpResponse outputMessage, MediaType contentType) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Class<?> valueType = getReturnValueType(value, returnType);
// 적절한 HttpMessageConverter를 찾습니다.
HttpMessageConverter<T> messageConverter = (HttpMessageConverter<T>) getMessageConverter(valueType, contentType);
// 컨버터를 사용하여 반환 값을 변환하고 응답 본문으로 작성합니다.
messageConverter.write(value, contentType, outputMessage);
}
doDispatch 로 DispatcherServlet 이 Handler 를 실행하며 returnValueHandler 의 handleReturnValue 를 호출합니다.
RequestResponseBodyMethodProcessor 는
@ResponseBody 를 처리하는 데 사용되는 ReturnValueHandler의 구현체이자, ArgumentResolver의 구현체입니다.
실제로 handleReturnValue 를 수행하는 구현체입니다.
이처럼 저희가 구현한 코드와 비슷한 과정을 거친다는 것을 알 수 있습니다.
물론 저희의 코드는 조악하지만, 구현한 코드들을 통해 개괄적으로 동작하는 모습을 상상할 수 있겠습니다.
전체 코드
package com.example.messageconverter;
import com.example.messageconverter.controller.Controller;
import com.example.messageconverter.controller.MyRestController;
import com.example.messageconverter.controller.RequestHttpBodyController;
import com.example.messageconverter.controller.RequestParameterController;
import org.reflections.Reflections;
import org.springframework.http.MediaType;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import javax.annotation.PostConstruct;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@WebServlet(name = "MessageConverterController", urlPatterns = "/*")
public class MessageConverterDispatcherServlet extends HttpServlet {
Map<String, Controller> mappingMap = new HashMap<>();
@PostConstruct
void initMappingMap() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String base = getBasePackage(); // 프로젝트의 base package 를 확인합니다.
Reflections reflections = new Reflections(base);
// reflection 라이브러리를 사용해서 제가 만든 MyRestController 애노테이션이 붙어있는 class 만 가져옵니다.
Set<Class<?>> annotatedClass = reflections.getTypesAnnotatedWith(MyRestController.class);
for (Class<?> clazz : annotatedClass) {
MyRestController myRestController = clazz.getAnnotation(MyRestController.class);
// mapping map 에 추가합니다.
mappingMap.put(myRestController.value(), (Controller) clazz.getDeclaredConstructor().newInstance());
}
}
private String getBasePackage() {
return this.getClass().getPackage().getName();
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
Controller controller = mappingMap.get(requestURI);
ServletOutputStream out = response.getOutputStream();
if (ObjectUtils.isEmpty(controller)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "fail");
return;
}
String parsedJson = "";
if (request.getMethod().equals("GET")) {
// 쿼리 파라미터 처리
Map<String, String> map = createParamMap(request);
Map<String, String> responseMap = controller.process(map);
parsedJson = mapToJson(responseMap);
} else {
// http body(json) 처리
Map<String, String> bodyMap = rawToMap(readRequestBody(request, parsedJson));
Map<String, String> responseMap = controller.process(bodyMap);
parsedJson = mapToJson(responseMap);
}
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
out.println(parsedJson);
out.flush();
}
// request 로 들어온 json 을 map 으로 parsing 합니다.
private Map<String, String> rawToMap(String rawJson) {
Map<String, String> temp = new HashMap<>();
rawJson = replaceToWhiteSpace(rawJson, "{");
rawJson = replaceToWhiteSpace(rawJson, "}");
String[] split = rawJson.split(",");
for (int i = 0; i < split.length; i++) {
String[] innerSplit = split[i].split(":");
innerSplit[0] = replaceToWhiteSpace(innerSplit[0].trim(), "\"");
innerSplit[1] = replaceToWhiteSpace(innerSplit[1].trim(), "\"");
temp.put(innerSplit[0], innerSplit[1]);
}
return temp;
}
// request 로부터 inputstream 을 얻어 body 에 있는 데이터를 읽습니다.
private static String readRequestBody(HttpServletRequest request, String parsedJson) throws IOException {
StringBuilder temp = new StringBuilder();
// request 로부터 소켓의 fd 를 바로 엽니다. -> 서블릿컨테이너가 헤더는 처리하였으므로 본문 데이터만 포함합니다.
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
String requestLine;
while ((requestLine = br.readLine()) != null) {
temp.append(requestLine);
}
return temp.toString();
}
private static String replaceToWhiteSpace(String rawJson, String target) {
return rawJson.replace(target, "");
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
private String mapToJson(Map<String, String> map) {
StringBuilder json = new StringBuilder();
json.append("{");
int count = 0;
for (Map.Entry<String, String> key : map.entrySet()) {
json.append(key.getKey());
json.append(" : ");
json.append(key.getValue());
if (++count < map.size()) json.append(", ");
}
json.append("}");
return json.toString();
}
}
https://github.com/taesukang-dev/my-own-spring-mvc
끝!
'Java, Spring' 카테고리의 다른 글
JWT 대신 Session 을 쓰는 이유 (Redis Session Clustering, Spring Security) (2) | 2023.07.17 |
---|---|
HikariCP 설정과 유의사항 (0) | 2023.05.03 |
Java 소켓으로 HTTP 요청 구현하기 (0) | 2023.04.16 |
Java NIO 의 작동 원리 (0) | 2023.04.04 |
코드로 살펴보는 Spring Thread Model 과 blocking/non-blocking I/O (0) | 2023.04.02 |