[Spring] Spring MVC 따라하기

Bobby·2021년 9월 9일
0

즐거운 개발일지

목록 보기
6/22
post-thumbnail

1. SpringMVC

SpringMVC 구조는 다음과 같다.

구성요소

1. DispatcherServlet

  • Spring MVC에서 사용하는 FrontController 이다.
  • 모든 요청을 받아 각 요청에 맞는 컨트롤러를 찾아서 호출 하는 역할을 한다.
  • FrontController를 사용 함으로써 공통적으로 수행해야하는 로직의 코드 중복을 줄일 수 있다.
  • DispatcherServlet이 컨트롤러를 호출함으로써 컨트롤러 들은 서블릿을 사용하지 않아도 된다.

2. HandlerMapping

  • 스프링이 실행되고 DispatcherServlet이 생성 되면서 MappingRegistry에 매핑정보를 담는다.
  • 들어온 요청을 어느 컨트롤러(핸들러)로 보낼 지에 대한 정보를 가지고 있다.
  • 거의 대부분 RequestMappingHandlerMapping를 사용한다. (@Controller, @RequestMapping 어노테이션 기반)

3. HandlerAdapter

  • 요청에 대해 선택된 핸들러를 실행하고 결과 값(ModelAndView)을 리턴한다.
  • 어노테이션 기반 사용 시 RequestMappingHandlerAdapter를 사용
  • 다음 메소드를 구현하여 작동
  • supports : 해당 핸들러를 지원하는지 체크
  • handler : 수행

4. ViewResolver

  • 핸들러 실행 후 결과에서 알맞은 View를 찾아 리턴한다.
  • 해당 View의 render() 함수를 실행하여 렌더링하여 응답한다.

5. MessageConverter

  • http 바디에 직접 데이터를 담을 경우(@ResponseBody)에는 ViewResolver가 아닌 MessageConverter가 수행 된다.
  • 다양한 메시지 컨버터가 있으며 리턴 된 결과에 맞는 컨버터가 선택되어 body에 데이터를 담는다.

실행 흐름

  1. 클라이언트가 서버로 request를 보내면 DispatcherServlet이 해당 요청을 받는다.
  2. 해당 요청을 수행할 수 있는 핸들러를 찾는다. (HandlerMapping)
  3. 핸들러를 수행 할 핸들러 어댑터를 찾는다.
  4. 핸들러 어댑터는 핸들러의 메소드를 호출한다.
  5. 메소드 수행 결과를 모델에 담고 뷰를 리턴한다.
  6. 뷰 리졸버가 해당 뷰를 찾아 리턴한다.
  7. 렌더링 하고 응답한다.

2. Spring MVC (내맘대로) 만들기

  • 스프링은 어떻게 어노테이션만 등록하면 해당 메소드를 찾아서 수행할까?
  • 초초초 간단하게 Spring MVC를 만들면서 공부해 보자.
  • 해당 흐름의 컨셉을 따라해 보는 것이고 실제 동작과 다른 점이 있을 수 있다.
  • 어노테이션 기반으로 만든다.

프로젝트 생성

사용할 어노테이션 만들기

  • @Controller
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
  • @RequestMapping
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {

    String value() default "";
}
  • @ResponseBody
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

HandlerMapping

  • handlerMapping은 Map<String, MappingRegistry> 형태로 저장.
  • @RequestMapping에 등록한 URL을 키로 해당 컨트롤러와 메소드 정보를 저장.
// 매핑 정보
public class MappingRegistry {

    private Object handler;
    private Method method;
    
    ...
}

HandlerAdapter

  • 인터페이스를 상속 받아 구현
public interface HandlerAdapter {

    boolean supports(Object handler);

    String handle(HttpServletRequest request, HttpServletResponse response, MappingRegistry handler);
}
  • 구현체 : RequestMappingHandlerAdapter
  • 어댑터 지원 여부를 해당 컨트롤러가 @Controller 어노테이션을 가지고 있는지 유무로 판단
  • @ResponseBody 어노테이션이 있으면 직접 body에 결과 값을 쓴다.
  • String만 사용했고 MessageConverter와 ArgumentResolver는 생략.
