스프링 프레임워크 따라해보기 - 컨트롤러 매핑과 응답 전송

    앞에서는 웹 서버를 구축하여 Http 요청을 받고 기본적인 응답을 전송하는 기능을 구현했다.

    스프링 프로젝트를 해 본 사람이라면 모두 알겠지만, @ReuqestMapping, @Controller 등의 어노테이션을 통해서 url을 분석하고 이에 맞는 컨트롤러로 요청을 전달 하는 모습을 확인할 수 있었다.

     

    그렇다면, 이러한 과정이 어떻게 이루어지는지 간단하게 작성을 해보자.

     

    DispatcherServlet


    스프링 프레임워크에는 DispatcherServlet이라는 클래스가 있는데 이 클래스의 역할은 애플리케이션으로 들어오는 모든 요청을 핸들링하고 공통작업을 처리해준다고 한다. DispatcherServlet은 프론트 컨트롤러라고도 부르며, 서블릿 컨테이너의 맨 앞에서 모든 요청을 받아 처리해주는 컨트롤러를 의미한다고 한다.

    가령 작업을 할 때에 "/users"라는 path의 하위로 "/add", "/delete" 가 있을 때에, 우리는 메소드 상단에 @RequestMapping("/add") 와 같은 path만 적용하더라도 스프링 자체에서 알아서 해당 메소드를 찾아 로직을 실행하게 된다.

     

    먼저 컨트롤러들을 저장할 controllerMap, 핸들러들을 저장할 handlerMap과 추후 생성할 컨트롤러들을 탐색할 ClassPathScanner라는 클래스를 필요로 한다.

     

    과정을 어떻게 작성할 지에 고민을 했는데,

    1. 먼저 내가 작업하는 패키지들 내부의 java 파일을 모두 찾으며 각 클래스의 상단에 @Controller라고 적혀져 있는 클래스만 저장을 한다.

    2. 이제 저장을 한 클래스의 메소드 상단에 @RequestMapping 이라는 어노테이션이 있다면 어노테이션에서 path를 추출하고, url과 메소드를 저장한다.

    3. 그 후에 해당하는 url로 요청이 들어올 경우 해당 url의 메소드를 실행한다.

     

    의 방식으로 진행하면 될 것이라 생각했다.

     

    먼저 init을 통해 컨트롤러를 스캔한다.

    ClassLoader라는 클래스는 JVM의 구성요소 중 하나로 .class 바이트 코드를 읽어 class 객체를 생성한다고 한다.

    우리가 실제로 인텔리제이에서 코드를 실행해보면 소스코드 이외에도 컴파일 된 코드가 생성되는데, 이 때 파일들을 확인해보면 .java 파일이 컴파일 된 .class파일이 생성되어 있다. ClassLoader는 이러한 .class 파일들을 읽어서 클래스 객체를 생성한다.

    밑의 scanDirectroy 로직을 보면 현재 찾은 파일이 directory일 경우 재귀적으로 계속해서 들어가 .class 파일을 찾게 된다. 따라서 우리가 파일을 탐색할 특정할 위치를 정해준다면, 전체 파일이 아닌 특정한 위치서부터 탐색하며 탐색하는 시간을 줄일 수 있게 된다.

     

    또한, 우리는 현재 컨트롤러들을 등록하기 위해 클래스를 탐색하고 있기 때문에, 클래스 상단 어노테이션에 Controller 라는 클래스가 있는지 파악하고 존재한다면 이를 List에 등록하게 된다.

     

    그렇다면, 이러한 과정을 위해서 어노테이션을 생성해야 한다.

    여기서 리플렉션이라는 개념이 존재한다.

    리플렉션이란, 구체적인 클래스 타입을 알지 못해도, 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API이다.

    컴파일 타임이 아닌 런타임에서 동적으로 특정 클래스의 정보를 추출할 수 있다.

    즉, 클래스의 정보와 생성자, 메소드 등을 가져오기 때문에 우리가 원하고자 하는 정보를 얻어 매핑을 진행할 수 있게 된다.

    Retention 어노테이션의 경우에는 어노테이션이 언제까지 유효할 지, 어노테이션의 라이프 사이클을 정하는 것이다.

    현재 Controller의 Retention은 Runtime이기 때문에, 애플리케이션이 실행되는 동안 계속 유지된다는 것을 뜻한다.

     

    그럼 이제 예시용 컨트롤러를 하나 만들어보자.

    컨트롤러 클래스의 상단에 @Controller 어노테이션이 있기 때문에,
    if (clazz.isAnnotationPresent(Controller.class) 구문에 의해서 HelloController가 리스트에 추가되고 모든 @Controller 클래스를 찾은 리스트가 return되게 된다.

     

    스캔이 끝났다면, for문을 통해 List에 있는 class들을 파악하고 안에 있는 메소드들을 저장할 차례이다.

    먼저 controller 인스턴스를 생성 후에, 컨트롤러에 적용된 @RequestMapping 어노테이션을 가져오게 된다.

    getAnnotation을 통해 Annotation들을 반환받고, value() 메소드를 통해서 url 값을 가져올 수 있다.

    예시와 같은 코드에서 baseUrl은 "/hello" 가 되는 것이다.

     

    그 후에 controllerMap에 url과 컨트롤러 객체를 담은 후, 이제 컨트롤러 안의 메소드들을 차례로 순회한다.

    같은 방식으로 method를 탐색하며 @RequestMapping이 되어 있으면 어노테이션을 가져오고, baseUrl과 합쳐서

    "/hello/test"의 url을 메소드와 함께 handlerMap에 저장한다.

     

    여기까지 되면 간단하게나마 어노테이션을 통해 컨트롤러 객체들과 그 안의 메소드들을 매핑해둔 상태가 될 것이다.

     

    그 후에는, 요청의 path에 따라서 메소드를 가져오고, invoke() 메소드를 통해서 메소드 안의 내용을 실행한 후 Http 응답을 생성하여 write하게 해준다.

     

    여기서 invoke() 메소드의 경우에는 가변인자를 받는다.

    실제로 안의 내용을 확인해보면 가변인자를 받아서 처리하는 것을 볼 수 있다.

     

    만약, helloTest라는 컨트롤러에서는 인자를 아무것도 받지 않는데,

    Object result = handler.invoke(controller, request, response);

    와 같은 방식으로 넘긴다면

    이러한 인자 개수가 다르다는 오류 문구를 확인할 수 있다.

     

    따라서, 이후에 메소드들의 인자를 동적으로 받아서 invoke() 구문에 넣어주는 방식을 구현해야 한다.

     

    만약, 지금까지 상황에 대해서 이해하고 구현하였다면 실제로 localhost:8080/hello/test 에 접속할 시 다음과 같은 화면을 볼 수 있다.

    댓글