Project Winter
본격적으로 프로젝트를 시작하기 앞서, 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()
를 추가한것도 테스트 코드 실행 전 서버를 시작하기 위함인데,
Junit5
의 BeforeAllCallback
으로 서버를 실행시키면 톰캣 서버가 채 실행되기 전에 테스트 코드가 실행된다.
톰캣 서버가 완전히 실행된 후에 테스트 코드를 실행시키기 위해 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();
}
}
Junit5
의 BeforeAllCallback
을 구현하여 테스트 코드가 실행 전에 톰캣 서버가 실행될 수 있도록 하였다.
ServerRunner
에 정의한 addLifecycleListener()
를 통해 톰캣 서버의 AFTER_START_EVENT
이벤트를 수신받아 CountDownLatch
의 await()
와 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
에서 모두 정상적으로 요청을 받았다.
다음은 이 흐름도를 기반으로 2, 3, 4, 5 의 흐름에 해당하는 Controller 와 HandlerMapping 에 대한 기능 개발하려 한다.