public class RequestMappingHandlerAdapter implements HandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return handler.getClass().isAnnotationPresent(Controller.class);
    }

    @Override
    public String handle(HttpServletRequest request, HttpServletResponse response, MappingRegistry handler) {
        Object result = null;
        try {
            Method method = handler.getMethod();
            result = method.invoke(handler.getHandler());
            if (method.isAnnotationPresent(ResponseBody.class)) {
                if (result instanceof String) {
                    response.setContentType("text/plain");
                    response.getWriter().println(result);
                }
                return null;
            }
        } catch (InvocationTargetException | IllegalAccessException | IOException e) {
            e.printStackTrace();
        }
        return (String) result;
    }
}

ViewResolver

  • 인터페이스를 상속 받아 구현
public interface ViewResolver {

    View resolveViewName(String viewName) throws Exception;
}
  • 구현체 : InternalResourceViewResolver
  • jsp를 처리할 InternalResourceView를 리턴한다.
public class InternalResourceViewResolver implements ViewResolver{

    @Override
    public View resolveViewName(String viewName) {
        return viewName.endsWith(".jsp") ? new InternalResourceView() : null;
    }
}

View

  • 인터페이스를 상속받아 구현
public interface View {

    void render(HttpServletRequest request, HttpServletResponse response, String path);
}
  • 구현체 : InternalResourceView
  • 경로에 있는 jsp 찾아 리턴
public class InternalResourceView implements View {
    @Override
    public void render(HttpServletRequest request, HttpServletResponse response, String path) {
        try {
            request.getRequestDispatcher(path).forward(request, response);
        } catch (ServletException | IOException e) {
            e.printStackTrace();
        }
    }
}

WebApplicationContext

  • 실행 될 때 패키지 내부에 있는 클래스들을 분석하여 저장해 놓는다.
public class WebApplicationContext {

    public static WebApplicationContext instance;

    private final Map<String, MappingRegistry> handlerMapping = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
    private final List<ViewResolver> viewResolvers = new ArrayList<>();

    // 싱글톤
    public static WebApplicationContext getInstance() {
       if (instance == null) {
           instance = new WebApplicationContext();
       }
       return instance;
    }

    // 객체 생성 시 리소스 등록
    private WebApplicationContext()  {
        initResources();
    }

    ...
    
    getter 메소드
    
    ...

