[Project Winter/#2] front-controller 패턴 적용하기

djawnstj·2023년 6월 9일
0

Project Winter

목록 보기
2/4

블로그 이전

본격적으로 프로젝트를 시작하기 앞서, Spring의 핵심인 DispatcherServlet을 생성하였다.

DispatcherServlet은 WAS 에서 받은 모든 Http 요청을 받은 다음에, 각 uri가 매핑된 컨트롤러를 찾아 개발자가 정의한 작업 수행을 호출하는 역할을 한다.

진행중인 프로젝트에서도 동일하게 'DispathcerServlet'이라는 이름으로 Servlet을 등록해 모든 요청을 받게 하였다.

DispatcherServlet

@WebServlet("/")
public class DispatcherServlet extends HttpServlet {
    private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);

    @Override
    public void init() throws ServletException {
        log.info("DispatcherServlet init() called.");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("DispatcherServlet service() called.");
    }
}

ServerRunner

서블릿 컨테이너는 Embedded Tomcat을 이용해 구현하였다.

public class ServerRunner {

    private static Tomcat tomcat;
    private static int port;
    private static String webappDirLocation;
    private static Thread serverThread;

    static {
        webappDirLocation = "webapps/";

        tomcat = new Tomcat();
        port = 8080;

        serverThread = new Thread(() -> {
            try {
                tomcat.setPort(port);
                tomcat.addWebapp("/", new File(webappDirLocation).getAbsolutePath());

                tomcat.start();
                tomcat.getServer().await();
            } catch (LifecycleException e) {
                e.printStackTrace();
            }

        });
    }

    public static void setPort(int port) {
        ServerRunner.port = port;
    }

    public static void setWebappDirLocation(String webappDirLocation) {
        ServerRunner.webappDirLocation = webappDirLocation;
    }

    public static void addLifecycleListener(LifecycleListener listener) {
        tomcat.getServer().addLifecycleListener(listener);
    }

    public static void startServer() {
        serverThread.start();
    }

}

setWebappDirLocation() 함수는 main 과 test 코드의 class 생성 경로를 다르게 하기 위해 개발하였다.

addLifecycleListener() 를 추가한것도 테스트 코드 실행 전 서버를 시작하기 위함인데,
Junit5BeforeAllCallback 으로 서버를 실행시키면 톰캣 서버가 채 실행되기 전에 테스트 코드가 실행된다.
톰캣 서버가 완전히 실행된 후에 테스트 코드를 실행시키기 위해 LifecycleListener 를 추가하기 위한 기능 개발을 했다.

TestServerExtension

public class TestServerExtension implements BeforeAllCallback {

    private static CountDownLatch serverStartedLatch = new CountDownLatch(1);

    @Override
    public void beforeAll(ExtensionContext context) throws Exception {
        ServerRunner.setWebappDirLocation("webapps/test/");
        ServerRunner.addLifecycleListener((event) -> {
            if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) serverStartedLatch.countDown();
        });
        ServerRunner.startServer();
        serverStartedLatch.await();
    }
}

Junit5BeforeAllCallback을 구현하여 테스트 코드가 실행 전에 톰캣 서버가 실행될 수 있도록 하였다.

ServerRunner에 정의한 addLifecycleListener()를 통해 톰캣 서버의 AFTER_START_EVENT 이벤트를 수신받아 CountDownLatchawait()countDown()을 호출하여 서버가 완전 실행된 후에 테스트 코드가 실행될 수 있도록 하였다.

이렇게 BeforeAllCallback을 구현하면 Test 코드를 실행하는 class에 @ExtendWith(TestServerExtension.class) 애노테이션을 붙이면 테스트 코드를 실행하기 전에 서버를 실행시켜준다.

하지만 이 애노테이션도 길다 느껴지기 때문에 간단한 편의 애노테이션을 개발했다.

@WinterServerTest

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TestServerExtension.class)
public @interface WinterServerTest {
}

이런 애노테이션을 개발해 간단히 @WinterServerTest 애노테이션만으로 테스트 코드 실행 전 서버를 실행시킬 수 있게 하였다.

DispatcherServlet 테스트 코드

@WinterServerTest
class ServerRunnerTest {

    @Test
    public void test_servlet_get_method() throws Exception {

        HttpClient httpClient = HttpClient.newHttpClient();
            HttpRequest httpRequest = HttpRequest.newBuilder()
                    .uri(URI.create("http://localhost:8080/get"))
                    .build();

            HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());

            assertEquals(response.statusCode(), 200);
    }

    @Test
    public void test_servlet_post_method() throws Exception {

        HttpClient httpClient = HttpClient.newHttpClient();
            HttpRequest httpRequest = HttpRequest.newBuilder()
                    .uri(URI.create("http://localhost:8080/post"))
                    .POST(HttpRequest.BodyPublishers.ofString(""))
                    .build();

            HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());

            assertEquals(response.statusCode(), 200);
    }

}

이렇게 GET, POST 메소드의 요청을 모두 정상적으로 받는지 확인하는 테스트 코드를 작성하였다.
실행 결과는 DispatcherServlet에서 모두 정상적으로 요청을 받았다.

Spring-MVC-흐름도
다음은 이 흐름도를 기반으로 2, 3, 4, 5 의 흐름에 해당하는 Controller 와 HandlerMapping 에 대한 기능 개발하려 한다.

profile
이용자가 아닌 개발자가 되자!

0개의 댓글