    private void initResources() {
        String packageName = "com.example.dispatcherservlet";
        Set<Class<?>> classes = new HashSet<>();
        setClasses(classes, packageName);

        for (Class<?> aClass : classes) {
            try {
                Object instance = aClass.getConstructor().newInstance();
                // 핸들러 어댑터 등록
                if (instance instanceof MyHandlerAdapter) {
                    handlerAdapters.add((MyHandlerAdapter) instance);
                }
                // 뷰 리졸버 등록
                if (instance instanceof ViewResolver) {
                    viewResolvers.add((ViewResolver) instance);
                }
                // 각 요청 URL에 맞는 핸들러와 메소드 등록
                if (aClass.isAnnotationPresent(Controller.class)) {
                    Method[] declaredMethods = aClass.getDeclaredMethods();
                    for (Method declaredMethod : declaredMethods) {
                        if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {
                            RequestMapping annotation = declaredMethod.getAnnotation(RequestMapping.class);
                            handlerMapping.put(annotation.value(), new MappingRegistry(instance, declaredMethod));
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 패키지 내부의 모든 클래스 찾기
    private void setClasses(Set<Class<?>> classes, String packageName) {
        String directoryString = getDirectoryString(packageName);
        File directory = new File(directoryString);
        if(directory.exists()){
            String[] files = directory.list();
            for(String fileName : files){
                if (isExcluded(fileName)) continue;
                
                // .class 파일 인 경우 
                if (fileName.endsWith(".class")) {
                    fileName = fileName.substring(0, fileName.length() - 6);
                    try{
                        // 해당 이름의 클래스 찾기
                        Class<?> c = Class.forName(packageName + "." + fileName);
                        // 인터페이스 제외
                        if (!c.isInterface()) {
                            classes.add(c);
                        }
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                // 디렉토리 일 경우 하위 디렉토리의 클래스 찾기
                } else {
                    setClasses(classes, packageName + "." +  fileName);
                }
            }
        }
    }

    // 스캔 제외 대상
    private boolean isExcluded(String fileName) {
        String[] exclude = {"WebApplicationContext", "MyDispatcherServlet"};
        for (String s : exclude) {
            if (fileName.contains(s)) return true;
        }
        return false;
    }

    // 디렉토리 경로
    private String getDirectoryString(String packageName) {
        String packageNameSlashed =
                "./" + packageName.replace(".", "/");
        URL packageDirURL =
                Thread.currentThread().getContextClassLoader().getResource(packageNameSlashed);
        return packageDirURL.getFile();
    }
}

DispatcherServlet

  • 모든 경로의 요청을 받는다.
  • urlPatterns를 "/" 로 한다. "/*"일 경우에는 jsp로 포워드 된 경우도 요청을 받아 무한루프에 빠진다..
@WebServlet(name = "dispatcherServlet", urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {

    private List<ViewResolver> viewResolvers;
    private List<HandlerAdapter> handlerAdapters;
    private Map<String, MappingRegistry> handlerMapping;

    // 서블릿 생성 시 WebApplicationContext 초기화
    public DispatcherServlet() {
        WebApplicationContext resources = WebApplicationContext.getInstance();
        this.viewResolvers = resources.getViewResolvers();
        this.handlerMapping = resources.getHandlerMapping();
        this.handlerAdapters = resources.getHandlerAdapters();
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");

        try {
            // 핸들러 찾기
            MappingRegistry handler = getHandler(request);
            if (handler != null) {
                // 핸들러 어댑터 찾기
                HandlerAdapter handlerAdapter = getHandlerAdapter(handler);
                if (handlerAdapter != null) {
                    // 핸들러 수행
                    String result = handlerAdapter.handle(request, response, handler);
                    if (result != null) {
                        // 렌더링
                        render(request, response, result);
                    }
                }
            } else {
                // 없으면 404.jsp
                request.getRequestDispatcher("/404.jsp").forward(request, response);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private MappingRegistry getHandler(HttpServletRequest request) {
        return handlerMapping.get(request.getRequestURI());
    }

    private HandlerAdapter getHandlerAdapter(MappingRegistry handler) {
        for (HandlerAdapter handlerAdapter : handlerAdapters) {
            if (handlerAdapter.supports(handler.getHandler())) {
                return handlerAdapter;
            }
        }
        return null;
    }

    private void render(HttpServletRequest request, HttpServletResponse response, String result) throws Exception {
        View view = resolveViewName(result);
        if (view != null) {
            view.render(request, response, result);
        }
    }

    private View resolveViewName(String path) throws Exception {

        for (ViewResolver viewResolver : viewResolvers) {
            View view = viewResolver.resolveViewName(path);
            if (view != null) {
                return view;
            }
        }
        return null;
    }
}

3. 테스트

  • 테스트 컨트롤러 생성
@Controller
public class TestController {

    @ResponseBody
    @RequestMapping("/hi")
    public String hi() {
        return "hi";
    }

    @RequestMapping("/main")
    public String main() {
        return "main.jsp";
    }
}
  • localhost:8080/hi -> body에 데이터 출력

  • localhost:8080/main -> main.jsp

  • 없는 요청 -> 404.jsp

  • 컨트롤러 추가해보기

@Controller
public class HelloController {

    @ResponseBody
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}
  • localhost:8080/hello

결론

  • 생각했던 결과대로 잘 동작하는 것을 확인
  • Spring 만드신 분들 정말 존경...

코드


4. 참고

profile
물흐르듯 개발하다 대박나기

1개의 댓글

comment-user-thumbnail
2021년 11월 18일

너무 좋은 글 감사합니다 잘 배우고 갑니다.

답글 달기